Files
onyx/Packages/OnyxKit/Tests/OnyxNotchTests/NotchGeometryTests.swift
T
ScarriffleandClaude Opus 5 c05ccb8ae1 Die Notch wird zur Insel, wo es keine Hardware gibt
Die Silhouette bestand aus zwei überlappenden Teilpfaden. Gefüllt stimmte die
Vereinigung, aber die Kontur zeichnete jeden Teilpfad vollständig nach — auch
dort, wo er innen lag. Dazu kippte die Form ab einer Breite in einen anderen
Zweig, und die beiden Zweige waren nicht dieselbe Geometrie. Zusammen war das
das Flackern an den Rundungen. Jetzt ein durchgehender Umriss, dessen Zweige
ineinander übergehen statt zu springen.

Neu ist die Wahl zwischen verschmolzen und schwebend. Eine echte Notch **muss**
an der Kante kleben und oben eckig sein, sonst klafft ein Spalt zum Gehäuse;
eine Insel **muss** Abstand halten und rundum gerundet sein, sonst sieht sie aus
wie ein abgeschnittenes Rechteck. Beides zu mischen geht nicht, deshalb eine
Wahl und keine zwei Schalter.

Ihre Höhe rät Onyx nicht mehr, sondern misst die Menüleiste: 24 Punkte minus
Luft oben und unten. Vorher hing ein festes Maß unten heraus.

Dazu „nur der Hauptbildschirm" als dritte Anzeigewahl — gemeint ist der mit der
Menüleiste, nicht `NSScreen.main`, das mit dem aktiven Fenster mitwandert.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:05:13 +02:00

304 lines
13 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Testing
import CoreGraphics
@testable import OnyxNotch
// Bildschirmkoordinaten auf macOS haben ihren Ursprung unten links, y wächst nach
// oben. Die Notch sitzt also bei y = frame.maxY − höhe, nicht bei y = 0.
@Suite("Notch-Geometrie")
struct NotchGeometryTests {
/// Gemessen auf dem Zielgerät (MacBook Pro Mac17,9, M5 Pro) am 10.08.2026.
/// Dient als Referenzfall — wenn dieser Test bricht, hat Apple die
/// Notch-Metrik geändert und nicht wir.
static let builtIn = ScreenMetrics(
frame: CGRect(x: 0, y: 0, width: 1800, height: 1169),
safeAreaTop: 38,
auxiliaryTopLeft: CGRect(x: 0, y: 1131, width: 790, height: 38),
auxiliaryTopRight: CGRect(x: 1010, y: 1131, width: 790, height: 38)
)
/// Typischer externer Monitor, rechts neben dem eingebauten angeordnet.
static let external = ScreenMetrics(
frame: CGRect(x: 1800, y: 0, width: 2560, height: 1440),
safeAreaTop: 0,
auxiliaryTopLeft: nil,
auxiliaryTopRight: nil
)
static let virtualSize = CGSize(width: 220, height: 32)
// MARK: - Erkennung
@Test("Eingebautes Display: echte Notch aus den Hilfsflächen berechnet")
func physicalNotchFromRealDevice() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
#expect(g.kind == .physical)
#expect(g.rect == CGRect(x: 790, y: 1131, width: 220, height: 38))
}
@Test("Externes Display ohne Notch: virtueller Ersatz, oben mittig")
func virtualNotchOnExternal() {
let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize)
#expect(g.kind == .virtual)
#expect(g.isDetached)
// Mittig: 1800 + (2560 − 220)/2 = 2970.
// Und **nicht** an der Oberkante: ohne Hardware gibt es nichts zum
// Verschmelzen, also schwebt die Insel. Dieser Bildschirm meldet keine
// Menüleistenhöhe, deshalb bleibt es beim vorgegebenen Maß und der
// kleinen Luft: 1440 − 3 − 32 = 1405.
#expect(g.rect == CGRect(x: 2970, y: 1405, width: 220, height: 32))
}
@Test("Die Insel liegt in der Menüleiste, nicht darunter")
func islandFitsIntoTheMenuBar() {
// Ein Bildschirm, der seine Menüleiste meldet — der Normalfall.
let screen = ScreenMetrics(
frame: CGRect(x: 1800, y: 0, width: 2560, height: 1440),
safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil,
isBuiltIn: false, menuBarHeight: 24, displayID: 2)
let g = NotchGeometry.resolve(screen, virtualSize: CGSize(width: 72, height: 26))
#expect(g.isDetached)
// 24 minus je 3 Punkt Luft: die vorgegebenen 26 wären zu hoch gewesen
// und hätten unten aus der Leiste herausgeragt.
#expect(g.rect.height == 18)
// Oben und unten gleich viel Luft.
#expect(screen.frame.maxY - g.rect.maxY == 3)
#expect(g.rect.minY - (screen.frame.maxY - screen.menuBarHeight) == 3)
}
@Test("Eine sehr flache Menüleiste drückt die Insel nicht zum Strich")
func islandKeepsAMinimumHeight() {
let screen = ScreenMetrics(
frame: CGRect(x: 0, y: 0, width: 2560, height: 1440),
safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil,
isBuiltIn: false, menuBarHeight: 12, displayID: 9)
let g = NotchGeometry.resolve(screen, virtualSize: CGSize(width: 72, height: 26))
#expect(g.rect.height == NotchGeometry.minimumIslandHeight)
}
@Test("Echte Notch verschmilzt und schwebt nicht")
func physicalNotchStaysMerged() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
#expect(g.kind == .physical)
#expect(!g.isDetached)
// Kein Spalt: die Fläche *ist* die Kante.
#expect(g.topMargin == 0)
}
@Test("Stil „immer schwebend\": auch mit Notch hängt die Insel darunter")
func islandStyleDetachesEvenWithHardware() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize,
style: .island)
#expect(g.isDetached)
// Unter der Hardware, nicht in ihr: sonst wäre die Insel zur Hälfte
// schwarz auf schwarzem Gehäuse.
#expect(g.topMargin > Self.builtIn.safeAreaTop)
}
@Test("Stil „immer verschmolzen\": ohne Hardware klebt die Fläche an der Kante")
func notchStyleStaysAttachedWithoutHardware() {
let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize,
style: .notch)
#expect(!g.isDetached)
#expect(g.topMargin == 0)
#expect(g.rect.maxY == Self.external.frame.maxY)
}
@Test("safeAreaTop ohne Hilfsflächen gilt nicht als Notch")
func safeAreaWithoutAuxIsVirtual() {
// Kommt bei manchen Spiegelungs- und Fernanzeige-Konfigurationen vor:
// ein Inset ist gesetzt, aber es gibt keine Notch, um die man herumlegen
// könnte. Ein Panel an dieser Stelle säße im Nichts.
let odd = ScreenMetrics(
frame: CGRect(x: 0, y: 0, width: 1800, height: 1169),
safeAreaTop: 38,
auxiliaryTopLeft: nil,
auxiliaryTopRight: nil
)
#expect(NotchGeometry.resolve(odd, virtualSize: Self.virtualSize).kind == .virtual)
}
@Test("Notch-Mitte liegt auf der Bildschirmmitte")
func notchIsCentred() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
#expect(g.rect.midX == Self.builtIn.frame.midX)
}
// MARK: - Auslösefläche
@Test("Auslösefläche ist breiter und tiefer als die Notch")
func triggerIsLargerThanNotch() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
let t = g.triggerRect
#expect(t.width > g.rect.width)
#expect(t.height > g.rect.height)
#expect(t.midX == g.rect.midX)
}
@Test("Auslösefläche ragt nie über den oberen Bildschirmrand hinaus")
func triggerStaysOnScreen() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
#expect(g.triggerRect.maxY <= Self.builtIn.frame.maxY)
#expect(g.triggerRect.minX >= Self.builtIn.frame.minX)
#expect(g.triggerRect.maxX <= Self.builtIn.frame.maxX)
}
@Test("Auslösefläche eines schmalen Bildschirms bleibt innerhalb")
func triggerClampsOnNarrowScreen() {
let narrow = ScreenMetrics(
frame: CGRect(x: 0, y: 0, width: 240, height: 400),
safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil
)
let g = NotchGeometry.resolve(narrow, virtualSize: Self.virtualSize)
#expect(g.triggerRect.minX >= 0)
#expect(g.triggerRect.maxX <= 240)
}
// MARK: - Panelfläche
@Test("Panel hängt unter der Notch und ist auf sie zentriert")
func panelHangsBelowNotch() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
let panel = g.panelRect(for: CGSize(width: 640, height: 320))
#expect(panel.midX == g.rect.midX)
// Oberkante bündig mit dem Bildschirmrand — das Panel wächst aus der
// Notch heraus, es schwebt nicht darunter.
#expect(panel.maxY == Self.builtIn.frame.maxY)
#expect(panel.height == 320)
}
@Test("Panel breiter als der Bildschirm wird eingepasst statt abgeschnitten")
func panelClampsToScreen() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
let panel = g.panelRect(for: CGSize(width: 5000, height: 320))
#expect(panel.minX >= Self.builtIn.frame.minX)
#expect(panel.maxX <= Self.builtIn.frame.maxX)
}
@Test("Panel auf dem externen Display liegt in dessen Koordinaten, nicht bei null")
func panelUsesScreenOrigin() {
let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize)
let panel = g.panelRect(for: CGSize(width: 640, height: 320))
#expect(panel.minX >= 1800)
// Die Oberkante des Fensters folgt der schwebenden Insel, nicht der
// Bildschirmkante — sonst klebte das Panel oben fest, während die
// Insel darunter hängt.
#expect(panel.maxY == 1440 - NotchGeometry.detachedMargin)
}
// MARK: - Sichtbarer Inhaltsbereich
//
// Die Notch ist undurchsichtig. Ein Panel, dessen Oberkante am Bildschirmrand
// liegt, verliert dort alles, was gezeichnet wird — genau der Fehler, der in
// der ersten lauffähigen Fassung sichtbar wurde.
@Test("Der Inhaltsbereich liegt vollständig unterhalb der Notch")
func contentClearsTheNotch() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
let content = g.contentRect(for: CGSize(width: 446, height: 340))
#expect(!content.intersects(g.rect))
#expect(content.maxY <= g.rect.minY)
}
@Test("Der Inhaltsbereich behält die angeforderte Höhe — nichts geht verloren")
func contentKeepsRequestedHeight() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
let content = g.contentRect(for: CGSize(width: 446, height: 340))
#expect(content.height == 340)
#expect(content.width == 446)
}
@Test("Das Fenster darüber reicht bis an die Bildschirmkante, damit es mit der Notch verschmilzt")
func panelStillReachesScreenTop() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
let panel = g.panelRect(for: CGSize(width: 446, height: 340 + g.rect.height))
#expect(panel.maxY == Self.builtIn.frame.maxY)
}
@Test("Auf einem externen Display gilt dasselbe für den virtuellen Balken")
func contentClearsVirtualNotch() {
let g = NotchGeometry.resolve(Self.external, virtualSize: Self.virtualSize)
let content = g.contentRect(for: CGSize(width: 446, height: 340))
#expect(!content.intersects(g.rect))
#expect(content.height == 340)
}
@Test("Zu hohes Panel wird auf die Bildschirmhöhe begrenzt")
func panelClampsHeight() {
let g = NotchGeometry.resolve(Self.builtIn, virtualSize: Self.virtualSize)
let panel = g.panelRect(for: CGSize(width: 640, height: 99_999))
#expect(panel.height <= Self.builtIn.frame.height)
#expect(panel.minY >= Self.builtIn.frame.minY)
}
}
@Suite("Hardware-Erkennung")
struct HardwareNotchTests {
@Test("Ein Bildschirm mit Hilfsflächen hat eine echte Notch")
func builtInHasHardware() {
let metrics = ScreenMetrics(
frame: CGRect(x: 0, y: 0, width: 1800, height: 1169),
safeAreaTop: 38,
auxiliaryTopLeft: CGRect(x: 0, y: 1131, width: 790, height: 38),
auxiliaryTopRight: CGRect(x: 1010, y: 1131, width: 790, height: 38),
isBuiltIn: true, displayID: 1)
#expect(NotchGeometry.hardwareRect(metrics) != nil)
}
@Test("Ein externer Bildschirm hat keine")
func externalHasNone() {
let metrics = ScreenMetrics(
frame: CGRect(x: 1800, y: 0, width: 2560, height: 1440),
safeAreaTop: 0, auxiliaryTopLeft: nil, auxiliaryTopRight: nil,
isBuiltIn: false, displayID: 2)
#expect(NotchGeometry.hardwareRect(metrics) == nil)
}
@Test("safeAreaTop allein genügt nicht")
func insetAloneIsNotANotch() {
// Bei Spiegelung und Fernanzeige ist das Inset gesetzt, obwohl es
// nichts gibt, um das man herumlegen könnte.
let metrics = ScreenMetrics(
frame: CGRect(x: 0, y: 0, width: 1800, height: 1169),
safeAreaTop: 38, auxiliaryTopLeft: nil, auxiliaryTopRight: nil,
isBuiltIn: true, displayID: 3)
#expect(NotchGeometry.hardwareRect(metrics) == nil)
}
@Test("Die Einstellungen und die Geometrie kommen zum selben Schluss")
func settingsAgreeWithGeometry() {
// Genau dafür ist `hardwareRect` öffentlich: würden beide die Frage
// getrennt beantworten, zeigten die Einstellungen irgendwann einen
// Regler für eine Breite, die gar nicht gilt.
let builtIn = ScreenMetrics(
frame: CGRect(x: 0, y: 0, width: 1800, height: 1169),
safeAreaTop: 38,
auxiliaryTopLeft: CGRect(x: 0, y: 1131, width: 790, height: 38),
auxiliaryTopRight: CGRect(x: 1010, y: 1131, width: 790, height: 38),
isBuiltIn: true, displayID: 1)
let hasHardware = NotchGeometry.hardwareRect(builtIn) != nil
let geometry = NotchGeometry.resolve(builtIn, virtualSize: CGSize(width: 72, height: 26))
#expect(NotchStyle.automatic.merges(withHardware: hasHardware))
#expect(geometry.kind == .physical)
}
}