108 lines
4.2 KiB
Swift
108 lines
4.2 KiB
Swift
import SwiftUI
|
|
|
|
/// A physical key. `latched` keeps it looking pressed (REC while recording).
|
|
public struct KeyButtonStyle: ButtonStyle {
|
|
public enum Kind { case standard, record }
|
|
|
|
private let kind: Kind
|
|
private let latched: Bool
|
|
|
|
public init(kind: Kind = .standard, latched: Bool = false) {
|
|
self.kind = kind
|
|
self.latched = latched
|
|
}
|
|
|
|
public func makeBody(configuration: Configuration) -> some View {
|
|
KeyBody(configuration: configuration, kind: kind, latched: latched)
|
|
}
|
|
|
|
private struct KeyBody: View {
|
|
let configuration: Configuration
|
|
let kind: Kind
|
|
let latched: Bool
|
|
@Environment(\.isEnabled) private var isEnabled
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
|
|
private var pressed: Bool { configuration.isPressed || latched }
|
|
|
|
private var face: AnyShapeStyle {
|
|
switch (kind, pressed) {
|
|
case (.standard, false):
|
|
AnyShapeStyle(LinearGradient(colors: [Palette.surfaceKeyHigh, Palette.surfaceKeyLow], startPoint: .top, endPoint: .bottom))
|
|
case (.standard, true):
|
|
AnyShapeStyle(Palette.surfaceKeyPressed)
|
|
case (.record, false):
|
|
AnyShapeStyle(Palette.accentRecord)
|
|
case (.record, true):
|
|
AnyShapeStyle(Palette.accentRecord.opacity(Opacity.pressedRecord))
|
|
}
|
|
}
|
|
|
|
var body: some View {
|
|
configuration.label
|
|
.textStyle(Typography.control)
|
|
.foregroundStyle(kind == .record ? Palette.inkInverse : Palette.inkPrimary)
|
|
.padding(.horizontal, Space.s4)
|
|
.frame(
|
|
minWidth: kind == .record ? Size.keyRecWidth : Size.keyMinWidth,
|
|
minHeight: kind == .record ? Size.keyRecHeight : Size.controlHeight)
|
|
.background(PhysicalSurface(
|
|
fill: face, cornerRadius: Radius.sm,
|
|
shadows: pressed ? Shadows.keyPressed : Shadows.keyRaised))
|
|
.offset(y: pressed ? Shadows.keyPressedOffset : 0)
|
|
.opacity(isEnabled ? 1 : Opacity.disabled)
|
|
.animation(
|
|
reduceMotion ? nil : .timingCurve(
|
|
Motion.easingMechanical.0, Motion.easingMechanical.1, Motion.easingMechanical.2, Motion.easingMechanical.3,
|
|
duration: pressed ? Motion.keyPress : Motion.keyRelease),
|
|
value: pressed)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// A round indicator lamp. The record lamp has a small glow; text never does.
|
|
public struct LampView: View {
|
|
public enum Tint { case record, amber }
|
|
|
|
private let isOn: Bool
|
|
private let tint: Tint
|
|
private let large: Bool
|
|
@Environment(\.colorScheme) private var scheme
|
|
@Environment(\.accessibilityReduceMotion) private var reduceMotion
|
|
|
|
public init(isOn: Bool, tint: Tint = .record, large: Bool = false) {
|
|
self.isOn = isOn
|
|
self.tint = tint
|
|
self.large = large
|
|
}
|
|
|
|
private var color: Color {
|
|
switch (tint, isOn) {
|
|
case (.record, true): Palette.accentRecord
|
|
case (.record, false): Palette.accentRecordUnlit
|
|
case (.amber, true): Palette.levelAmber
|
|
case (.amber, false): Palette.levelAmberUnlit
|
|
}
|
|
}
|
|
|
|
public var body: some View {
|
|
let diameter = large ? Size.lampLarge : Size.lampDiameter
|
|
let glow = Shadows.lampOn[0]
|
|
Circle()
|
|
.fill(color)
|
|
.overlay(
|
|
Circle().fill(RadialGradient(
|
|
colors: [Color.white.opacity(isOn ? Opacity.specularOn : Opacity.specularOff), .clear],
|
|
center: UnitPoint(x: 0.35, y: 0.3), startRadius: 0, endRadius: diameter * 0.45)))
|
|
.overlay(Circle().strokeBorder(Palette.lineSeamDark, lineWidth: Border.hairline))
|
|
.frame(width: diameter, height: diameter)
|
|
.shadow(
|
|
color: isOn && tint == .record ? glow.color(scheme) : .clear,
|
|
radius: glow.blur / 2)
|
|
.animation(
|
|
reduceMotion ? nil : .easeOut(duration: isOn ? Motion.lampOn : Motion.lampOff),
|
|
value: isOn)
|
|
.accessibilityHidden(true)
|
|
}
|
|
}
|