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:
@@ -656,15 +656,21 @@ private struct StyleWellGrid: View {
|
||||
/// One view for both menus because the entries are identical on a card and on a lane: only the
|
||||
/// *target* differs, and that is the caller's to compute (the clicked item, or the selection it
|
||||
/// belongs to).
|
||||
///
|
||||
/// **The target arrives as a closure, deliberately.** `.contextMenu`'s builder is non-escaping, so a
|
||||
/// caller's menu is assembled while its own body runs — and a target computed eagerly is a
|
||||
/// `store.selection` read at body time, which under Observation subscribes that body to every
|
||||
/// selection change on the board. Deferring it means a card face's context menu costs its face
|
||||
/// nothing until a row actually acts (`CardFaceView.isSelected` has the measurement).
|
||||
struct StyleMenuItems: View {
|
||||
|
||||
let store: BoardStore
|
||||
let recents: StyleRecents
|
||||
let target: StyleTarget
|
||||
let target: () -> StyleTarget
|
||||
|
||||
var body: some View {
|
||||
Button("Style…") {
|
||||
store.transient.beginStyleEditor(for: target)
|
||||
store.transient.beginStyleEditor(for: target())
|
||||
}
|
||||
.disabled(!store.acceptsBoardMutations)
|
||||
|
||||
@@ -683,11 +689,15 @@ struct StyleMenuItems: View {
|
||||
///
|
||||
/// **Absent until it has something to offer.** A brand-new install has no recents, and an empty
|
||||
/// picker in a context menu is a row that looks broken.
|
||||
///
|
||||
/// The target is deferred for `StyleMenuItems`' reason, and this row is where the deferral has to
|
||||
/// hold: the `Binding` below is read when the menu shows a checkmark and written when a dot is
|
||||
/// picked, both of them the picker's own doing rather than the enclosing card face's body.
|
||||
struct QuickStyleRow: View {
|
||||
|
||||
let store: BoardStore
|
||||
let recents: StyleRecents
|
||||
let target: StyleTarget
|
||||
let target: () -> StyleTarget
|
||||
|
||||
/// A sentinel for "the current value is not one of these", so a mixed batch — or a background
|
||||
/// that has aged out of the recents — leaves the row unchecked rather than checking the wrong
|
||||
@@ -717,13 +727,13 @@ struct QuickStyleRow: View {
|
||||
private var selection: Binding<Choice> {
|
||||
Binding(
|
||||
get: {
|
||||
let state = StyleFieldState.resolve(store.styleSubjects(of: target).map(\.background))
|
||||
let state = StyleFieldState.resolve(store.styleSubjects(of: target()).map(\.background))
|
||||
guard case let .uniform(value) = state, recents.backgrounds.contains(value) else { return .other }
|
||||
return .value(value)
|
||||
},
|
||||
set: { picked in
|
||||
guard case let .value(name) = picked else { return }
|
||||
StyleCommand.apply(background: .set(name), to: target, in: store, recents: recents)
|
||||
StyleCommand.apply(background: .set(name), to: target(), in: store, recents: recents)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user