feat: add physical key button style and lamp

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 15:23:56 +02:00
co-authored by Claude Sonnet 5.5
parent 9ac93bc868
commit 529934b177
+107
View File
@@ -0,0 +1,107 @@
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)
}
}