An alignment tick when the drop proposal lands somewhere new

NSHapticFeedbackManager .alignment at propose()'s change edge — the
one funnel every retarget commits through, whose existing guards
(value-equal refusal, hold freeze) already make firings per-landing-
spot rather than per-pixel; withdrawal stays silent (losing a target
is not an alignment), and the external-file mode gets nothing. Force
Touch hardware only, silent no-op elsewhere; performanceTime .default
syncs the tick to the reflow the change triggers. The performer rides
an injectable seam beside holdTimeout for the same reason it has one.

Drag-perf card 11b85111 — the cluster's last.

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-03 13:41:06 -04:00
parent 43e6ad229e
commit c60553f17f
2 changed files with 132 additions and 0 deletions
+14
View File
@@ -433,6 +433,13 @@ final class DragSession {
/// (`DragSessionTests`). Nothing in the app writes it.
@ObservationIgnored var holdTimeout: Duration = CommittedHold.timeout
/// Fires the alignment tick for a genuine new landing spot (`propose`). A `var` for the reason
/// `holdTimeout` is one: the real `NSHapticFeedbackManager` call needs a Force Touch trackpad a
/// test bundle has no way to feed, so a test substitutes a closure that counts firings instead.
@ObservationIgnored var hapticTick: () -> Void = {
NSHapticFeedbackManager.defaultPerformer.perform(.alignment, performanceTime: .default)
}
/// **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]
@@ -680,6 +687,13 @@ final class DragSession {
func propose(_ target: DropTarget?) {
guard isActive, hold == nil, proposal != target else { return }
proposal = target
// One tick per genuine new landing spot, never per pixel: the hysteresis/dead-region model
// above already makes retargets sparse, which is exactly the case Apple gives `.alignment`
// for rather than the "don't overuse haptics" one a per-sample tick would land in. A
// withdrawal (`target == nil`) is losing a target, not landing on one, so it stays silent.
if target != nil {
hapticTick()
}
}
/// Records which board window's surface just resolved the proposal (`RetargetOrigin`) the