Files
onyx/Onyx/OnyxApp.swift
Guido Schmit 715031f153 Panel wächst aus der Notch, statt einzublenden
Die Fläche wird nicht eingeblendet — sie beginnt exakt in der Notch-Silhouette
und wächst von dort nach unten und zur Seite, deckungsgleich mit der schwarzen
Hardware darüber. Eingeblendetes Panel wirkt wie Glas; Onyx soll sich wie ein
fester Körper anfühlen, der herausfährt.

Der Inhalt hat eine eigene Kurve: beim Öffnen um 90 ms verzögert, damit die
Fläche vorweg läuft und kein Text sich mitdehnt; beim Schließen sofort weg,
damit beim Einfahren nichts gestaucht wird. Die Eckenrundung wandert mit
(10 → 26), sonst sieht die kleine Form aufgeblasen aus.

Drei Fallstricke, die im Code stehen:

Der Fensterschatten folgt dem Fensterrahmen, nicht der gezeichneten Form. Das
Fenster hat immer volle Panelgröße, damit die Animation Platz hat — der
Systemschatten läge also die ganze Zeit um eine unsichtbare Fläche. Deshalb
hasShadow = false und Schatten aus SwiftUI.

Ausgefahren wird erst im nächsten Runloop-Durchlauf. Im selben Durchlauf wie
orderFrontRegardless sieht SwiftUI keinen Zustandswechsel, sondern nur den
Endzustand, und das Panel wäre schlagartig da.

NotchHostingView nimmt Klicks nur dort an, wo etwas zu sehen ist. Sonst
verschluckt das volle Fenster während des Aus- und Einfahrens Klicks in seiner
leeren Fläche.
2026-08-10 17:25:27 +02:00

115 lines
3.8 KiB
Swift

import SwiftUI
import AppKit
import OnyxDesign
import OnyxNotch
import OnyxMenuBar
@main
struct OnyxApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate
var body: some Scene {
// Onyx hat kein Hauptfenster. Alles lebt im Notch-Panel und in der
// Menüleiste; die Einstellungen kommen in Phase 2 als eigenes Fenster.
Settings { EmptyView() }
}
}
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private var coordinator: NotchCoordinator?
private var menuBar: MenuBarController?
private var onyxItem: NSStatusItem?
func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.accessory)
let coordinator = NotchCoordinator(policy: .builtInOnly) { displayID, presentation in
NotchHostingView(
rootView: NotchContainer(presentation: presentation) {
NotchPanelContent(displayID: displayID)
},
presentation: presentation)
}
coordinator.start()
self.coordinator = coordinator
let menuBar = MenuBarController()
menuBar.start()
self.menuBar = menuBar
installOnyxStatusItem()
}
func applicationWillTerminate(_ notification: Notification) {
coordinator?.stop()
menuBar?.stop()
}
/// Das feste Onyx-Element: Panel öffnen, Einstellungen, Beenden. Es ist auch
/// das Sicherheitsnetz, falls die Notch einmal nicht reagiert.
private func installOnyxStatusItem() {
let item = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
item.autosaveName = "onyx.menubar.main"
item.button?.image = NSImage(systemSymbolName: "circle.hexagongrid.fill",
accessibilityDescription: "Onyx")
item.button?.image?.isTemplate = true
let menu = NSMenu()
menu.addItem(withTitle: "Panel öffnen", action: #selector(togglePanel), keyEquivalent: "")
.target = self
menu.addItem(.separator())
menu.addItem(withTitle: "Einstellungen …", action: #selector(openSettings), keyEquivalent: ",")
.target = self
menu.addItem(.separator())
menu.addItem(withTitle: "Onyx beenden", action: #selector(quit), keyEquivalent: "q")
.target = self
item.menu = menu
onyxItem = item
}
@objc private func togglePanel() { coordinator?.togglePanelUnderPointer() }
@objc private func openSettings() {
NSApp.activate(ignoringOtherApps: true)
NSApp.sendAction(Selector(("showSettingsWindow:")), to: nil, from: nil)
}
@objc private func quit() { NSApp.terminate(nil) }
}
/// Platzhalterinhalt für Phase 1. Er beweist, dass Fenster, Geometrie und
/// Zustandsmaschine zusammenspielen; die Widget-Ebene kommt in Phase 2.
private struct NotchPanelContent: View {
let displayID: CGDirectDisplayID
var body: some View {
ZStack {
OnyxSurface()
VStack(alignment: .leading, spacing: Onyx.Metric.tileSpacing) {
Text("Onyx")
.font(Onyx.Font.title)
.foregroundStyle(Onyx.Color.textPrimary)
Text("Phase 1 — Notch-Shell steht.")
.font(Onyx.Font.body)
.foregroundStyle(Onyx.Color.textSecondary)
HStack(spacing: Onyx.Metric.tileSpacing) {
ForEach(["Kalender", "Wetter", "Medien"], id: \.self) { name in
OnyxTile {
Text(name)
.font(Onyx.Font.caption)
.foregroundStyle(Onyx.Color.textTertiary)
}
}
}
}
.padding(Onyx.Metric.panelPadding)
}
}
}