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
|
/// 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
|
||||||
|
|||||||
Reference in New Issue
Block a user