Selected-ness rides down as a compared parameter — a marquee crossing repaints its faces, not the board
A selection change re-ran every CardFaceView on the board (180 bodies ≈ 85 ms on the 6×30 fixture, 515 ≈ 233 ms on a real 515-card board, debug): the face's body read store.selection in three places — isSelected, the drag replica's count, and the context menu's styleTarget — and Observation invalidates every reader of the property, past the equatable gate entirely. The band overlay stayed cheap, which is why the marquee tracked the cursor while the highlight lagged ~0.4 s behind. Now LaneView and TrashLaneView hoist one selection read per body and hand each face isSelected/selectedCount as compared parameters; StyleMenuItems takes its target as a deferred closure; TrashLaneRowView gains the same treatment plus the Equatable gate it never needed before. Select-one-card: 180 bodies → 1. A growing band costs the selection's own running size; the real board's crossing fell 233 → 112 ms — the remainder is lane bodies re-measuring their masonry, a separate lane-level finding recorded in RENDER-INSTRUMENTATION.md. Also: select() gains defaultsSoleMember — the marquee's explicit nils never avoided the sole-member default, so a one-card band acquired a selectionHead and could scroll the lane out from under its own drag. MarqueeRenderCostTests pins the shape: redundant samples cost zero bodies, a growing band pays per crossing, and selectionStillRepaints holds a ≤8 budget.
This commit is contained in:
@@ -343,7 +343,9 @@ struct LaneView: View, Equatable {
|
||||
|
||||
Divider()
|
||||
|
||||
StyleMenuItems(store: store, recents: appModel.styleRecents, target: styleTarget)
|
||||
// Deferred, `CardFaceView`'s reason (`StyleMenuItems`): a non-escaping menu builder makes an
|
||||
// eagerly computed target a body-time selection read.
|
||||
StyleMenuItems(store: store, recents: appModel.styleRecents, target: { styleTarget })
|
||||
|
||||
Divider()
|
||||
|
||||
@@ -734,6 +736,18 @@ struct LaneView: View, Equatable {
|
||||
// the session's hidden-member resolution) once per element — O(n²) per lane body, which
|
||||
// a drag pickup's synchronous whole-board layout multiplied into a visible stall.
|
||||
let slots = self.slots
|
||||
// **The board-side selection, read once for the whole lane.** This body is already subscribed
|
||||
// to it — the header's own `isSelected` reads it — so hoisting the set here costs nothing new
|
||||
// and lets every face take its selected-ness as a *compared parameter* instead of reading the
|
||||
// store itself. That is the difference between a selection change re-running one body per
|
||||
// lane and re-running one per card on the board (`CardFaceView.isSelected`,
|
||||
// RENDER-INSTRUMENTATION.md ▸ Selection is O(board) in card bodies).
|
||||
//
|
||||
// Hoisted for `slots`' reason too: reading it inside the `ForEach` closure would re-derive
|
||||
// the container branch and re-count the set once per element.
|
||||
let selection = store.selection
|
||||
let selectedIDs = selection.container == .board ? selection.ids : []
|
||||
let selectedCount = max(1, selectedIDs.count)
|
||||
return ScrollView(.vertical) {
|
||||
// Cards stay standard width whatever the lane spans: at a slot width of
|
||||
// `units × standard + (units - 1) × gap`, `MasonryLayout` divides back into exactly
|
||||
@@ -748,7 +762,12 @@ struct LaneView: View, Equatable {
|
||||
card: card,
|
||||
role: .board(openCard: openCard),
|
||||
marquee: marquee,
|
||||
drops: drops
|
||||
drops: drops,
|
||||
isSelected: selectedIDs.contains(card.id),
|
||||
// 1 for an unselected face: the replica's fan and count badge want
|
||||
// "how many ride along", and a card outside the selection drags
|
||||
// alone (`CardFaceView.draggedIDs`).
|
||||
selectedCount: selectedIDs.contains(card.id) ? selectedCount : 1
|
||||
)
|
||||
// **The value gate** (`CardFaceView.==`) — the lane's own, one level
|
||||
// down: this body re-runs on every proposal change while a drag is over
|
||||
|
||||
Reference in New Issue
Block a user