Panel deckt die Menüleiste nicht mehr zu
Die Fläche reichte über die ganze Breite bis an die Bildschirmoberkante, damit sie mit der schwarzen Notch zu einem Körper verschmilzt. Bei einer Kachel ging das auf; bei sechs deckt sie die komplette Menüleiste zu, samt aller Statuselemente. Die Silhouette ist jetzt ein T: oben ein Steg in Notch-Breite, ab Unterkante der Menüleiste die volle Fläche. Die Notch bleibt verschmolzen, die Menüleiste bleibt frei. Das allein hätte nur so ausgesehen, als wäre es gelöst. Das Fenster liegt weiterhin über der Leiste, dort nur durchsichtig — und ein durchsichtiges Fenster schluckt Klicks trotzdem. Die Trefferprüfung reicht sie jetzt durch: links und rechts der Notch gehört der oberste Streifen der Menüleiste. Aus demselben Grund fragt der Koordinator nicht mehr den Fensterrahmen, sondern die sichtbare Fläche. Wer den Rahmen befragt, hält einen Klick auf ein fremdes Statuselement für einen Klick ins Panel — und einen Zeiger über der Menüleiste für einen im Panel. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,13 @@
|
|||||||
{
|
{
|
||||||
"sourceLanguage" : "en",
|
"sourceLanguage" : "en",
|
||||||
"strings" : {
|
"strings" : {
|
||||||
"": {},
|
"" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"–" : {
|
||||||
|
"comment" : "A placeholder for the volume of a process.",
|
||||||
|
"isCommentAutoGenerated" : true
|
||||||
|
},
|
||||||
"%lld %%" : {
|
"%lld %%" : {
|
||||||
"comment" : "A row of controls for adjusting the volume and muting a process.",
|
"comment" : "A row of controls for adjusting the volume and muting a process.",
|
||||||
"isCommentAutoGenerated" : true,
|
"isCommentAutoGenerated" : true,
|
||||||
|
|||||||
@@ -160,7 +160,7 @@ public final class NotchCoordinator {
|
|||||||
for controller in controllers.values where controller.isVisible {
|
for controller in controllers.values where controller.isVisible {
|
||||||
if controller.geometry.triggerRect.contains(point) {
|
if controller.geometry.triggerRect.contains(point) {
|
||||||
controller.click()
|
controller.click()
|
||||||
} else if !controller.panelFrame.contains(point) {
|
} else if !controller.containsVisiblePoint(point) {
|
||||||
controller.clickedOutside()
|
controller.clickedOutside()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,12 +28,75 @@ public final class NotchPresentation {
|
|||||||
self.contentTopInset = contentTopInset
|
self.contentTopInset = contentTopInset
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Die Fläche, die das Panel oben einnehmen darf, ohne die Menüleiste
|
||||||
|
/// zuzudecken — also die Breite der Notch.
|
||||||
|
///
|
||||||
|
/// Darunter darf es beliebig breit werden; dort ist keine Menüleiste mehr.
|
||||||
|
public var notchWidth: CGFloat { collapsedSize.width }
|
||||||
|
|
||||||
/// Wie lange das Einfahren braucht, bis das Fenster gefahrlos verschwinden darf.
|
/// Wie lange das Einfahren braucht, bis das Fenster gefahrlos verschwinden darf.
|
||||||
/// Muss zu `Onyx.Motion.collapse` passen: zu früh entfernt reißt das Panel
|
/// Muss zu `Onyx.Motion.collapse` passen: zu früh entfernt reißt das Panel
|
||||||
/// sichtbar weg, zu spät bleibt ein unsichtbares Fenster im Weg stehen.
|
/// sichtbar weg, zu spät bleibt ein unsichtbares Fenster im Weg stehen.
|
||||||
public static let collapseDuration: TimeInterval = 0.30
|
public static let collapseDuration: TimeInterval = 0.30
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Die Silhouette des Panels: oben nur so breit wie die Notch, darunter voll.
|
||||||
|
///
|
||||||
|
/// **Warum nicht einfach ein Rechteck.** Das Panel reicht bis an die
|
||||||
|
/// Bildschirmoberkante, damit seine Fläche mit der schwarzen Notch zu einem
|
||||||
|
/// Körper verschmilzt. Sobald es breiter wird als die Notch, deckt es links
|
||||||
|
/// und rechts davon aber die Menüleiste zu — bei sechs Kacheln die ganze,
|
||||||
|
/// samt aller Statuselemente.
|
||||||
|
///
|
||||||
|
/// Also ein T: oben ein Steg in Notch-Breite, ab Unterkante der Menüleiste die
|
||||||
|
/// volle Fläche. Die Notch bleibt verschmolzen, die Menüleiste bleibt frei.
|
||||||
|
struct NotchShape: Shape, InsettableShape {
|
||||||
|
var cornerRadius: CGFloat
|
||||||
|
var topBandHeight: CGFloat
|
||||||
|
var notchWidth: CGFloat
|
||||||
|
var inset: CGFloat = 0
|
||||||
|
|
||||||
|
var animatableData: AnimatablePair<CGFloat, CGFloat> {
|
||||||
|
get { .init(cornerRadius, notchWidth) }
|
||||||
|
set { cornerRadius = newValue.first; notchWidth = newValue.second }
|
||||||
|
}
|
||||||
|
|
||||||
|
func inset(by amount: CGFloat) -> NotchShape {
|
||||||
|
var copy = self
|
||||||
|
copy.inset += amount
|
||||||
|
return copy
|
||||||
|
}
|
||||||
|
|
||||||
|
func path(in rect: CGRect) -> Path {
|
||||||
|
let rect = rect.insetBy(dx: inset, dy: inset)
|
||||||
|
// Ist das Panel nicht breiter als die Notch, ist der Steg die ganze
|
||||||
|
// Form — dann bleibt es die schlichte abgerundete Fläche.
|
||||||
|
let band = min(max(notchWidth - 2 * inset, 0), rect.width)
|
||||||
|
let radius = min(cornerRadius, rect.width / 2, rect.height / 2)
|
||||||
|
|
||||||
|
var path = Path()
|
||||||
|
guard topBandHeight > 0, band < rect.width - 0.5 else {
|
||||||
|
path.addRoundedRect(in: rect,
|
||||||
|
cornerSize: CGSize(width: radius, height: radius),
|
||||||
|
style: .continuous)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
let bandRect = CGRect(x: rect.midX - band / 2, y: rect.minY,
|
||||||
|
width: band, height: topBandHeight + radius)
|
||||||
|
let body = CGRect(x: rect.minX, y: rect.minY + topBandHeight,
|
||||||
|
width: rect.width, height: rect.height - topBandHeight)
|
||||||
|
|
||||||
|
// Der Steg bekommt keine Rundung nach oben: dort sitzt die Notch, und
|
||||||
|
// eine Rundung an der Bildschirmkante wäre ohnehin nicht zu sehen.
|
||||||
|
path.addRect(bandRect)
|
||||||
|
path.addRoundedRect(in: body,
|
||||||
|
cornerSize: CGSize(width: radius, height: radius),
|
||||||
|
style: .continuous)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Der Rahmen, der aus der Notch herauswächst.
|
/// Der Rahmen, der aus der Notch herauswächst.
|
||||||
///
|
///
|
||||||
/// Die Fläche wird **nicht** eingeblendet. Sie beginnt exakt in der Notch-Silhouette
|
/// Die Fläche wird **nicht** eingeblendet. Sie beginnt exakt in der Notch-Silhouette
|
||||||
@@ -66,7 +129,9 @@ public struct NotchContainer<Content: View>: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
let shape = RoundedRectangle(cornerRadius: cornerRadius, style: .continuous)
|
let shape = NotchShape(cornerRadius: cornerRadius,
|
||||||
|
topBandHeight: presentation.contentTopInset,
|
||||||
|
notchWidth: presentation.notchWidth)
|
||||||
|
|
||||||
ZStack(alignment: .top) {
|
ZStack(alignment: .top) {
|
||||||
// Das Weichzeichner-Material behält **feste** Größe; animiert wird
|
// Das Weichzeichner-Material behält **feste** Größe; animiert wird
|
||||||
@@ -94,7 +159,7 @@ public struct NotchContainer<Content: View>: View {
|
|||||||
.init(color: .clear, location: 0.4)],
|
.init(color: .clear, location: 0.4)],
|
||||||
startPoint: .top, endPoint: .bottom))
|
startPoint: .top, endPoint: .bottom))
|
||||||
}
|
}
|
||||||
.overlay { shape.strokeBorder(Onyx.Color.hairline, lineWidth: 1) }
|
.overlay { shape.stroke(Onyx.Color.hairline, lineWidth: 1) }
|
||||||
.frame(width: size.width, height: size.height)
|
.frame(width: size.width, height: size.height)
|
||||||
// Konstante Deckkraft: einen Schatten aufzublenden kostet in
|
// Konstante Deckkraft: einen Schatten aufzublenden kostet in
|
||||||
// jedem Einzelbild eine neue Weichzeichnung. Er schrumpft
|
// jedem Einzelbild eine neue Weichzeichnung. Er schrumpft
|
||||||
@@ -159,6 +224,17 @@ public final class NotchHostingView<Content: View>: NSHostingView<Content> {
|
|||||||
width: size.width,
|
width: size.width,
|
||||||
height: size.height)
|
height: size.height)
|
||||||
guard visible.contains(point) else { return nil }
|
guard visible.contains(point) else { return nil }
|
||||||
|
|
||||||
|
// Links und rechts neben der Notch gehört der oberste Streifen der
|
||||||
|
// Menüleiste. Dort ist das Panel durchsichtig — und muss auch
|
||||||
|
// durchlässig sein, sonst schluckt es Klicks auf fremde
|
||||||
|
// Statuselemente, ohne dass man sieht, warum.
|
||||||
|
if presentation.isExpanded, point.y < presentation.contentTopInset {
|
||||||
|
let band = CGRect(x: (bounds.width - presentation.notchWidth) / 2, y: 0,
|
||||||
|
width: presentation.notchWidth,
|
||||||
|
height: presentation.contentTopInset)
|
||||||
|
guard band.contains(point) else { return nil }
|
||||||
|
}
|
||||||
return super.hitTest(point)
|
return super.hitTest(point)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,15 +75,25 @@ public final class NotchScreenController {
|
|||||||
// groß wie das ausgefahrene Panel, damit die Animation Platz hat —
|
// groß wie das ausgefahrene Panel, damit die Animation Platz hat —
|
||||||
// würde man `panel.frame` nehmen, hielte ein Zeiger in der noch leeren
|
// würde man `panel.frame` nehmen, hielte ein Zeiger in der noch leeren
|
||||||
// Fläche das Panel offen.
|
// Fläche das Panel offen.
|
||||||
let inside = geometry.triggerRect.contains(point)
|
let inside = geometry.triggerRect.contains(point) || containsVisiblePoint(point)
|
||||||
|| (presentation.isExpanded && panel.frame.contains(point))
|
|
||||||
send(inside ? .pointerEntered : .pointerExited)
|
send(inside ? .pointerEntered : .pointerExited)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func click() { send(.click) }
|
public func click() { send(.click) }
|
||||||
public func clickedOutside() { send(.clickedOutside) }
|
public func clickedOutside() { send(.clickedOutside) }
|
||||||
/// Für den Koordinator: wo das Panel gerade steht.
|
|
||||||
public var panelFrame: CGRect { panel.frame }
|
/// Ob ein Punkt auf der **sichtbaren** Fläche des Panels liegt.
|
||||||
|
///
|
||||||
|
/// Nicht `panel.frame`: das Fenster reicht bis an die Bildschirmoberkante
|
||||||
|
/// und ist links und rechts der Notch dort durchsichtig — dieser Streifen
|
||||||
|
/// gehört der Menüleiste. Wer den Fensterrahmen befragt, hält einen Klick
|
||||||
|
/// auf ein fremdes Statuselement für einen Klick ins Panel.
|
||||||
|
public func containsVisiblePoint(_ point: CGPoint) -> Bool {
|
||||||
|
guard presentation.isExpanded, panel.frame.contains(point) else { return false }
|
||||||
|
let menuBarBand = panel.frame.maxY - geometry.rect.height
|
||||||
|
guard point.y > menuBarBand else { return true }
|
||||||
|
return abs(point.x - panel.frame.midX) <= geometry.rect.width / 2
|
||||||
|
}
|
||||||
public func escape() { send(.escape) }
|
public func escape() { send(.escape) }
|
||||||
public func setSuppressed(_ suppressed: Bool) {
|
public func setSuppressed(_ suppressed: Bool) {
|
||||||
guard suppressed != machine.isSuppressed else { return }
|
guard suppressed != machine.isSuppressed else { return }
|
||||||
|
|||||||
Reference in New Issue
Block a user