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
/// 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
/// flight (a drag-resize or the animation of a double-click *zoom*) defers it. That
/// matters because the non-animated `setFrame` would otherwise cancel a running zoom
/// and snap the window back to its pre-zoom size and a real resize is itself the
/// relayout we're faking, so nudging during one is redundant anyway.
/// The nudge is debounced until the window is geometrically stable. That matters
/// because the non-animated `setFrame` would otherwise cancel a running zoom (or
/// drag-resize) and snap the window back to its pre-zoom size a visible stutter
/// followed by a revert. A real resize is itself the relayout we're faking, so nudging
/// 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 {
func makeNSView(context: Context) -> NSView {
let view = NSView(frame: .zero)
@@ -45,6 +53,11 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
private var repairPending = false
/// The debounced apply, cancelled/rescheduled while frames are still in flight.
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.
func attach(to window: NSWindow?) {
@@ -64,12 +77,14 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
forName: NSApplication.didBecomeActiveNotification,
object: nil, queue: .main
) { [weak self] _ in MainActor.assumeIsolated { self?.requestRepair() } },
// A resize a drag-resize or, crucially, each step of a double-click
// *zoom* animation is itself the relayout that cures the glass, so the
// nudge is redundant during one and, worse, its non-animated `setFrame`
// would cancel an in-flight zoom and snap the window back to its pre-zoom
// size. So a resize only defers an already-pending nudge until the frame
// settles; it never starts one.
// A resize a drag-resize or a step of a double-click *zoom* is itself
// the relayout that cures the glass, so the nudge is redundant during one
// and, worse, its non-animated `setFrame` would cancel an in-flight zoom
// and snap the window back to its pre-zoom size. When one of these
// notifications does fire it pushes an already-pending nudge past the
// 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(
forName: NSWindow.didResizeNotification,
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() {
repairPending = true
lastSampledFrame = window?.frame
scheduleSettle()
}
@@ -96,8 +114,9 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
MainActor.assumeIsolated { self?.applyRepairIfPending() }
}
settleWorkItem = work
// Long enough to bridge the ~16ms gaps between an animated zoom's resize
// steps, so a running zoom keeps re-deferring this until it truly ends.
// Also the interval between frame-stability samples: long enough that a zoom
// 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)
}
@@ -105,12 +124,22 @@ struct ToolbarGlassKeeper: NSViewRepresentable {
guard repairPending else { return }
guard let window, window.occlusionState.contains(.visible) else {
repairPending = false
lastSampledFrame = nil
return
}
// Still resizing (a drag hasn't been let go) wait for stability.
if window.inLiveResize { scheduleSettle(); return }
repairPending = false
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 }
// 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