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:
@@ -49,6 +49,26 @@ enum CardFaceRole {
|
||||
case .trash: .trash
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether two roles put the face in the same home with the same collaborator — the comparison
|
||||
/// `CardFaceView.==` makes, and the reason this enum is not simply `Equatable`.
|
||||
///
|
||||
/// **The board case's `openCard` is deliberately not compared.** It is a closure the strip
|
||||
/// rebuilds on every body pass, so comparing it is impossible and ignoring it is correct: it is a
|
||||
/// pure hand-off to the host's `WindowGroup` key, identical in behaviour whatever closure object
|
||||
/// carries it, and a face that changed *which window it opens into* would be a face in a
|
||||
/// different window and therefore a different view identity entirely.
|
||||
///
|
||||
/// The trash case's `confirmations` **is** compared, by identity: it is window-lived state
|
||||
/// (`@State` in `BoardWindowHost`), so identity is both cheap and meaningful, and it is the one
|
||||
/// collaborator a role carries that the face actually reads state off.
|
||||
nonisolated func isEquivalent(to other: CardFaceRole) -> Bool {
|
||||
switch (self, other) {
|
||||
case (.board, .board): true
|
||||
case let (.trash(lhs), .trash(rhs)): lhs === rhs
|
||||
default: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Card face
|
||||
@@ -94,7 +114,15 @@ enum CardFaceRole {
|
||||
/// attachment's media is the card window's job (⌘↩ / double-click, 05-card-window.md), not the
|
||||
/// face's — the paperclip chip below is the face's whole attachment story (03-board-ui.md § Card
|
||||
/// face).
|
||||
struct CardFaceView: View {
|
||||
///
|
||||
/// ### Equality gate
|
||||
///
|
||||
/// The face is `Equatable` and instantiated through `.equatable()` (`LaneView.scrollableCards`,
|
||||
/// `TrashLaneView.scrollableCards`), because its parent re-runs for reasons that have nothing to do
|
||||
/// with any one card: a lane's body re-evaluates on **every drop-proposal change** while a drag is
|
||||
/// in flight, and without a gate that rebuilds every face in every lane on every cursor move. See
|
||||
/// the `==` below for what the gate covers and what it deliberately does not.
|
||||
struct CardFaceView: View, Equatable {
|
||||
|
||||
let store: BoardStore
|
||||
let card: Card
|
||||
@@ -140,6 +168,30 @@ struct CardFaceView: View {
|
||||
/// fallback is for.
|
||||
@State private var measuredWidth: CGFloat = 0
|
||||
|
||||
/// The whole of what this face is a function of **as far as its parent is concerned**: the card
|
||||
/// value (`Card` is `Equatable` down to its attachment names and its parsed document), which home
|
||||
/// it is drawn in (`CardFaceRole.isEquivalent(to:)`), and the three window-lived collaborators —
|
||||
/// the store by identity, the band and the drop machinery by their own equivalence tests, which
|
||||
/// exist because the strip 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 — `store.selection`, `store.searchFilter`, `store.transient.pendingCut` and the
|
||||
/// rename editor, `drops.session.isDragging`, `appModel.styleRecents` — invalidates this view
|
||||
/// directly, and `.equatable()` has no say in that. The gate only stops the *parent* handing a
|
||||
/// face a new-but-identical set of inputs and re-running it for nothing, which during a card drag
|
||||
/// is what every proposal change does to every face in the lane.
|
||||
///
|
||||
/// Deliberately NOT compared: `@State` (per-identity, preserved across updates anyway),
|
||||
/// `@Environment` values (SwiftUI invalidates on those itself), and the `.board` role's
|
||||
/// `openCard` closure (see `CardFaceRole.isEquivalent(to:)`).
|
||||
nonisolated static func == (lhs: CardFaceView, rhs: CardFaceView) -> Bool {
|
||||
lhs.card == rhs.card
|
||||
&& lhs.role.isEquivalent(to: rhs.role)
|
||||
&& lhs.store === rhs.store
|
||||
&& lhs.marquee.isEquivalent(to: rhs.marquee)
|
||||
&& lhs.drops.isEquivalent(to: rhs.drops)
|
||||
}
|
||||
|
||||
/// **The role's three absences, as a branch rather than as disabled modifiers.** Everything both
|
||||
/// sides share is in `face`; what the board has and the trash does not is attached here, so the
|
||||
/// trash's no-Open/no-Rename/no-Style is expressed by code that is not written rather than by
|
||||
|
||||
Reference in New Issue
Block a user