feat: add physical key button style and lamp
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user