Files
2026-10-01 15:35:37 +02:00

144 lines
5.6 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)
}
}
/// A two-position rocker switch: a key cap sliding in a recessed track.
public struct RockerToggleStyle: ToggleStyle {
public init() {}
public func makeBody(configuration: Configuration) -> some View {
HStack(spacing: Space.s4) {
configuration.label
Button {
configuration.isOn.toggle()
} label: {
RockerTrack(isOn: configuration.isOn)
}
.buttonStyle(.plain)
.accessibilityValue(configuration.isOn ? "aan" : "uit")
}
}
private struct RockerTrack: View {
let isOn: Bool
@Environment(\.accessibilityReduceMotion) private var reduceMotion
var body: some View {
ZStack(alignment: isOn ? .trailing : .leading) {
WellBackground()
PhysicalSurface(
fill: AnyShapeStyle(LinearGradient(colors: [Palette.surfaceKeyHigh, Palette.surfaceKeyLow], startPoint: .top, endPoint: .bottom)),
cornerRadius: Radius.sm, shadows: Shadows.keyRaised)
.frame(width: Size.switchWidth / 2, height: Size.switchHeight - Space.s2)
.padding(Space.s1)
}
.frame(width: Size.switchWidth, height: Size.switchHeight)
.animation(reduceMotion ? nil : .easeOut(duration: Motion.durationFast), value: isOn)
}
}
}