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
+17
View File
@@ -110,6 +110,23 @@ struct BoardDropContext {
/// The strip's 1× lane width for the current drag context (`LaneLayoutMath.standardWidth`).
let standard: @MainActor () -> CGFloat
/// Whether two of these would send a drop to the same place **the whole of what this value
/// contributes to `LaneView.==` and `CardFaceView.==`.**
///
/// `BoardView.dropContext` rebuilds this struct on every body pass, closures and all, so a naive
/// value comparison is impossible and a reference comparison is meaningless: the three closures
/// are freshly allocated each time and would make every lane and every card face unequal on every
/// pass, which is exactly the rebuild the gates exist to stop. They are also the members it is
/// safe to ignore each one *reads* window and strip geometry at event time rather than carrying
/// any (see this type's own note), so two contexts with the same store, session and registry
/// resolve every hover identically whatever closure objects they happen to hold.
nonisolated func isEquivalent(to other: BoardDropContext) -> Bool {
store === other.store
&& session === other.session
&& registry === other.registry
&& gap == other.gap
}
// MARK: The cursor
/// The physical cursor in the window's SwiftUI global space.