A stationary modifier flip answers at the keystroke — the third client of the one shared retarget

Modifiers were sampled only inside resolveOperation, reached from
dropUpdated and the commit — and drop callbacks arrive only while the
mouse moves, so ⌥ pressed against a still pointer changed nothing
until the next twitch. DragSession now arms a local .flagsChanged
watch for exactly the session's lifetime (begin arms, end stops, the
watchdog guarantees end; ⇧/⌃/caps don't count, a settled hold
swallows) and publishes modifierGeneration; the three retargets record
which window's surface resolved the proposal (RetargetOrigin — weak
registry identity, never board root, so cross-board and two-windows-
one-board both answer correctly), and the hovered window replays that
same retarget with the operation re-resolved FIRST, since the index
space is a function of it. The monitor is injectable — the real one
needs a live event stream no test bundle has.

Drag-perf card d491e7d3.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-03 13:24:53 -04:00
parent 1fc8aaa249
commit 1ffb64913b
5 changed files with 885 additions and 10 deletions
+223 -4
View File
@@ -351,6 +351,56 @@ final class DragSession {
/// `CommittedHold`.
private(set) var hold: CommittedHold?
// MARK: The modifier flip
/// **The stationary-flip nudge**: bumped once for every change to / while a session is in
/// flight and unsettled (`ModifierFlipSource`, armed at `begin` and stopped at `end`).
///
/// The whole of what this object can say about a flip, and deliberately so. The *effect* of a
/// flip is a re-proposal, and re-proposing needs a board window's geometry which board is
/// under the cursor, where its lanes are drawn none of which an app-wide session has. So the
/// flip is published as a counter and the hovered board's own drop context turns it back into
/// the one shared retarget (`BoardDropContext.retargetAfterModifierFlip`), which is the same
/// seam the autoscroll driver's every scroll step goes through.
///
/// Observed, unlike everything else the event handlers write here: it exists to invalidate a
/// board window's body. That costs a strip body pass per **keystroke**, not per mouse sample
/// and the flip changes `hiddenMembers`, so that pass was happening anyway.
private(set) var modifierGeneration = 0
/// The board surface that last resolved this session's proposal which window, and which of
/// its drop surfaces so a flip can re-run *that* retarget rather than guess at one.
///
/// **The window is matched by registry identity, never by board root.** Two windows open on one
/// board share a store and a root but not a `LaneDropRegistry` (the cache's key already turns on
/// exactly this), and only one of them has the cursor over it; a root comparison would have the
/// other one retargeting against a cursor that is nowhere near its lanes.
///
/// **Weak**, because a board window can close mid-drag: the session outlives it, and a flip
/// afterwards simply finds no one to answer which is the honest answer, since the surface that
/// was resolving the proposal is gone.
///
/// `@ObservationIgnored` for `LaneDropRegistry`'s own reason: it is written from *event*
/// handlers, on every sample of every drag, and a body that re-ran for it would be the animation
/// feedback loop the drop model exists to avoid.
@ObservationIgnored private(set) var retargetOrigin: RetargetOrigin?
/// Which board window ran the last retarget, and which of its surfaces.
struct RetargetOrigin {
/// The three surfaces that resolve a proposal the three retargets a flip can replay.
/// Each names the function that recorded it: `.strip` is `retargetLanes`, `.lane` is
/// `retargetCards(inLane:)`, `.trash` is `retargetTrash`.
enum Surface: Equatable {
case strip
case lane(ItemID)
case trash
}
weak var registry: LaneDropRegistry?
var surface: Surface
}
// MARK: The external file mode
/// Where an external Finder file drag would land, or `nil` when there is none in flight or it is
@@ -368,13 +418,31 @@ final class DragSession {
@ObservationIgnored private var fileWatchdog: Task<Void, Never>?
@ObservationIgnored private var holdTimeoutTask: Task<Void, Never>?
/// Where the flips come from, and the running watch armed at `begin`, stopped at `end`, and
/// nowhere else (see `armFlipWatch`).
@ObservationIgnored private let flipSource: any ModifierFlipSource
@ObservationIgnored private var flipWatch: (any ModifierFlipWatch)?
/// The / state the last flip reported, so a `.flagsChanged` that moved neither for the
/// marquee's own grammar, , caps lock, a function key costs nothing at all.
@ObservationIgnored private var flipFlags: NSEvent.ModifierFlags = []
/// How long this session's holds may stand with no snapshot arriving `CommittedHold.timeout`,
/// and a `var` for one reason only: the discard path is a `Task` sleeping on the main actor, and
/// a test that had to wait the real figure out would be a 1.5 s wall clock in the suite
/// (`DragSessionTests`). Nothing in the app writes it.
@ObservationIgnored var holdTimeout: Duration = CommittedHold.timeout
init() {}
/// **The two flags the effective operation is a function of** `DragLocality.operation` reads
/// these and nothing else, so these are the whole of what "a flip" means here.
static let operationFlags: NSEvent.ModifierFlags = [.option, .command]
/// `flipSource` is defaulted to the shipping local monitor; it is a parameter for the reason
/// `DragAutoScroller`'s tick source is one the real thing needs a live `NSApplication` event
/// stream, which a test bundle has no way to feed (`ModifierFlipTests`).
init(flipSource: any ModifierFlipSource = LocalModifierFlipSource()) {
self.flipSource = flipSource
}
// MARK: Queries
@@ -411,9 +479,12 @@ final class DragSession {
///
/// Reading `operation` here is what makes the flip visible at all: the property is observed, so
/// every surface that builds a resting layout off this method re-renders when it changes. The
/// flip lands on the next `dropUpdated`, since that is where the operation is re-resolved; a
/// modifier pressed with the mouse perfectly still waits for the next motion (Backlog
/// Stationary modifier flips).
/// flip lands on the next `dropUpdated`, since that is where the operation is re-resolved
/// **or, with the mouse perfectly still, on the flip's own nudge**: drop callbacks arrive only
/// while the mouse moves, so a `.flagsChanged` monitor bumps `modifierGeneration` and the
/// hovered board re-runs the retarget the last callback ran
/// (`BoardDropContext.retargetAfterModifierFlip`). A stationary is therefore answered at the
/// keystroke rather than at the next mouse sample.
///
/// **The hold freezes the answer**, because `resolveOperation` refuses to move once a release
/// has settled: a settled copy keeps its originals on screen and a settled move keeps them
@@ -596,7 +667,9 @@ final class DragSession {
// The reload-resolved drag set: vanished members leave it silently, which is what
// `survivors` reads and what "an emptied drag cancels itself" is stated in terms of.
source.transient.dragMembers = ItemReferenceSet(ids: memberSet, container: container)
self.retargetOrigin = nil
armWatchdog()
armFlipWatch()
}
/// Records a new proposal. `nil` withdraws it rule 2's "the shadow withdraws".
@@ -609,6 +682,28 @@ final class DragSession {
proposal = target
}
/// Records which board window's surface just resolved the proposal (`RetargetOrigin`) the
/// address a stationary modifier flip replays its retarget at.
///
/// Written by the three retargets themselves rather than by their five callers, so the drop
/// delegates, the strip's fall-through and the autoscroll driver cannot disagree about where a
/// flip should land any more than they can disagree about where the drop should.
///
/// **Recorded even when the retarget goes on to hold**, which is the point: a cursor over a gap
/// or a dead region leaves the proposal exactly where it was, and a flip there must re-ask the
/// same surface the same question with the modifiers now saying something else.
///
/// Unchanged addresses are not re-stored, `propose`'s own habit and for a sharper reason here:
/// this runs on every mouse sample *and* every autoscroll frame, and the steady state of a drag
/// is the same surface answering over and over a weak reference restored per frame for no
/// change at all is exactly the kind of hot-path cost the drag model has been shedding.
func noteRetarget(_ surface: RetargetOrigin.Surface, registry: LaneDropRegistry) {
if let origin = retargetOrigin, origin.surface == surface, origin.registry === registry {
return
}
retargetOrigin = RetargetOrigin(registry: registry, surface: surface)
}
/// Re-resolves the effective operation against the board under the cursor and the modifiers
/// **right now**, and hands it back for the `DropProposal` the badge tracks.
///
@@ -651,8 +746,13 @@ final class DragSession {
operation = .move
sourceStore = nil
sourceRoot = nil
retargetOrigin = nil
watchdog?.cancel()
watchdog = nil
// The monitor's lifetime is the session's, and this is the sentence that makes it true:
// every path that ends a drag a delegate's cancel, the button-up belt, the watchdog, the
// hold's hand-off, the hold's timeout funnels through here.
stopFlipWatch()
endHold()
}
@@ -761,6 +861,56 @@ final class DragSession {
}
}
// MARK: The modifier flip the watch, and what a flip means
/// Starts watching / for this session (`ModifierFlipSource`).
///
/// **Armed at `begin` and stopped at `end`, deliberately nowhere else.** The watchdog already
/// guarantees `end` runs for every session macOS never reports the finish of, so tying the
/// monitor to that same bracket is what makes "it cannot outlive the drag" structural rather
/// than a list of call sites to keep in step the resting-layout cache's precedent exactly.
/// A previous watch is stopped first for `armWatchdog`'s reason: a second `begin` with a session
/// somehow still standing must not leave the first one's monitor installed.
///
/// The **committed hold** deliberately does not stop it: a hold is still this session, and the
/// freeze it applies is `resolveOperation`'s and `propose`'s, asked at the flip rather than
/// spelled a second time in the lifecycle (`noteFlip` restates it anyway, so a flip during a
/// settle costs not even a body pass).
private func armFlipWatch() {
stopFlipWatch()
// The baseline is the modifier state the drag is *starting* under a that was already
// down at pickup is not a flip, and `DragLocality` has already seen it.
flipFlags = NSEvent.modifierFlags.intersection(Self.operationFlags)
flipWatch = flipSource.watch { [weak self] flags in
self?.noteFlip(flags)
}
}
private func stopFlipWatch() {
flipWatch?.stop()
flipWatch = nil
}
/// A `.flagsChanged` arrived. Publishes the nudge the hovered board turns back into a
/// re-proposal, and only when something the operation depends on actually moved.
///
/// **The event's location is deliberately unread.** A flip is not a pointer event: the mouse is
/// exactly where the last drop callback left it, and every retarget reads the *physical* cursor
/// through its own window (`BoardDropContext.globalCursor`) rather than any event's coordinates.
/// That is the same reason the autoscroll driver needs no events at all to keep re-proposing.
///
/// **A settled release ignores flips**, the third face of the freeze `resolveOperation` and
/// `propose` already wear: the write named an operation and the overlay draws that operation
/// until the echo lands, so a released between the drop and the reload must not re-lift
/// originals the write is leaving in place.
private func noteFlip(_ flags: NSEvent.ModifierFlags) {
let relevant = flags.intersection(Self.operationFlags)
guard relevant != flipFlags else { return }
flipFlags = relevant
guard isActive, hold == nil else { return }
modifierGeneration &+= 1
}
/// The file mode's own watchdog, and its only guaranteed termination path.
///
/// An external session is not ours to end: no `performDrop` runs when the user drops the files
@@ -785,3 +935,72 @@ final class DragSession {
}
}
}
// MARK: - Watching the modifiers
/// Where `DragSession` learns that or moved the seam under the local `.flagsChanged` monitor,
/// and `DragAutoScrollTickSource`'s exact cousin: the shipping implementation needs a live
/// `NSApplication` event stream, which a test bundle has no way to feed.
///
/// `Sendable` so a session's stored source is, like the tick source's; the watch it hands back is
/// main-actor state and the handler runs there, which is where every drag input is read.
protocol ModifierFlipSource: Sendable {
/// Starts reporting modifier changes, until the returned watch is stopped. The flags are the
/// event's own the *whole* set, since deciding which bits matter is the session's job
/// (`DragSession.operationFlags`).
@MainActor
func watch(_ onFlip: @escaping @MainActor (NSEvent.ModifierFlags) -> Void) -> any ModifierFlipWatch
}
/// A running modifier watch. `stop()` is what retires the underlying monitor, and it is idempotent
/// so the session's teardown can call it on every path without asking whether one is installed.
@MainActor
protocol ModifierFlipWatch: AnyObject {
func stop()
}
/// The shipping source: a **local** `NSEvent` monitor for `.flagsChanged`.
///
/// Local rather than global, for two reasons that point the same way. A global monitor for keyboard
/// events needs Accessibility permission an enormous ask for a drag affordance and it would
/// report modifiers pressed while another app is frontmost, which is not a flip in *this* drag at
/// all. The events a drag actually needs are the ones being dispatched to this application while it
/// holds the session.
struct LocalModifierFlipSource: ModifierFlipSource {
@MainActor
func watch(_ onFlip: @escaping @MainActor (NSEvent.ModifierFlags) -> Void) -> any ModifierFlipWatch {
LocalModifierFlipWatch(onFlip)
}
}
/// The monitor token's owner, and the one place it is removed.
///
/// `addLocalMonitorForEvents` hands back an opaque token that `removeMonitor` **must** be given
/// AppKit retains the handler until it is, so a token dropped on the floor is a block that keeps
/// firing for the rest of the process. Holding it in an object whose only method retires it is what
/// makes the removal unmissable: `DragSession.end()` stops the watch, and every way a drag can
/// finish goes through `end()`.
@MainActor
private final class LocalModifierFlipWatch: ModifierFlipWatch {
private var token: Any?
init(_ onFlip: @escaping @MainActor (NSEvent.ModifierFlags) -> Void) {
token = NSEvent.addLocalMonitorForEvents(matching: .flagsChanged) { event in
// Local monitors run on the main thread, before the event reaches its window.
MainActor.assumeIsolated { onFlip(event.modifierFlags) }
// **Returned unchanged, always.** and mean things to the rest of the app the
// click grammar, the menu bar's key equivalents and a monitor that swallowed them
// would be reading the drag's modifiers by taking them away from everything else.
return event
}
}
func stop() {
guard let token else { return }
NSEvent.removeMonitor(token)
self.token = nil
}
}