Fix the collapsed-lanes render on initial board open
The strip's standard-width division read the drop registry's strip frame during body evaluation, but the registry is deliberately unobserved (it exists for event-time reads): the first body ran against a zero frame, every lane collapsed to the 1pt floor, and nothing invalidated the view when the real measurement landed — the board stayed corrupted until an unrelated re-render. The layout now reads the GeometryReader's live proposal (reactive by construction); the drop delegates keep reading the registry at event time. Claude-Session: https://claude.ai/code/session_01SR4XGjmBE16ZUYWpfFHXwY
This commit is contained in:
@@ -121,7 +121,7 @@ struct BoardView: View {
|
|||||||
private let spacing: CGFloat = 12
|
private let spacing: CGFloat = 12
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GeometryReader { _ in
|
GeometryReader { proxy in
|
||||||
// The strip's slots: the lanes the strip should *show*, with the drag's N contiguous
|
// The strip's slots: the lanes the strip should *show*, with the drag's N contiguous
|
||||||
// shadows opened at the proposal. Recomputed on every render, so a foreign reload
|
// shadows opened at the proposal. Recomputed on every render, so a foreign reload
|
||||||
// mid-drag simply moves the zones (rule 1 of 04-interactions.md ▸ Drag and drop's
|
// mid-drag simply moves the zones (rule 1 of 04-interactions.md ▸ Drag and drop's
|
||||||
@@ -129,7 +129,7 @@ struct BoardView: View {
|
|||||||
let slots = stripSlots
|
let slots = stripSlots
|
||||||
ZStack(alignment: .topLeading) {
|
ZStack(alignment: .topLeading) {
|
||||||
backdrop
|
backdrop
|
||||||
laneStrip(slots)
|
laneStrip(slots, standard: standardWidth(stripWidth: proxy.size.width))
|
||||||
}
|
}
|
||||||
.padding(spacing)
|
.padding(spacing)
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||||
@@ -254,8 +254,7 @@ struct BoardView: View {
|
|||||||
|
|
||||||
/// The lanes and the drag's shadows, plus the trash column when it is shown.
|
/// The lanes and the drag's shadows, plus the trash column when it is shown.
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func laneStrip(_ slots: [StripSlot]) -> some View {
|
private func laneStrip(_ slots: [StripSlot], standard: CGFloat) -> some View {
|
||||||
let standard = standardWidth
|
|
||||||
HStack(alignment: .top, spacing: spacing) {
|
HStack(alignment: .top, spacing: spacing) {
|
||||||
ForEach(slots) { slot in
|
ForEach(slots) { slot in
|
||||||
switch slot {
|
switch slot {
|
||||||
@@ -480,11 +479,20 @@ struct BoardView: View {
|
|||||||
gap: spacing,
|
gap: spacing,
|
||||||
window: window,
|
window: window,
|
||||||
stripFrame: { laneDrops.stripFrame },
|
stripFrame: { laneDrops.stripFrame },
|
||||||
standard: { standardWidth }
|
standard: { standardWidth(stripWidth: laneDrops.stripFrame.width) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The strip's 1× lane width.
|
/// The strip's 1× lane width, for a strip `stripWidth` points wide.
|
||||||
|
///
|
||||||
|
/// **The width is a parameter, and which width the caller passes matters.** Body evaluation
|
||||||
|
/// passes the `GeometryReader`'s live proposal, because that is the one source that invalidates
|
||||||
|
/// the view when it changes — the registry's `stripFrame` is deliberately unobserved (it exists
|
||||||
|
/// for event-time reads), so a body computed from it renders the *previous* layout's width: on
|
||||||
|
/// first open that is `.zero`, every lane collapses to `standardWidth`'s 1pt floor, and the
|
||||||
|
/// board stays that way until an unrelated invalidation happens by. The drop delegates pass
|
||||||
|
/// `laneDrops.stripFrame.width` instead, which is the same number read at event time — exactly
|
||||||
|
/// the registry's purpose. The two agree because the padded container fills the reader.
|
||||||
///
|
///
|
||||||
/// During a resize session the standard is **frozen** at its drag-start value: the window is
|
/// During a resize session the standard is **frozen** at its drag-start value: the window is
|
||||||
/// animating mid-resize, so deriving the standard from the live width would feed that animation
|
/// animating mid-resize, so deriving the standard from the live width would feed that animation
|
||||||
@@ -504,12 +512,12 @@ struct BoardView: View {
|
|||||||
/// - a **cross-board lane arrival**'s units while its shadow hovers here, by the same rule read
|
/// - a **cross-board lane arrival**'s units while its shadow hovers here, by the same rule read
|
||||||
/// from the destination's side: the shadow occupies its units, and the strip has to make room
|
/// from the destination's side: the shadow occupies its units, and the strip has to make room
|
||||||
/// for them or the shadow would be drawn at a width the lane will not have.
|
/// for them or the shadow would be drawn at a width the lane will not have.
|
||||||
private var standardWidth: CGFloat {
|
private func standardWidth(stripWidth: CGFloat) -> CGFloat {
|
||||||
if resize.isActive { return resize.standard }
|
if resize.isActive { return resize.standard }
|
||||||
var units = LaneLayoutMath.totalUnits(of: liveLanes, trashUnits: isTrashVisible ? 1 : 0)
|
var units = LaneLayoutMath.totalUnits(of: liveLanes, trashUnits: isTrashVisible ? 1 : 0)
|
||||||
units += arrivingLaneUnits
|
units += arrivingLaneUnits
|
||||||
return LaneLayoutMath.standardWidth(
|
return LaneLayoutMath.standardWidth(
|
||||||
stripWidth: laneDrops.stripFrame.width,
|
stripWidth: stripWidth,
|
||||||
totalUnits: units,
|
totalUnits: units,
|
||||||
gap: spacing
|
gap: spacing
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user