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
+84 -6
View File
@@ -381,7 +381,11 @@ struct BoardDropContext {
/// still "the lanes minus the run" every time this function actually runs, and `moveLanes` /
/// `receiveLanes` keep the index space they always had.
func retargetLanes() {
guard session.isDraggingLanes, let cursor = stripCursor() else { return }
guard session.isDraggingLanes else { return }
// Before the cursor, so the flip's replay address is "the surface that got the event" rather
// than "the surface that managed to answer" (`DragSession.noteRetarget`).
session.noteRetarget(.strip, registry: registry)
guard let cursor = stripCursor() else { return }
let hidden = session.hiddenMembers(onBoardRooted: store.rootKey)
let resting = store.snapshot.lanes.filter { !hidden.contains($0.id) }
let restingUnits = resting.map { LaneLayoutMath.displayUnits(of: $0) }
@@ -422,7 +426,12 @@ struct BoardDropContext {
let span = DragSignposts.beginRetargetCards()
defer { DragSignposts.endRetargetCards(span) }
#endif
guard session.isDraggingCards, let cursor = globalCursor() else { return }
guard session.isDraggingCards else { return }
// See `retargetLanes` recorded before the cursor is read, and by the retarget rather than
// by its callers, so the delegate, the strip's fall-through and the autoscroll driver all
// leave the same address behind.
session.noteRetarget(.lane(laneID), registry: registry)
guard let cursor = globalCursor() else { return }
guard let resting = session.restingLayouts.layout(
inLane: laneID,
of: store,
@@ -484,13 +493,17 @@ struct BoardDropContext {
/// A method rather than a property because it is not free of consequence: the operation is
/// re-resolved against the modifiers *at this instant*, which is also what keeps the badge honest
/// while the cursor sits over the column (`dropProposal`).
func acceptsTrashDrop() -> Bool {
///
/// `modifiers` defaults to the live flags, which is the only thing the app ever passes the
/// parameter is `DragSession.resolveOperation`'s own seam, one rung out, so the refusal this
/// gate exists for ( over the column) is checkable without a keyboard (`ModifierFlipTests`).
func acceptsTrashDrop(modifiers: NSEvent.ModifierFlags = NSEvent.modifierFlags) -> Bool {
guard let sourceRoot = session.sourceRoot else { return false }
return TrashDrop.accepts(
kind: session.kind,
container: session.container,
isWithinBoard: sourceRoot == store.rootKey,
operation: session.resolveOperation(destinationRoot: store.rootKey),
operation: session.resolveOperation(destinationRoot: store.rootKey, modifiers: modifiers),
isTrashShown: store.transient.isTrashVisible,
acceptsMutations: store.acceptsBoardMutations
)
@@ -506,9 +519,19 @@ struct BoardDropContext {
/// trash proposes nothing for you": the column declines to be a target, it does not cancel the
/// drag the user is still holding. So a lane drag reorders across the column exactly as it always
/// did, and an -copy released over it still lands where its shadows are.
func retargetTrash() {
func retargetTrash(modifiers: NSEvent.ModifierFlags = NSEvent.modifierFlags) {
// A session, or there is nothing to record an address for. Provably a no-op otherwise
// every branch below is kind-guarded one level down and stated so that "an origin means a
// drag" holds (`DragSession.noteRetarget`).
guard session.isActive else { return }
revalidateProposal()
guard acceptsTrashDrop() else {
// The fall-through below may overwrite this address with the surface that actually answered,
// and that is exactly right: a flip replays whatever resolved the proposal last. The one
// case this gate exists for keeps it a cursor over the column resolves to no lane, so the
// strip's card fall-through holds without recording, and the column stays the address the
// -release has to be re-asked at.
session.noteRetarget(.trash, registry: registry)
guard acceptsTrashDrop(modifiers: modifiers) else {
retargetFromStrip()
return
}
@@ -519,6 +542,61 @@ struct BoardDropContext {
))
}
// MARK: Retargeting the modifier flip
/// **The stationary modifier flip's answer** (04-interactions.md Drag and drop: " always
/// forces copy and always forces move the badge tracks the effective operation live").
///
/// The modifiers are sampled inside `resolveOperation`, which is reached from `dropUpdated` and
/// the commit and drop callbacks arrive only while the mouse **moves**. So pressed with the
/// pointer perfectly still used to change nothing visible until the next twitch: the source
/// board's originals stayed lifted, the trash kept refusing (or kept offering) against the old
/// operation, and the shadows stood in an index space the release would no longer count in.
/// This is the missing sample, arriving from the keyboard instead of from the mouse.
///
/// **The third client of the one shared retarget** (DRAG-REORDER.md § Edge autoscroll: "the
/// lane's drop delegate and the autoscroll driver must go through one shared retarget, so they
/// can never disagree"). It resolves nothing itself it re-runs the retarget that resolved the
/// standing proposal, at the surface that resolved it (`DragSession.RetargetOrigin`), so a flip
/// and a mouse sample land in exactly the same place by construction.
///
/// **No location comes off the flip event, and none is needed.** Every retarget reads the
/// physical cursor through this window (`globalCursor`), which is precisely why the autoscroll
/// driver can re-propose against a stationary pointer while the content moves under it; here the
/// content stands still and the *operation* moves instead.
///
/// Three guards, in the order that makes each one cheap:
///
/// - **A settled release is past retargeting.** `resolveOperation` and `propose` both freeze
/// under a hold, so this would be inert anyway; refusing outright means a flip during the
/// settle costs nothing at all, and states the freeze where it can be read.
/// - **Only the window under the cursor answers.** The origin names a `LaneDropRegistry`, which
/// is per board *window* so a cross-board drag re-proposes against the board being hovered
/// rather than the one it was picked up from, two windows on one board do not both answer, and
/// a window closed mid-drag drops out silently (the reference is weak).
/// - **The operation is re-resolved before the retarget, not after it.** `dropUpdated` runs them
/// the other way round and can afford to: at 60+ samples a second the next event repairs the
/// one-sample lag. A flip gets one shot, and the retarget's index space is a function of the
/// operation (`retargetCards` `hiddenMembers`), so the operation has to be current *first*
/// or the shadows would land in the space the flip just left.
func retargetAfterModifierFlip(modifiers: NSEvent.ModifierFlags = NSEvent.modifierFlags) {
guard session.isActive, session.hold == nil else { return }
guard let origin = session.retargetOrigin, origin.registry === registry else { return }
session.resolveOperation(destinationRoot: store.rootKey, modifiers: modifiers)
switch origin.surface {
case .strip:
retargetFromStrip()
case let .lane(laneID):
// The lane delegate's own pair, in its order: rule 2 first, then the masonry's zones.
revalidateProposal()
retargetCards(inLane: laneID)
case .trash:
retargetTrash(modifiers: modifiers)
}
}
// MARK: Retargeting external Finder file sessions
/// Whether `info` is an **external Finder file** session rather than one of ours.