Proposal changes stop rebuilding the whole board — lanes and card faces gate on their values

LaneView and CardFaceView become Equatable and are instantiated through
.equatable(): the strip's body re-runs on every drop-proposal change, and
without the gates that rebuilt every lane and every card face on every
cursor move of a drag. The == compares value inputs and window-lived
collaborator identities; the closures BoardView rebuilds each pass are
deliberately excluded (BoardDropContext.isEquivalent / MarqueeControl.
isEquivalent / CardFaceRole.isEquivalent own that judgment). Observation
reads inside the bodies still self-invalidate — the lane under the drag
keeps re-running; the other lanes stop.

Ports the pathfinder's CardView/ColumnView gating pattern (drag-perf
suspect #2, card cbb6e476).

Claude-Session: https://claude.ai/code/session_01CqjXB7ASoWtbyoGod68k97
This commit is contained in:
2026-08-01 18:28:33 -04:00
parent ef423bb9d2
commit 84f909a720
7 changed files with 497 additions and 2 deletions
+11
View File
@@ -40,6 +40,17 @@ struct MarqueeControl {
let registry: MarqueeTargetRegistry
let store: BoardStore
/// Whether two of these lend the same band the whole of what this value contributes to
/// `LaneView.==` and `CardFaceView.==` (`BoardDropContext.isEquivalent(to:)` is its twin).
///
/// All three members are window-lived objects, so identity is the comparison: this struct holds
/// no geometry and no closures of its own, and the strip rebuilds it on every body pass.
nonisolated func isEquivalent(to other: MarqueeControl) -> Bool {
session === other.session
&& registry === other.registry
&& store === other.store
}
/// The band, as one gesture attached with `simultaneousGesture` wherever empty space is.
///
/// - **The begin guard is geometric**: a drag whose start lands inside a registered frame is