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:
@@ -32,7 +32,15 @@ import SwiftUI
|
||||
/// 2026-07-28, reversing the pathfinder's selection-keyed carousel): the masonry never reflows on
|
||||
/// a click, and viewing an attachment's media is the card window's job, not the face's
|
||||
/// (03-board-ui.md § Card face).
|
||||
struct LaneView: View {
|
||||
///
|
||||
/// ### Equality gate
|
||||
///
|
||||
/// The lane is `Equatable` and instantiated through `.equatable()` (`BoardView.laneSlot`), because
|
||||
/// the strip re-runs for reasons that have nothing to do with any one lane: `BoardView`'s body reads
|
||||
/// the drag session, so **every drop-proposal change re-evaluates it**, and without a gate that
|
||||
/// rebuilds every lane on the board — and, through them, every card face — on every cursor move
|
||||
/// during a drag. See the `==` below for what the gate covers.
|
||||
struct LaneView: View, Equatable {
|
||||
|
||||
let store: BoardStore
|
||||
let lane: Lane
|
||||
@@ -109,6 +117,34 @@ struct LaneView: View {
|
||||
/// flight (`DragAutoScroller`, DRAG-REORDER.md § Edge autoscroll).
|
||||
@State private var autoScroller = DragAutoScroller()
|
||||
|
||||
/// The whole of what this lane is a function of **as far as the strip is concerned**: the lane
|
||||
/// value (`Lane` is `Equatable`, its cards included, so an edit anywhere under this lane makes it
|
||||
/// a different value), the two layout figures the strip resolves rather than the lane
|
||||
/// (`columns` follows the live resize session, `slotWidth` the strip's standard width), and the
|
||||
/// window-lived collaborators — the store by identity, the band and the drop machinery by their
|
||||
/// own equivalence tests, which exist because `BoardView` rebuilds both structs, closures and
|
||||
/// all, on every body pass.
|
||||
///
|
||||
/// **What the gate does not suppress is the point.** Everything this body reads through
|
||||
/// Observation — `drops.session`'s proposal, members and file target, `store.selection`,
|
||||
/// `store.searchFilter`, `store.transient`, `appModel.styleRecents` — invalidates this view
|
||||
/// directly, and `.equatable()` has no say in that. So the lane a drag is actually over still
|
||||
/// re-runs on every proposal change, and its `shadowRun` animation key still moves with it; what
|
||||
/// stops is the *other* lanes re-running because the strip did.
|
||||
///
|
||||
/// Deliberately NOT compared: `@State` (per-identity, preserved across updates anyway),
|
||||
/// `@Environment` values (SwiftUI invalidates on those itself), and `openCard` — a closure the
|
||||
/// host rebuilds every pass, which is a pure hand-off to a `WindowGroup` key and identical in
|
||||
/// behaviour whatever closure object carries it.
|
||||
nonisolated static func == (lhs: LaneView, rhs: LaneView) -> Bool {
|
||||
lhs.lane == rhs.lane
|
||||
&& lhs.columns == rhs.columns
|
||||
&& lhs.slotWidth == rhs.slotWidth
|
||||
&& lhs.store === rhs.store
|
||||
&& lhs.marquee.isEquivalent(to: rhs.marquee)
|
||||
&& lhs.drops.isEquivalent(to: rhs.drops)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
// `spacing: 0` and the padding moved inside: the accent band is **full-width** along the
|
||||
// lane's top edge, so it must sit outside the content inset rather than in it.
|
||||
@@ -679,6 +715,10 @@ struct LaneView: View {
|
||||
marquee: marquee,
|
||||
drops: drops
|
||||
)
|
||||
// **The value gate** (`CardFaceView.==`) — the lane's own, one level
|
||||
// down: this body re-runs on every proposal change while a drag is over
|
||||
// *this* lane, and the faces it draws are almost never what changed.
|
||||
.equatable()
|
||||
case let .placeholder(phase):
|
||||
NewCardStubView(store: store, phase: phase, openCard: openCard)
|
||||
case let .shadow(_, height):
|
||||
|
||||
Reference in New Issue
Block a user