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:
@@ -122,10 +122,16 @@ struct MarqueeControl {
|
||||
// Neither cursor: a band names no click to range from and no item to arrow from,
|
||||
// so a ⇧-click after one acts plain and an arrow re-derives a position from the
|
||||
// set's last member (`TransientBoardState.selectionAnchor`, `selectionHead`).
|
||||
// Both are spelled out rather than defaulted, because `select`'s sole-member
|
||||
// default would otherwise pick one up the moment a band happened to sweep
|
||||
// exactly one card.
|
||||
store.select(ids, in: session.container, anchor: nil, head: nil)
|
||||
//
|
||||
// **`defaultsSoleMember: false` is what says that, and the explicit `nil`s never
|
||||
// did.** `nil` *is* the default, so a band that swept exactly one card used to
|
||||
// pick up both cursors anyway — harmless for the anchor, not for the head:
|
||||
// `LaneView.cardStack` watches `selectionHead` and scrolls the lane to it, which
|
||||
// means a one-card band could scroll the board out from under the drag that was
|
||||
// drawing it. The flag is the only way to spell "no gesture named these".
|
||||
store.select(
|
||||
ids, in: session.container, anchor: nil, head: nil, defaultsSoleMember: false
|
||||
)
|
||||
}
|
||||
}
|
||||
.onEnded { _ in session.end() }
|
||||
|
||||
Reference in New Issue
Block a user