Commit Graph

6 Commits

Author SHA1 Message Date
Scarriffle
5ba9e551a4 Ladelimit gehört zum Akku, nicht zu den Lüftern
Der Regler stand in der Lüfterkachel, weil dort der Helfer schon angebunden
war. Das ist die Sicht des Programmierers, nicht die des Nutzers: gesucht
wird er beim Akku, und zwar genau dann, wenn man auf den Ladestand schaut.

Er steht jetzt in der Akku-Kachel des Panels und im Akku-Popover der
Menüleiste. Angebunden über ein Protokoll statt über einen direkten Zugriff:
geschrieben wird vom privilegierten Helfer, und der gehört zur App, nicht zu
MetricsProvider — die Akku-Ansicht soll ihn bedienen können, ohne ihn zu
kennen. Ohne laufenden Helfer ist der Regler sichtbar, aber gesperrt, mit dem
Hinweis, wo er herkommt. Ein Regler, der still nichts tut, ist schlimmer als
einer, der sagt warum.

Dazu Protokollierung, warum das Panel schließt: Zeigerposition,
Auslösefläche, Fensterrahmen. „Es ging zu, obwohl ich noch drin war" ist
ohne diese drei Zahlen eine Behauptung gegen eine andere — und meine
automatisierten Proben halten den Zeiger perfekt still, treffen den Fall
also nie.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:20:00 +02:00
Scarriffle
a54b332723 Notch: Toleranz für zitternde Hände, „Panel öffnen" fliegt raus
Warum meine Proben grün waren und deine Hand nicht: jedes einzelne
Abtastbild außerhalb der Auslösefläche hat das Aufziehen sofort
abgebrochen. Ein Zeiger, den eine Hand an den oberen Rand führt, steht
dort aber nicht still — er wandert um ein paar Punkte. Damit fing die
Entprellzeit von 220 ms dauernd von vorn an und kam nie ans Ziel.
`CGWarpMouseCursorPosition` setzt den Zeiger auf einen Punkt und hält ihn
absolut still; deshalb war jede automatisierte Probe 19 von 19 grün.

Kurzes Herausrutschen bricht jetzt nicht mehr ab: die Entprellzeit läuft
weiter, parallel läuft die Abbruchfrist von 300 ms. Kommt der Zeiger
zurück, wird die Frist gestoppt — die Entprellzeit setzt bewusst nicht neu
an, sonst dauerte es bei jedem Grenzübertritt wieder von vorn.

Das allein hätte einen neuen Fehler gebracht, und ein vorhandener Test hat
ihn gefunden: beim beiläufigen Streifen der Notch wäre die Entprellzeit
abgelaufen, während der Zeiger längst weg war — das Panel wäre für ein
Zehntel aufgeblitzt. Die Maschine merkt sich deshalb, wo der Zeiger steht,
und öffnet nur, wenn er da ist. Ist die Entprellzeit abgelaufen, während er
draußen war, geht es beim Zurückkommen ohne weiteres Warten auf.

Die Auslösefläche reicht außerdem 14 statt 4 Punkte unter die Notch. Vier
Punkte sind keine Fläche, die eine Hand trifft.

„Panel öffnen" ist aus dem Menü verschwunden. Ein Menüeintrag für etwas,
das beim Berühren der Notch von selbst passiert, beschreibt einen Umweg um
die eigentliche Bedienung herum. Als Notausgang bleibt es erreichbar, nur
ohne eigene Zeile: Linksklick auf das Symbol fährt das Panel aus,
Rechtsklick zeigt Einstellungen und Beenden.

Dazu Protokollierung jedes Zustandswechsels und jeder Unterdrückung. Ohne
diese Spur ist ein Panel, das sich „manchmal nicht öffnet", nicht zu
untersuchen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-11 12:12:31 +02:00
Guido Schmit
0297b3ad0d 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.
2026-08-10 17:52:42 +02:00
Guido Schmit
65f9fbfa4e Inhalt beginnt unterhalb der Notch statt dahinter
Das Panel reichte oben bündig an die Bildschirmkante, damit seine Fläche mit
der schwarzen Notch-Hardware zu einem Körper verschmilzt. Die Notch ist aber
undurchsichtig — die obersten 38 pt Inhalt waren schlicht nicht zu sehen. Links
und rechts daneben liegt in diesem Streifen zusätzlich die Menüleiste, der ganze
Streifen ist also unbrauchbar.

Die Fläche bleibt deshalb, wo sie war; nur der Inhalt rückt nach unten. Das
Fenster wächst entsprechend mit (446x340 → 446x378), damit unten nichts
abgeschnitten wird.

Der Controller nimmt jetzt die Inhaltsgröße entgegen und schlägt den Freiraum
selbst auf. Keine Aufrufstelle muss daran denken, und beim Wechsel zwischen
eingebautem Display (38 pt) und externem (virtueller Balken) wandert der
Freiraum automatisch mit.

Gegengemessen statt geschätzt: die Zentrierung war korrekt (Panel-midX 900 =
Notch-midX 900), der Fehler lag allein in der Höhe. Vier Tests halten den
Inhaltsbereich jetzt außerhalb der Notch fest, auch für den virtuellen Balken.

70 Tests grün.
2026-08-10 17:43:30 +02:00
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
Guido Schmit
fbe13fd1cb Phase 1: Notch-Shell, Design-System, Menüleisten-Infrastruktur
Zustandsmaschine und Geometrie sind testgetrieben entstanden und tragen 40
Tests — sie sind frei von AppKit, damit jeder Übergang ohne Fenster und ohne
Warten prüfbar ist. Zeit kommt nur als Ereignis herein.

Zwei Entscheidungen, die im Code begründet sind:

Der Zeiger wird über einen globalen Ereignismonitor verfolgt, nicht über ein
unsichtbares Fenster auf der Notch. Ein solches Fenster müsste Mausereignisse
annehmen, um sie zu bemerken, und würde damit Menüleiste und Fensterknöpfe
darunter unbenutzbar machen.

"Nur internes Display" fällt auf ein externes zurück, wenn kein eingebautes da
ist. Am Dock mit geschlossenem Deckel hieße die Einstellung wörtlich genommen,
dass Onyx unerreichbar wird.

Design: NSVisualEffectView mit eigenem Tint statt Liquid Glass — Onyx ist Stein,
kein Glas. Alle Farben und Maße liegen als Tokens in OnyxDesign.

Menüleiste: jedes Modul bekommt ein eigenes NSStatusItem. Elemente, denen macOS
mangels Platz keine Breite gibt, werden erkannt und gemeldet, statt still zu
verschwinden.

App-Target über XcodeGen, damit die Projektdefinition im Diff lesbar bleibt.
Nicht sandboxed, mit App-Group-Entitlement — baut, startet, signiert mit
PP34X97WS3.
2026-08-10 17:17:16 +02:00