Merge nucleic/keen-yarn-wren-n8iw into dev
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user