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:
2026-08-07 11:22:16 -04:00
parent cfee4a4b41
commit eef0a4539f
7 changed files with 602 additions and 39 deletions
+13 -7
View File
@@ -166,17 +166,23 @@ struct CardAttachmentsSection: View {
)
.contentShape(Rectangle())
// Double-click opens, a single click selects (05 Attachments; the window's click grammar,
// where clicking selects and never edits). The two-count gesture is declared first so
// SwiftUI gives it the chance to claim the second click.
.onTapGesture(count: 2) {
isFocused = true
attachments.selected = name
attachments.open(name)
}
// where clicking selects and never edits). The two-count gesture rides `simultaneousGesture`
// rather than stacking as a second `.onTapGesture` a sequential pair makes the single
// click wait out the double-click interval before selecting (`CardFaceView`'s arrangement;
// the 2026-08-06 latency fix). Simultaneity stays instant *here* because the row carries
// `.onDrag` below, which forces immediate click delivery a surface without a drag source
// must branch one recogniser on `PointerClick.count` instead (`LaneView`'s empty space).
// The first click of a pair selects, the second opens; the open re-asserting focus and
// selection is idempotent.
.onTapGesture {
isFocused = true
attachments.selected = name
}
.simultaneousGesture(TapGesture(count: 2).onEnded {
isFocused = true
attachments.selected = name
attachments.open(name)
})
// **Rows drag out their file URL** (05 Attachments; 11-command-nexus.md Pointer-only
// affordances) which is what makes drag-to-Finder and drag-into-another-app work with no
// export path of this app's own. An empty provider for a row whose file has gone refuses the