Clicks land the instant they happen — the empty-space gestures move behind the masonry
The measured defect: LaneView's empty-space double-click was a second sequential .onTapGesture(count: 2) stacked over the single tap, and that recogniser held every click in the lane — its own empty space and every card face alike — hostage to the system double-click interval while it disambiguated (~475 ms click-to-selection on a hosted board). The fix is structural: the empty-space surfaces live on a background layer behind the masonry, so a card's click never shares a gesture path with a lane recogniser; one .onTapGesture branches on PointerClick.count (AppKit's own clickCount, read the way ClickModifier reads the keyboard) — first click selects, second creates, Finder's cadence with nothing to disambiguate; and the layer carries a load-bearing empty .onDrag, because without a drag source macOS holds primary clicks pending multi-click disambiguation (measured: never fires alone, ~90 ms with one present). A measured viewport floor makes each lane's blank space actually belong to the layer — a ScrollView proposes nothing along its scroll axis, so only an explicit minimum stretches the content — with the trash column as its twin, less the padding that sits inside its scroll content. The template chooser's stacked pair collapses to the same one-recogniser branch, and the attachment rows move their double-click to a simultaneous gesture (instant there, because the row's real .onDrag forces immediate delivery). PointerLatencyTests pins the recovery with synthetic pointer events on a hosted board. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -24,6 +24,42 @@ extension ClickModifier {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The click a handler is riding
|
||||
|
||||
/// Which click of a multi-click run the current gesture handler is riding — `NSEvent.clickCount`
|
||||
/// off the event being dispatched, read the way `ClickModifier.current` reads the keyboard:
|
||||
/// SwiftUI's `TapGesture` hands its handler nothing about the event.
|
||||
///
|
||||
/// **This is how a surface without a drag source gets a double-click meaning** (the 2026-08-06
|
||||
/// click-latency fix). A second tap recogniser is never the way: a sequential
|
||||
/// `.onTapGesture(count: 2)` makes every single click on its subtree wait out the system
|
||||
/// double-click interval — and on macOS even a *simultaneous* two-tap recogniser holds primary
|
||||
/// clicks on views that carry no `.onDrag`. (A drag source forces immediate event delivery, which
|
||||
/// is why the card faces, the lane header and the trash rows — `CardFaceView`'s simultaneous
|
||||
/// arrangement — stay instant; `LaneView`'s empty-space layer measurably does not.) A single
|
||||
/// `.onTapGesture` fires once per click of a run, so branching on this count expresses
|
||||
/// "first click selects, second creates" — Finder's cadence — with exactly one recogniser and
|
||||
/// nothing to disambiguate.
|
||||
enum PointerClick {
|
||||
|
||||
/// The `clickCount` of the click being handled: 1 for a lone click or a run's first, 2 for
|
||||
/// the second click of a double, and so on.
|
||||
///
|
||||
/// `NSApp.currentEvent` rather than a stored flag: the event being dispatched *is* the click,
|
||||
/// and AppKit's `clickCount` already embodies the system double-click interval and the
|
||||
/// spatial-proximity rule, so no timer here could disagree with the event stream's own
|
||||
/// pairing. A current event that is not a mouse click (or is absent — a synthetic call) reads
|
||||
/// as a first click, which fails toward the single-click action: selection stays reachable.
|
||||
@MainActor
|
||||
static var count: Int {
|
||||
guard let event = NSApp.currentEvent else { return 1 }
|
||||
switch event.type {
|
||||
case .leftMouseDown, .leftMouseUp: return max(1, event.clickCount)
|
||||
default: return 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - The rubber band's gesture
|
||||
|
||||
/// What a board window lends its empty surfaces so each can be a rubber band: the one session, the
|
||||
|
||||
Reference in New Issue
Block a user