Files
onyx/Packages/OnyxKit/Sources/OnyxDesign/OnyxSurface.swift
T
ScarriffleandClaude Opus 5 ecb39fbe5f Eine Designschicht statt hundertfach gesetzter Zahlen
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>
2026-09-05 21:04:51 +02:00

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))
}
}