Kacheln und Fenster sprechen dieselbe Sprache
Alle Kacheln, Popovers und Fenster auf die Designschicht umgestellt: rund hundert gesetzte Schriftgrößen und 159 gesetzte Abstände und Radien sind jetzt Token. Übrig bleibt `spacing: 0` — das ist eine Aussage, keine geratene Zahl. Die Einstellungen bekommen eine Seitenleiste statt sieben Reitern, deren deutsche Beschriftungen zusammen 880 Punkte brauchten. Panel-Token stehen nicht mehr in Fenstern: dort gilt Systemsemantik, damit der Hellmodus nicht schwarz auf schwarz endet. Neu ist ein Schaufenster, das alle Bauteile nebeneinander zeigt — ohne das lässt sich eine Änderung an einem Token nur prüfen, indem man die App bedient, bis jede Kachel einmal zu sehen war. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
9b600de9a9
commit
edb327cabe
@@ -27,8 +27,9 @@ import ShelfProvider
|
||||
final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
|
||||
@ObservationIgnored private var window: NSWindow?
|
||||
/// Muss außerhalb der View liegen, damit ein erneutes Öffnen den Reiter setzen kann.
|
||||
private var selectedTab: SettingsTab = .widgets
|
||||
/// Muss außerhalb der View liegen, damit ein erneutes Öffnen den Bereich
|
||||
/// setzen kann.
|
||||
private var section: SettingsSection = .general
|
||||
|
||||
/// Ob Onyx dauerhaft im Dock steht. Bestimmt, wohin nach dem Schließen
|
||||
/// zurückgeschaltet wird.
|
||||
@@ -37,8 +38,8 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
func show(model: AppModel, calendarModel: CalendarModel, weatherModel: WeatherModel,
|
||||
metricsModel: MetricsModel?, shelfStore: ShelfStore?, fanControl: FanControl,
|
||||
launchAtLogin: LaunchAtLogin, showOnboarding: @escaping () -> Void,
|
||||
tab: SettingsTab = .widgets) {
|
||||
selectedTab = tab
|
||||
section: SettingsSection = .general) {
|
||||
self.section = section
|
||||
keepsDockIcon = model.showsDockIcon
|
||||
|
||||
// Solange ein Fenster offen ist, gehört Onyx in den Dock.
|
||||
@@ -59,39 +60,48 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
fanControl: fanControl,
|
||||
launchAtLogin: launchAtLogin,
|
||||
showOnboarding: showOnboarding,
|
||||
selectedTab: Binding(
|
||||
get: { [weak self] in self?.selectedTab ?? .widgets },
|
||||
set: { [weak self] in self?.selectedTab = $0 })))
|
||||
section: Binding(
|
||||
get: { [weak self] in self?.section ?? .general },
|
||||
set: { [weak self] in self?.section = $0 })))
|
||||
|
||||
let window = NSWindow(contentViewController: hosting)
|
||||
let window = NSWindow(contentViewController: OnyxBackdropController(content: hosting))
|
||||
window.title = "Onyx"
|
||||
// Der Titelbalken geht in den Inhalt über, **und** seine Trennlinie
|
||||
// verschwindet.
|
||||
// **Kein durchsichtiger Titelbalken mehr.**
|
||||
//
|
||||
// `titlebarAppearsTransparent` allein reichte nicht: macOS zieht
|
||||
// unter dem Titelbalken zusätzlich einen Trenner quer durchs
|
||||
// Fenster, und die Reiterleiste saß mittig darauf. Man setzt keinen
|
||||
// Titel auf eine Trennlinie.
|
||||
// Er war für die selbstgebaute Reiterleiste da: macOS zieht unter
|
||||
// dem Titelbalken einen Trenner quer durchs Fenster, und die
|
||||
// Leiste saß mittig darauf. Die Leiste gibt es nicht mehr — und
|
||||
// durchsichtig ist er jetzt schädlich: Seitenleiste und Inhalt
|
||||
// zeichnen darunter weiter, statt darunter zu beginnen. Sichtbar
|
||||
// wurde das im Widgets-Bereich, wo die Liste unter den
|
||||
// Fensterknöpfen hindurchlief.
|
||||
window.titlebarSeparatorStyle = .automatic
|
||||
// **Kein `isOpaque = false`, kein durchsichtiger Hintergrund.**
|
||||
//
|
||||
// Beides klingt nach dem Weg zur Durchlässigkeit und ist ihr
|
||||
// Gegenteil, sobald die Vibrancy-Ebene selbst der Fensterinhalt
|
||||
// ist: erklärt sich das Fenster für durchsichtig, stellt der
|
||||
// Fensterserver ihm keinen Hintergrund mehr zum Abtasten bereit,
|
||||
// und `behindWindow` hat nichts, woraus es Unschärfe machen könnte.
|
||||
// Der Titelbalken teilt sich die Fläche mit dem Inhalt. Ohne das
|
||||
// zeichnet macOS dort seine **eigene**, deutlich durchlässigere
|
||||
// Vibrancy — sichtbar als heller Streifen quer über den oberen
|
||||
// Rand, durch den man den Hintergrund lesen kann.
|
||||
window.titlebarAppearsTransparent = true
|
||||
window.titlebarSeparatorStyle = .none
|
||||
// Veränderbar, und das ist keine Kür: der Bereich „Displays" mit
|
||||
// der Vorschau der Auslösefläche passt in 420 Punkte Höhe nicht
|
||||
// mehr, und ein Fenster, in dem man scrollen muss, obwohl der
|
||||
// Bildschirm frei ist, ist eine Zumutung.
|
||||
window.styleMask = [.titled, .closable, .miniaturizable, .resizable]
|
||||
window.setContentSize(NSSize(width: 920, height: 680))
|
||||
// Breit genug für die Reiterleiste.
|
||||
//
|
||||
// Sieben Reiter mit deutschen Beschriftungen brauchen rund 880
|
||||
// Punkte — mehr, als man beim Nachrechnen der reinen Textbreiten
|
||||
// herausbekommt: macOS legt je Reiter Innenabstand und Trenner
|
||||
// dazu. Ist das Fenster schmaler, ragt die Leiste über beide
|
||||
// Kanten hinaus, und der erste wie der letzte Reiter sind nicht
|
||||
// mehr erreichbar.
|
||||
window.contentMinSize = NSSize(width: 900, height: 460)
|
||||
window.setContentSize(NSSize(width: 940, height: 700))
|
||||
// Seit die Bereiche in einer Seitenleiste stehen, hängt die
|
||||
// Mindestbreite am Inhalt statt an der Beschriftung: das
|
||||
// Reiterband brauchte 900 Punkte, nur damit der erste und der
|
||||
// letzte Reiter nicht über die Kante hinausragten.
|
||||
window.contentMinSize = NSSize(width: 780, height: 480)
|
||||
window.isReleasedWhenClosed = false
|
||||
window.delegate = self
|
||||
Self.place(window)
|
||||
OnyxWindowPlacement.center(window)
|
||||
// Über die Position freuen sich Nutzer mit mehreren Displays:
|
||||
// beim zweiten Öffnen steht es wieder da, wo sie es hingeschoben haben.
|
||||
window.setFrameAutosaveName("onyx.settings")
|
||||
@@ -104,27 +114,6 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
window?.makeKeyAndOrderFront(nil)
|
||||
}
|
||||
|
||||
/// Setzt das Fenster so, dass seine Titelleiste **nicht** an der Notch
|
||||
/// klebt.
|
||||
///
|
||||
/// `center()` setzt etwas oberhalb der Mitte — bei einem 452 Punkt hohen
|
||||
/// Fenster landet der Schließknopf damit knapp unter der Menüleiste. Wer
|
||||
/// ihn treffen will, fährt durch die Auslösefläche der Notch und hat
|
||||
/// plötzlich das Panel vor sich.
|
||||
private static func place(_ window: NSWindow) {
|
||||
guard let screen = window.screen ?? NSScreen.main else { window.center(); return }
|
||||
let visible = screen.visibleFrame
|
||||
let size = window.frame.size
|
||||
// Wirklich mittig, nicht „etwas unterhalb der Notch".
|
||||
//
|
||||
// `center()` setzt oberhalb der Bildschirmmitte, und mein erster
|
||||
// Versuch mit 60 Punkten Abstand war zu zaghaft: der Titelbalken lag
|
||||
// weiter so hoch, dass man auf dem Weg zu seinen Knöpfen durch die
|
||||
// Auslösefläche der Notch fuhr und aus Versehen das Panel öffnete.
|
||||
window.setFrameOrigin(NSPoint(x: visible.midX - size.width / 2,
|
||||
y: visible.midY - size.height / 2))
|
||||
}
|
||||
|
||||
/// Der rote Knopf blendet aus, statt zu schließen.
|
||||
///
|
||||
/// Onyx ist eine App ohne Hauptfenster; „Fenster zu" heißt hier nie
|
||||
@@ -147,3 +136,116 @@ final class SettingsWindowController: NSObject, NSWindowDelegate {
|
||||
NSApp.hide(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Legt eine Vibrancy-Ebene **unter das ganze Fenster**, Titelbalken
|
||||
/// eingeschlossen.
|
||||
///
|
||||
/// In SwiftUI ginge das nicht: ein `.background` dort endet an der sicheren
|
||||
/// Fläche, und über dem oberen Rand blieb ein Streifen, durch den man den
|
||||
/// Hintergrund lesen konnte. Hier liegt die Ebene im Fensterinhalt und wächst
|
||||
/// mit ihm.
|
||||
///
|
||||
/// Zwei Schichten, nicht eine: die Vibrancy allein ist zu durchlässig — man
|
||||
/// liest den Schreibtisch mit, statt die Einstellung. Der getönte Grund darüber
|
||||
/// lässt die Unschärfe stehen und nimmt ihr die Lesbarkeit.
|
||||
@MainActor
|
||||
final class OnyxBackdropController: NSViewController {
|
||||
|
||||
/// Wie viel vom Fensterhintergrund über der Vibrancy stehen bleibt.
|
||||
///
|
||||
/// 1 wäre undurchlässig, 0 ließe das nackte Material stehen — dann ist der
|
||||
/// Hintergrund stellenweise mitzulesen. **Das ist die Schraube**, wenn es
|
||||
/// zu durchlässig oder zu deckend wirkt.
|
||||
static let tintOpacity: CGFloat = 0
|
||||
|
||||
private let content: NSViewController
|
||||
|
||||
init(content: NSViewController) {
|
||||
self.content = content
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) wird nicht benutzt") }
|
||||
|
||||
override func loadView() {
|
||||
// Die Vibrancy-Ebene **ist** der Fensterinhalt, sie liegt nicht darin.
|
||||
//
|
||||
// Als Unteransicht in einem gewöhnlichen `NSView` blieb sie wirkungslos
|
||||
// — sichtbar wurde nur der Titelbalken, den macOS selbst zeichnet. Als
|
||||
// `contentView` greift `behindWindow` zuverlässig, und zusammen mit
|
||||
// `.fullSizeContentView` am Fenster reicht sie auch unter den
|
||||
// Titelbalken.
|
||||
let container = NSVisualEffectView()
|
||||
// `.sidebar` und nicht `.underWindowBackground`: letzteres ist so
|
||||
// deckend, dass man von der Unschärfe nichts sieht, sobald hinter dem
|
||||
// Fenster etwas Dunkles liegt. `.sidebar` ist das Material, das man von
|
||||
// Finder und Systemeinstellungen kennt.
|
||||
container.material = .sidebar
|
||||
container.blendingMode = .behindWindow
|
||||
// `.active` statt `.followsWindowActiveState`: sonst fällt die Ebene
|
||||
// zurück, sobald das Fenster nicht vorn ist — dann sieht dasselbe
|
||||
// Fenster je nach Aufmerksamkeit verschieden aus, und das liest sich
|
||||
// als Fehler, nicht als Zustand.
|
||||
container.state = .active
|
||||
|
||||
let tint = TintView()
|
||||
tint.autoresizingMask = [.width, .height]
|
||||
container.addSubview(tint)
|
||||
|
||||
addChild(content)
|
||||
content.view.autoresizingMask = [.width, .height]
|
||||
// Ohne das bleibt die Vibrancy unsichtbar: `NSHostingView` legt einen
|
||||
// deckenden Grund unter den SwiftUI-Inhalt, und der steht genau
|
||||
// dazwischen. Nur der Titelbalken kam vorbei — er wird von macOS
|
||||
// gezeichnet, nicht von SwiftUI.
|
||||
content.view.wantsLayer = true
|
||||
content.view.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
container.addSubview(content.view)
|
||||
|
||||
self.view = container
|
||||
}
|
||||
|
||||
override func viewDidLayout() {
|
||||
super.viewDidLayout()
|
||||
for subview in view.subviews { subview.frame = view.bounds }
|
||||
}
|
||||
|
||||
/// Der Titelbalken bekommt keinen eigenen Grund — er liegt auf derselben
|
||||
/// Fläche wie alles andere.
|
||||
override func viewDidAppear() {
|
||||
super.viewDidAppear()
|
||||
view.window?.styleMask.insert(.fullSizeContentView)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Der getönte Grund über der Vibrancy.
|
||||
///
|
||||
/// Eine eigene Klasse, weil die Farbe erscheinungsabhängig ist: eine einmal
|
||||
/// gelesene `CGColor` bliebe beim Wechsel auf Hell dunkel stehen.
|
||||
/// `viewDidChangeEffectiveAppearance` gibt es nur an der View, nicht am
|
||||
/// Controller — daran ist der erste Versuch gescheitert.
|
||||
private final class TintView: NSView {
|
||||
|
||||
override var wantsUpdateLayer: Bool { true }
|
||||
|
||||
override init(frame: NSRect) {
|
||||
super.init(frame: frame)
|
||||
wantsLayer = true
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) { fatalError("init(coder:) wird nicht benutzt") }
|
||||
|
||||
override func updateLayer() {
|
||||
effectiveAppearance.performAsCurrentDrawingAppearance {
|
||||
layer?.backgroundColor = NSColor.windowBackgroundColor
|
||||
.withAlphaComponent(OnyxBackdropController.tintOpacity).cgColor
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidChangeEffectiveAppearance() {
|
||||
super.viewDidChangeEffectiveAppearance()
|
||||
needsDisplay = true
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user