Files
onyx/Packages/OnyxKit/Sources/OnyxNotch/NotchPanel.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

73 lines
3.0 KiB
Swift

import AppKit
/// Das Fenster, in dem das Notch-Panel lebt.
///
/// Zwei Anforderungen kollidieren hier: es muss über allem liegen, auch über der
/// Menüleiste und es darf der aktiven App niemals den Fokus wegnehmen. Wer beim
/// Blick auf den Kalender den Cursor aus seinem Editor verliert, benutzt das
/// Panel kein zweites Mal.
public final class NotchPanel: NSPanel {
public init(contentRect: NSRect) {
super.init(contentRect: contentRect,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false)
isFloatingPanel = true
// Ohne das wird das Panel beim ersten Klick zum Schlüsselfenster und
// entzieht der aktiven App den Fokus.
becomesKeyOnlyIfNeeded = true
hidesOnDeactivate = false
// Über der Menüleiste, aber unter Bildschirmschoner und Anmeldefenster.
level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.statusWindow)) + 1)
// Auf allen Schreibtischen sichtbar und auch neben Vollbildfenstern,
// ohne beim Umschalten mitzuwandern.
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary,
.stationary, .ignoresCycle]
backgroundColor = .clear
isOpaque = false
// Der Fensterschatten folgt dem Fensterrahmen, nicht der gezeichneten
// Form. Da das Fenster immer die volle Panelgröße hat, läge der Schatten
// während der ganzen Animation um eine unsichtbare Fläche. Den Schatten
// zeichnet deshalb SwiftUI, wo er der wachsenden Form folgt.
hasShadow = false
isMovable = false
isMovableByWindowBackground = false
animationBehavior = .none
// Nicht in Bildschirmaufnahmen und Fensterlisten auftauchen.
isExcludedFromWindowsMenu = true
}
/// Randlose Fenster gelten sonst als nicht fokussierbar das bräuchten wir
/// aber, damit `esc` im fixierten Zustand ankommt.
public override var canBecomeKey: Bool { true }
public override var canBecomeMain: Bool { false }
}
/// Der unsichtbare Fühler über der Notch.
///
/// Er zeichnet nichts und nimmt keine Klicks an `ignoresMouseEvents` bleibt an,
/// sonst würden die Menüleiste und die Fensterknöpfe darunter unerreichbar.
/// Die Zeigerposition kommt aus einem globalen Monitor, nicht aus diesem Fenster;
/// das Fenster dient nur der Sichtprüfung beim Entwickeln.
public final class NotchDebugOverlay: NSPanel {
public init(frame: NSRect) {
super.init(contentRect: frame,
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false)
level = NSWindow.Level(rawValue: Int(CGWindowLevelForKey(.statusWindow)) + 2)
collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
backgroundColor = NSColor.systemPink.withAlphaComponent(0.25)
isOpaque = false
hasShadow = false
ignoresMouseEvents = true
}
}