import SwiftUI import OnyxDesign /// Einstellung der Auslösefläche — mit Vorschau. /// /// Zwei Zahlen in Punkten sagen niemandem etwas: „24 Punkte unter der Notch" /// ist keine Vorstellung, die man im Kopf hat. Ein nachgebildeter Bildschirm /// mit der Notch und der eingefärbten Fläche darüber schon — man sieht sofort, /// wie weit man greifen muss. public struct TriggerAreaSettings: View { @Binding private var trigger: TriggerArea public init(trigger: Binding) { self._trigger = trigger } public var body: some View { VStack(alignment: .leading, spacing: 12) { Text("trigger.hint", bundle: .module) .font(.callout) .foregroundStyle(.secondary) .fixedSize(horizontal: false, vertical: true) TriggerPreview(trigger: trigger) .frame(height: 150) .frame(maxWidth: .infinity) LabeledContent { Slider(value: Binding( get: { trigger.extraWidth }, set: { trigger = TriggerArea(extraWidth: $0, extraHeight: trigger.extraHeight) }), in: 0...TriggerArea.maximumExtraWidth) } label: { Text("trigger.width \(Int(trigger.extraWidth))", bundle: .module) .monospacedDigit() } LabeledContent { Slider(value: Binding( get: { trigger.extraHeight }, set: { trigger = TriggerArea(extraWidth: trigger.extraWidth, extraHeight: $0) }), in: 0...TriggerArea.maximumExtraHeight) } label: { Text("trigger.height \(Int(trigger.extraHeight))", bundle: .module) .monospacedDigit() } Button { trigger = .standard } label: { Text("trigger.reset", bundle: .module) } .buttonStyle(.borderless) } } } /// Der nachgebildete Bildschirm. /// /// Mit dem echten Schreibtischhintergrund: Apples Bilder lassen sich nicht /// mitliefern — Lizenz, und ein Tahoe-Hintergrund wiegt zweistellige Megabyte. /// Der eigene ist ohnehin der bessere Anker, weil man genau ihn wiedererkennt. /// Fehlt er, bleibt ein Verlauf in Onyx-Farben. private struct TriggerPreview: View { let trigger: TriggerArea /// Maße eines MacBook Pro mit Notch, auf die Vorschau heruntergerechnet. private let screenAspect: CGFloat = 1800 / 1169 private let notchWidthFraction: CGFloat = 220 / 1800 private let notchHeightFraction: CGFloat = 38 / 1169 var body: some View { GeometryReader { geometry in let height = min(geometry.size.height, geometry.size.width / screenAspect) let width = height * screenAspect let scale = width / 1800 let notchWidth = width * notchWidthFraction let notchHeight = height * notchHeightFraction let areaWidth = min(notchWidth + 2 * trigger.extraWidth * scale, width) let areaHeight = min(notchHeight + trigger.extraHeight * scale, height) ZStack(alignment: .top) { wallpaper // Die Menüleiste als heller Streifen: sie erklärt, warum die // Fläche nicht beliebig breit werden sollte. Rectangle() .fill(.black.opacity(0.35)) .frame(height: notchHeight) .frame(maxWidth: .infinity, alignment: .top) // Die Auslösefläche. Rectangle() .fill(Onyx.Color.accent.opacity(0.35)) .overlay { Rectangle().strokeBorder(Onyx.Color.accent, lineWidth: 1) } .frame(width: areaWidth, height: areaHeight) // Die Notch selbst — undurchsichtig, wie in echt. UnevenRoundedRectangle(bottomLeadingRadius: 5, bottomTrailingRadius: 5, style: .continuous) .fill(.black) .frame(width: notchWidth, height: notchHeight) } .frame(width: width, height: height) // Die Rundung zuletzt: sie schneidet Hintergrund, Menüleiste und // Fläche gemeinsam ab, sonst ragt etwas über die Ecken hinaus. .clipShape(.rect(cornerRadius: 8, style: .continuous)) .overlay { RoundedRectangle(cornerRadius: 8, style: .continuous) .strokeBorder(Onyx.Color.hairline, lineWidth: 1) } .frame(maxWidth: .infinity, maxHeight: .infinity) .animation(Onyx.Motion.value, value: areaWidth) .animation(Onyx.Motion.value, value: areaHeight) } } @ViewBuilder private var wallpaper: some View { if let image = Self.desktopImage { Image(nsImage: image) .resizable() .aspectRatio(contentMode: .fill) } else { LinearGradient(colors: [Onyx.Color.elevated, Onyx.Color.surface], startPoint: .top, endPoint: .bottom) } } /// Der Schreibtischhintergrund des Hauptbildschirms. /// /// Einmal geladen und behalten: es ist eine Datei von mehreren Megabyte, /// und die Vorschau zeichnet sich bei jedem Zug am Regler neu. private static let desktopImage: NSImage? = { guard let screen = NSScreen.main, let url = NSWorkspace.shared.desktopImageURL(for: screen) else { return nil } return NSImage(contentsOf: url) }() }