Merge nucleic/keen-yarn-wren-n8iw into dev

This commit is contained in:
2026-07-17 19:32:49 -07:00
parent 5203b5ad36
commit 7aa4ea3893
+44 -15
View File
@@ -14,11 +14,19 @@ import AppKit
/// frame a pixel and immediately back. The two `setFrame` calls coalesce within one /// frame a pixel and immediately back. The two `setFrame` calls coalesce within one
/// runloop pass, so there's no visible jump, but the toolbar re-composites its glass. /// runloop pass, so there's no visible jump, but the toolbar re-composites its glass.
/// ///
/// The nudge is debounced until the window is geometrically stable, and any resize in /// The nudge is debounced until the window is geometrically stable. That matters
/// flight (a drag-resize or the animation of a double-click *zoom*) defers it. That /// because the non-animated `setFrame` would otherwise cancel a running zoom (or
/// matters because the non-animated `setFrame` would otherwise cancel a running zoom /// drag-resize) and snap the window back to its pre-zoom size a visible stutter
/// and snap the window back to its pre-zoom size and a real resize is itself the /// followed by a revert. A real resize is itself the relayout we're faking, so nudging
/// relayout we're faking, so nudging during one is redundant anyway. /// during one is redundant anyway.
///
/// We can't trust resize *notifications* to tell us when a zoom is in flight the
/// animation of a double-click zoom doesn't reliably post `didResizeNotification` for
/// every intermediate frame, so a notification-only debounce can still fire mid-zoom.
/// Instead we watch the window's frame *value*: an in-flight zoom is, by definition, a
/// frame that's still moving. Before nudging we require two consecutive samples (one
/// debounce interval apart) to be identical; while the frame keeps changing we keep
/// deferring. This catches any animation regardless of whether it emits notifications.
struct ToolbarGlassKeeper: NSViewRepresentable { struct ToolbarGlassKeeper: NSViewRepresentable {
func makeNSView(context: Context) -> NSView { func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero) let view = NSView(frame: .zero)
@@ -45,6 +53,11 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
private var repairPending = false private var repairPending = false
/// The debounced apply, cancelled/rescheduled while frames are still in flight. /// The debounced apply, cancelled/rescheduled while frames are still in flight.
private var settleWorkItem: DispatchWorkItem? private var settleWorkItem: DispatchWorkItem?
/// The window frame seen at the previous settle. The nudge only fires once two
/// consecutive samples match i.e. the frame has stopped moving so an
/// animating zoom (whose frame is still changing) keeps deferring instead of
/// being cancelled by our `setFrame`.
private var lastSampledFrame: CGRect?
/// Idempotent: registers once for the window's reveal / app-reactivation events. /// Idempotent: registers once for the window's reveal / app-reactivation events.
func attach(to window: NSWindow?) { func attach(to window: NSWindow?) {
@@ -64,12 +77,14 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
forName: NSApplication.didBecomeActiveNotification, forName: NSApplication.didBecomeActiveNotification,
object: nil, queue: .main object: nil, queue: .main
) { [weak self] _ in MainActor.assumeIsolated { self?.requestRepair() } }, ) { [weak self] _ in MainActor.assumeIsolated { self?.requestRepair() } },
// A resize a drag-resize or, crucially, each step of a double-click // A resize a drag-resize or a step of a double-click *zoom* is itself
// *zoom* animation is itself the relayout that cures the glass, so the // the relayout that cures the glass, so the nudge is redundant during one
// nudge is redundant during one and, worse, its non-animated `setFrame` // and, worse, its non-animated `setFrame` would cancel an in-flight zoom
// would cancel an in-flight zoom and snap the window back to its pre-zoom // and snap the window back to its pre-zoom size. When one of these
// size. So a resize only defers an already-pending nudge until the frame // notifications does fire it pushes an already-pending nudge past the
// settles; it never starts one. // resize; it never starts one. (The frame-stability check in
// `applyRepairIfPending` is the real guard a zoom animation doesn't
// reliably post this for every frame.)
center.addObserver( center.addObserver(
forName: NSWindow.didResizeNotification, forName: NSWindow.didResizeNotification,
object: window, queue: .main object: window, queue: .main
@@ -77,9 +92,12 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
] ]
} }
/// Reveal / reactivation: mark a nudge wanted and (re)arm the debounce. /// Reveal / reactivation: mark a nudge wanted and (re)arm the debounce. Seed the
/// frame baseline with the current frame so a still window nudges on the first
/// settle, while one caught mid-animation defers until its frame stops moving.
private func requestRepair() { private func requestRepair() {
repairPending = true repairPending = true
lastSampledFrame = window?.frame
scheduleSettle() scheduleSettle()
} }
@@ -96,8 +114,9 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
MainActor.assumeIsolated { self?.applyRepairIfPending() } MainActor.assumeIsolated { self?.applyRepairIfPending() }
} }
settleWorkItem = work settleWorkItem = work
// Long enough to bridge the ~16ms gaps between an animated zoom's resize // Also the interval between frame-stability samples: long enough that a zoom
// steps, so a running zoom keeps re-deferring this until it truly ends. // animation visibly moves the frame between two settles (so it keeps
// deferring), short enough that a still window nudges promptly.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: work) DispatchQueue.main.asyncAfter(deadline: .now() + 0.2, execute: work)
} }
@@ -105,12 +124,22 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
guard repairPending else { return } guard repairPending else { return }
guard let window, window.occlusionState.contains(.visible) else { guard let window, window.occlusionState.contains(.visible) else {
repairPending = false repairPending = false
lastSampledFrame = nil
return return
} }
// Still resizing (a drag hasn't been let go) wait for stability. // Still resizing (a drag hasn't been let go) wait for stability.
if window.inLiveResize { scheduleSettle(); return } if window.inLiveResize { scheduleSettle(); return }
repairPending = false
let frame = window.frame let frame = window.frame
// A frame that changed since the last settle is still animating (a zoom in
// flight). Touching it now would cancel the zoom and revert the window, so
// re-sample and wait for two matching frames before nudging.
if lastSampledFrame != frame {
lastSampledFrame = frame
scheduleSettle()
return
}
repairPending = false
lastSampledFrame = nil
guard frame.height > 2 else { return } guard frame.height > 2 else { return }
// Shrink by a pixel (anchored at the top edge AppKit's origin is // Shrink by a pixel (anchored at the top edge AppKit's origin is
// bottom-left, so raise the origin as height drops) then restore. Shrinking // bottom-left, so raise the origin as height drops) then restore. Shrinking