Token für Schrift, Symbolgröße, Abstand, Radius und Bewegung, dazu sieben Bauteile, die vorher in jeder Kachel neu gebaut wurden — Karte, Abschnittstitel, Hinweis, Pillenknopf, Regler, Ablegefläche, Haarlinie. Symbole bekommen eine **eigene** Staffel und nicht die der Schrift: ein 9-Punkt-Symbol neben 9-Punkt-Text wirkt kleiner, nicht gleich groß. Beide in eine Staffel zu zwingen hieße, an jeder Stelle gegen sie zu arbeiten. Das Flächenrezept — Tint, Bänderung, Kante — stand zweimal da, einmal für das Panel und einmal für Fenster. Es ist jetzt `OnyxStone`, generisch über die Form, damit auch die animierende Notch-Silhouette es benutzen kann. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
111 lines
4.2 KiB
Swift
111 lines
4.2 KiB
Swift
import SwiftUI
|
|
import AppKit
|
|
|
|
/// Die Vibrancy-Ebene hinter dem Panel.
|
|
///
|
|
/// `.hudWindow` mit `behindWindow` lässt den Hintergrund durchscheinen, ohne ihn
|
|
/// lesbar zu lassen. Der Tint darüber macht daraus schwarzen Stein statt grauem Glas.
|
|
public struct OnyxVisualEffect: NSViewRepresentable {
|
|
public init() {}
|
|
|
|
public func makeNSView(context: Context) -> NSVisualEffectView {
|
|
let view = NSVisualEffectView()
|
|
view.material = .hudWindow
|
|
view.blendingMode = .behindWindow
|
|
view.state = .active
|
|
view.appearance = NSAppearance(named: .darkAqua)
|
|
return view
|
|
}
|
|
|
|
public func updateNSView(_ view: NSVisualEffectView, context: Context) {}
|
|
}
|
|
|
|
/// Der Stein selbst — auf einer beliebigen Form.
|
|
///
|
|
/// Tint, Bänderung, Kante. Drei Lagen, die zusammen den Werkstoff ergeben, und
|
|
/// zwar unabhängig davon, welche Form sie annehmen.
|
|
///
|
|
/// **Warum generisch und nicht einfach ein abgerundetes Rechteck.** Das Panel
|
|
/// zeichnet auf ``NotchShape``, einer Form, die sich beim Aus- und Einfahren
|
|
/// verändert. Solange das Rezept an ein Rechteck gebunden war, konnte es dort
|
|
/// nicht mitbenutzt werden — also stand es zweimal da, einmal hier und einmal
|
|
/// im Panel. Zweimal heißt: eine Änderung an der Bänderung wirkt an einer
|
|
/// Stelle und an der anderen nicht, und das fällt erst auf, wenn beide
|
|
/// nebeneinander zu sehen sind.
|
|
public struct OnyxStone<S: InsettableShape>: View {
|
|
private let shape: S
|
|
|
|
public init(_ shape: S) {
|
|
self.shape = shape
|
|
}
|
|
|
|
public var body: some View {
|
|
// Aus dem Thema gelesen und nicht als Argument gereicht: sonst müsste
|
|
// jede Stelle, die eine Fläche zeichnet, den Werkstoff durchschleifen —
|
|
// bis hinunter zur Kachel, die ihn gar nicht kennt.
|
|
let material = OnyxTheme.shared.material
|
|
|
|
shape
|
|
.fill(Onyx.Color.surface.opacity(material.fillOpacity))
|
|
.overlay {
|
|
// Die Bänderung des Steins: oben ein Hauch heller, nach 40 %
|
|
// vollständig verlaufen. Mehr wirkt sofort billig — wie viel
|
|
// „ein Hauch" ist, hängt am Werkstoff.
|
|
shape.fill(
|
|
LinearGradient(
|
|
stops: [
|
|
.init(color: .white.opacity(material.sheen), location: 0),
|
|
.init(color: .clear, location: 0.4),
|
|
],
|
|
startPoint: .top, endPoint: .bottom))
|
|
}
|
|
.overlay {
|
|
// `strokeBorder` und nicht `stroke`: die Linie liegt dann
|
|
// vollständig **innen**. Mittig gezeichnet ragte sie zur Hälfte
|
|
// über die Kante hinaus und wurde an den Rundungen sichtbar
|
|
// dünner.
|
|
shape.strokeBorder(material.edge, lineWidth: Onyx.Metric.hairlineWidth)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Der Hintergrund des Panels: Vibrancy und darauf der Stein.
|
|
public struct OnyxSurface: View {
|
|
private let cornerRadius: CGFloat
|
|
|
|
public init(cornerRadius: CGFloat = Onyx.Radius.panel) {
|
|
self.cornerRadius = cornerRadius
|
|
}
|
|
|
|
public var body: some View {
|
|
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
|
OnyxStone(shape)
|
|
.background {
|
|
// Bei deckendem Stein trägt die Weichzeichnung nichts bei und
|
|
// wird auch nicht berechnet.
|
|
if OnyxTheme.shared.material.showsVibrancy {
|
|
OnyxVisualEffect().clipShape(shape)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Eine Kachel auf der Panelfläche.
|
|
public struct OnyxTile<Content: View>: View {
|
|
private let content: Content
|
|
|
|
public init(@ViewBuilder content: () -> Content) {
|
|
self.content = content()
|
|
}
|
|
|
|
public var body: some View {
|
|
let shape = RoundedRectangle(cornerRadius: Onyx.Radius.tile, style: .continuous)
|
|
|
|
content
|
|
.padding(Onyx.Space.lg)
|
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
|
.background(shape.fill(Onyx.Color.elevated))
|
|
.overlay(shape.strokeBorder(Onyx.Color.hairline, lineWidth: Onyx.Metric.hairlineWidth))
|
|
}
|
|
}
|