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:
@@ -389,17 +389,32 @@ public final class TransientBoardState {
|
||||
/// The grammar itself is `SelectionGrammar`'s — pure, testable, and the one funnel every click
|
||||
/// surface goes through (`BoardStore.click`). This is the storage half, and its only rule of its
|
||||
/// own is the **anchor default**, which the head shares: `nil` with a sole member takes that
|
||||
/// member, `nil` with any other count takes nothing. That makes the two callers that pass
|
||||
/// nothing behave exactly as they should — a one-item selection made by any route is a
|
||||
/// legitimate range origin *and* a legitimate place to arrow from, while a marquee or a Select
|
||||
/// All names no gesture and so leaves a ⇧-click acting plain and the arrows re-deriving a
|
||||
/// position from the set's last member.
|
||||
/// member, `nil` with any other count takes nothing. That makes the callers that pass nothing
|
||||
/// behave exactly as they should — a one-item selection made by any route is a legitimate range
|
||||
/// origin *and* a legitimate place to arrow from.
|
||||
///
|
||||
/// **The default is opt-out, because one caller genuinely names no gesture.** A Select All over a
|
||||
/// one-card board is still a selection the user pointed at, but a rubber band is not: it names no
|
||||
/// click to range from and no item to arrow from *whatever* it happens to enclose, and passing
|
||||
/// `nil` cannot say so — `nil` is what asks for the default. So `defaultsSoleMember: false` takes
|
||||
/// `anchor` and `head` verbatim, `nil` included, and a band that sweeps exactly one card leaves
|
||||
/// both cursors empty instead of quietly acquiring them (`MarqueeControl.gesture`; a
|
||||
/// `selectionHead` set mid-band would additionally fire `LaneView.cardStack`'s scroll-to under the
|
||||
/// user's own drag).
|
||||
///
|
||||
/// Deliberately **not** filtered against the snapshot: a caller selects what it is rendering, and
|
||||
/// `resolve(against:)` on the next reload is what keeps the set honest over time.
|
||||
public func select(_ ids: Set<ItemID>, in container: ItemContainer, anchor: ItemID? = nil, head: ItemID? = nil) {
|
||||
///
|
||||
/// - Parameter defaultsSoleMember: whether a `nil` anchor or head falls back to a sole member.
|
||||
public func select(
|
||||
_ ids: Set<ItemID>,
|
||||
in container: ItemContainer,
|
||||
anchor: ItemID? = nil,
|
||||
head: ItemID? = nil,
|
||||
defaultsSoleMember: Bool = true
|
||||
) {
|
||||
selection = ItemReferenceSet(ids: ids, container: container)
|
||||
let sole = ids.count == 1 ? ids.first : nil
|
||||
let sole = defaultsSoleMember && ids.count == 1 ? ids.first : nil
|
||||
selectionAnchor = anchor ?? sole
|
||||
selectionHead = head ?? sole
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user