Schließen entruckeln: Material fest, Maske animiert, Zeiger abgefragt
Drei Ursachen, davon zwei echte Fehler. Die NSVisualEffectView steckte in einem Rahmen, dessen Größe animiert wurde. Ein Weichzeichner muss dann in jedem Einzelbild neu in anderer Größe gerechnet werden — am teuersten genau beim Schließen, wo die Fläche am schnellsten schrumpft. Das Material behält jetzt feste Größe, animiert wird nur die Maske davor. Tint, Bänderung und Kante sind reine SwiftUI-Formen und dürfen weiter in der Größe animieren. Auch die Schattendeckkraft ist jetzt konstant statt animiert; sie aufzublenden kostete pro Bild eine neue Weichzeichnung. Onyx bekam Mausbewegungen über dem eigenen Panel praktisch nicht mit. Der globale Monitor feuert nur für Ereignisse an fremde Programme, der lokale nur wenn Onyx aktiv ist — was es als .accessory-App nie ist. Ausgerechnet beim Verlassen des Panels kam das Ereignis also verspätet oder gar nicht. Solange ein Panel offen ist, wird die Zeigerposition jetzt mit 60 Hz abgefragt; der Timer läuft ausschließlich in dieser Zeit. Öffnen und Schließen benutzten dieselbe Kurve. Schließen ist jetzt kürzer und ohne Nachschwingen: beim Öffnen sieht man zu, beim Schließen ist man schon woanders. Nebenbefund beim Prüfen: es liefen zwei Onyx-Instanzen aus verschiedenen Builds gleichzeitig, jede mit eigener Zustandsmaschine auf derselben Notch. Das dürfte einen guten Teil der Hakeligkeit erklärt haben.
This commit is contained in:
@@ -3,11 +3,17 @@ import AppKit
|
||||
/// Hält für jeden in Frage kommenden Bildschirm einen Controller und versorgt
|
||||
/// alle mit der Zeigerposition.
|
||||
///
|
||||
/// Die Zeigerposition kommt aus einem globalen Ereignismonitor statt aus einem
|
||||
/// unsichtbaren Fenster über der Notch. Ein solches Fenster müsste Mausereignisse
|
||||
/// annehmen, um sie zu bemerken — und würde damit die Menüleiste und die
|
||||
/// Fensterknöpfe darunter unbenutzbar machen. Mausbewegungen global zu beobachten
|
||||
/// verlangt anders als bei Tastatureingaben keine Bedienungshilfen-Berechtigung.
|
||||
/// Die Zeigerposition kommt aus zwei Quellen, und beide werden gebraucht:
|
||||
///
|
||||
/// **Ereignismonitor**, solange das Panel zu ist. Ein unsichtbares Fenster über
|
||||
/// der Notch wäre die naheliegende Alternative, müsste dafür aber Mausereignisse
|
||||
/// annehmen — und würde die Menüleiste und die Fensterknöpfe darunter
|
||||
/// unbenutzbar machen. Mausbewegungen global zu beobachten verlangt anders als
|
||||
/// bei Tastatureingaben keine Bedienungshilfen-Berechtigung.
|
||||
///
|
||||
/// **Abfrage**, solange es offen ist. Über dem eigenen Panel schweigen beide
|
||||
/// Monitore: der globale feuert nur für fremde Programme, der lokale nur wenn
|
||||
/// Onyx aktiv ist — was es als `.accessory`-App nie ist. Siehe `updatePolling()`.
|
||||
@MainActor
|
||||
public final class NotchCoordinator {
|
||||
|
||||
@@ -22,6 +28,8 @@ public final class NotchCoordinator {
|
||||
private var controllers: [CGDirectDisplayID: NotchScreenController] = [:]
|
||||
private var monitors: [Any] = []
|
||||
private var suppressionTimer: Timer?
|
||||
/// Läuft nur, solange ein Panel offen ist. Siehe `startPolling()`.
|
||||
private var pointerPollTimer: Timer?
|
||||
|
||||
/// Baut den Inhalt für einen Bildschirm. Ab Phase 2 liefert das die Widget-Ebene.
|
||||
/// Die `NotchPresentation` kommt mit, weil der Inhalt in den Animationsrahmen
|
||||
@@ -58,6 +66,8 @@ public final class NotchCoordinator {
|
||||
monitors.removeAll()
|
||||
suppressionTimer?.invalidate()
|
||||
suppressionTimer = nil
|
||||
pointerPollTimer?.invalidate()
|
||||
pointerPollTimer = nil
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
controllers.values.forEach { $0.tearDown() }
|
||||
controllers.removeAll()
|
||||
@@ -83,10 +93,14 @@ public final class NotchCoordinator {
|
||||
existing.update(metrics: metrics, virtualSize: virtualNotchSize)
|
||||
} else {
|
||||
let id = metrics.displayID
|
||||
controllers[id] = NotchScreenController(
|
||||
let controller = NotchScreenController(
|
||||
metrics: metrics,
|
||||
virtualSize: virtualNotchSize,
|
||||
makeContent: { presentation in makeContent(id, presentation) })
|
||||
controller.onVisibilityChanged = { [weak self] _ in
|
||||
self?.updatePolling()
|
||||
}
|
||||
controllers[id] = controller
|
||||
}
|
||||
}
|
||||
refreshSuppression()
|
||||
@@ -136,6 +150,35 @@ public final class NotchCoordinator {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fragt die Zeigerposition ab, solange ein Panel offen ist.
|
||||
///
|
||||
/// Über dem eigenen Panel bekommt Onyx sonst gar nichts mit: der globale
|
||||
/// Monitor feuert nur für Ereignisse, die an **fremde** Programme gehen, und
|
||||
/// der lokale nur, wenn Onyx das aktive Programm ist — was es als
|
||||
/// `.accessory`-App nie ist. Genau in dem Moment, in dem der Zeiger das
|
||||
/// Panel wieder verlässt, käme das Ereignis also verspätet oder gar nicht,
|
||||
/// und das Schließen wirkt hakelig.
|
||||
///
|
||||
/// Abfragen ist hier das ehrlichere Mittel als noch mehr Monitore: es läuft
|
||||
/// nur, während wirklich etwas offen ist, und kostet dabei einen
|
||||
/// Rechteckvergleich pro Bild.
|
||||
private func updatePolling() {
|
||||
let anyVisible = controllers.values.contains { $0.isVisible }
|
||||
|
||||
if anyVisible, pointerPollTimer == nil {
|
||||
let timer = Timer(timeInterval: 1.0 / 60.0, repeats: true) { _ in
|
||||
MainActor.assumeIsolated { [weak self] in self?.pointerMoved() }
|
||||
}
|
||||
// `.common` statt der Standard-Schleife: sonst steht die Abfrage,
|
||||
// während ein Menü offen ist oder gescrollt wird.
|
||||
RunLoop.main.add(timer, forMode: .common)
|
||||
pointerPollTimer = timer
|
||||
} else if !anyVisible {
|
||||
pointerPollTimer?.invalidate()
|
||||
pointerPollTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshSuppression() {
|
||||
for screen in NSScreen.screens {
|
||||
controllers[screen.displayID]?.setSuppressed(screen.isShowingFullScreenWindow)
|
||||
|
||||
Reference in New Issue
Block a user