The empty provider was never load-bearing — the dragless layer frees the rubber band
Measured on real events 2026-08-07, correcting the 2026-08-06 hosted finding: a bare count-1 tap on LaneView's empty-space layer fires in ~1-3 ms with no drag source at all — the hold that made the empty .onDrag look necessary was the sterile NSApp.postEvent stream over-disambiguating. And the provider was actively harmful: even an empty drag source claims the mouse-drag at threshold, starving the marquee's simultaneous DragGesture after one sample — the band froze and the mouseUp never arrived. The layer goes dragless; drags from empty space belong wholly to MarqueeControl. PointerClick's and the layer's comments retell the corrected story. Alongside: openCard is typed @MainActor throughout, which makes the closure Sendable and lets CardFaceRole carry it under CardFaceView's nonisolated ==. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -71,7 +71,7 @@ struct LaneView: View, Equatable {
|
||||
/// Opens a card's window — ⌘↩'s second half (04-interactions.md ▸ Grammar, "commits and opens
|
||||
/// the card window"). Supplied by the strip, which is supplied by the host: a lane has no
|
||||
/// business knowing about `WindowGroup` keys.
|
||||
let openCard: (ItemID) -> Void
|
||||
let openCard: @MainActor (ItemID) -> Void
|
||||
|
||||
/// Reduce Motion, for the card transition below (10-accessibility.md). Read from the environment
|
||||
/// and handed to `Motion`, which owns what "reduced" means.
|
||||
@@ -844,26 +844,26 @@ struct LaneView: View, Equatable {
|
||||
Rectangle()
|
||||
.fill(.clear)
|
||||
.contentShape(Rectangle())
|
||||
// **The empty provider is load-bearing, and it is not a drag** (measured,
|
||||
// 2026-08-06): without a drag source on this layer, macOS holds its primary
|
||||
// clicks pending multi-click disambiguation — a lone click on lane empty
|
||||
// space simply never fired its tap on the hosted board, drag source absent,
|
||||
// and fired in ~90 ms with one present. The card faces, the lane header and
|
||||
// the trash rows are instant for exactly this reason: their real `.onDrag`
|
||||
// forces immediate event delivery for the whole subtree. An **empty**
|
||||
// provider keeps that delivery guarantee while refusing every actual drag
|
||||
// before a session starts (`CardAttachmentsSection`'s gone-file idiom), so
|
||||
// dragging from empty space still belongs wholly to the rubber band's
|
||||
// simultaneous `DragGesture` on the container — whose begin guard already
|
||||
// expects to sample drags it must decline (`MarqueeControl`).
|
||||
.onDrag { NSItemProvider() }
|
||||
// **No drag source on this layer, deliberately** (measured on real events,
|
||||
// 2026-08-07): an `.onDrag` here — even one whose provider is empty — claims
|
||||
// the mouse-drag the moment the cursor crosses the drag threshold, and the
|
||||
// rubber band's simultaneous `DragGesture` on the container gets one sample
|
||||
// and then silence: the band begins and freezes, and even the mouseUp never
|
||||
// reaches the app. Dragless, the same sweep tracks every sample. And the drag
|
||||
// source buys nothing in return: a bare count-1 tap on this layer fires in
|
||||
// ~1–3 ms in a real event stream, identical to the with-`.onDrag` shape — the
|
||||
// hold that made an empty provider look load-bearing (2026-08-06, "a lone
|
||||
// click never fired") was the hosted harness's sterile `NSApp.postEvent`
|
||||
// stream, which over-holds; real streams have nothing to disambiguate here.
|
||||
// So: drags from empty space belong wholly to the band (`MarqueeControl`),
|
||||
// whose begin guard keeps card-face drags out by geometry.
|
||||
// **One recogniser, both meanings** — a single `.onTapGesture` that branches
|
||||
// on `PointerClick.count`, AppKit's own `mouseDown` idiom. Not a second
|
||||
// two-tap recogniser in *either* form: sequential stacking is the bug this
|
||||
// fix removes, and even a simultaneous `TapGesture(count: 2)` makes macOS
|
||||
// hold this layer's primary clicks for the whole double-click interval,
|
||||
// because the layer — unlike the card faces, the header and the trash rows —
|
||||
// carries no `.onDrag` to force immediate delivery (see `PointerClick`).
|
||||
// fix removes, and a *multi-click* recogniser — sequential or simultaneous —
|
||||
// is what makes macOS hold a dragless subtree's primary clicks for the whole
|
||||
// double-click interval (see `PointerClick`; the count-1 tap alone triggers
|
||||
// no such hold).
|
||||
// A lone tap fires once; a double fires it once per click, so the branch is
|
||||
// Finder's cadence exactly: the first click selects, the second creates.
|
||||
//
|
||||
@@ -1256,7 +1256,7 @@ private struct NewCardStubView: View {
|
||||
/// the face this view draws and the identity the slot is keyed by are the same answer.
|
||||
let phase: NewCardPlaceholder.Phase
|
||||
|
||||
let openCard: (ItemID) -> Void
|
||||
let openCard: @MainActor (ItemID) -> Void
|
||||
|
||||
/// Increase Contrast, for the editor well's stroke below (10-accessibility.md; `Accommodations`).
|
||||
@Environment(\.colorSchemeContrast) private var contrast
|
||||
|
||||
Reference in New Issue
Block a user