The drag learns the stepper's trick — past the screen's edge, lane growth re-divides instead of stopping

The right-edge drag's growth was capped at the screen's visible frame,
because each snap tick grows the window; on a window near the screen edge
that left a lane stuck at a tick or two of headroom. Settled 2026-08-08
(03-board-ui.md § Lane, superseding the pathfinder's hard stop): at the
screen the window pins and each further tick re-divides the fixed strip
width across one more unit — siblings compress, the stepper's mechanism
arriving under the drag's fingers. The regimes meet with no pixel jump
(the re-divided standard at the fit IS the frozen standard, by the
exact-fill identity), shrinking mirrors the way back, the rubber band
moves to the strip's own capacity, and a window with no headroom at all —
full screen included — re-divides from the very first snap.

New pure arithmetic in LaneLayoutMath (pinnedStripWidth, resizeStandard,
resizeMaxUnits, resizeWindowDelta, snappedUnits over per-count slots);
LaneResizeSession splits the tick across the regimes and derives its
standard from the live count; the handle and BoardView hand the session
the strip's whole divide. 2709 unit tests green (+11).

Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
2026-08-08 21:36:56 -04:00
parent 05b1a787de
commit 9a52b795b2
8 changed files with 564 additions and 91 deletions
+81 -5
View File
@@ -16,13 +16,18 @@ import Testing
///
/// Fixture geometry is `LaneLayoutMathTests`': a frozen standard of 100 and a gap of 12, so the slot
/// widths are 1× = 100 · 2× = 212 · 3× = 324, and the tick-down thresholds are 214 (32) and 102
/// (21). Every drag here **shrinks**, because with no `NSWindow` to measure the on-screen fit is
/// the count the drag started from (`LaneResizeSession.fittingMaxUnits`) and growth has no room
/// direction is nothing to the hold's state machine, which is what is under test.
/// (21). Every drag here **shrinks**, which keeps the arithmetic the frozen standard's throughout:
/// with no `NSWindow` to measure, the on-screen fit is the count the drag started from
/// (`LaneResizeSession.fittingMaxUnits`), so growth would land in the re-divide regime that is the
/// suite at the foot of this file, and direction is nothing to the hold's state machine, which is
/// what is under test here.
private let standard: CGFloat = 100
private let gap: CGFloat = 12
/// The fixture strip's whole divide the 1× lane plus the 3× one, trash hidden.
private let boardUnits = 4
/// Enough leftward translation from a 3× start to land on 2×, and on 1×: 324 120 = 204, under the
/// 214 threshold and over the 102 one; 324 240 rubber-bands to 96, under both.
private let toTwoUnits: CGFloat = -120
@@ -70,7 +75,8 @@ struct LaneResizeHoldTests {
/// A session mid-drag on lane two, `translation` points to the left of its 3× start.
private func dragging(_ translation: CGFloat) -> LaneResizeSession {
let session = LaneResizeSession()
session.begin(laneID: lane2, units: 3, standard: standard, gap: gap, window: nil)
session.begin(laneID: lane2, units: 3, standard: standard, gap: gap,
totalUnits: boardUnits, window: nil)
session.update(translation: translation)
return session
}
@@ -334,7 +340,8 @@ struct LaneResizeHoldTests {
// width the release wrote not the snapshot's, which is still a round trip behind.
let onScreen = session.displayUnits(of: try lane(Ident.lane2, in: store))
#expect(onScreen == 2)
session.begin(laneID: lane2, units: onScreen, standard: standard, gap: gap, window: nil)
session.begin(laneID: lane2, units: onScreen, standard: standard, gap: gap,
totalUnits: boardUnits, window: nil)
#expect(!session.isSettled)
#expect(session.isDragging(lane2))
@@ -357,3 +364,72 @@ struct LaneResizeHoldTests {
#expect(session.liveWidth == LaneLayoutMath.slotWidth(units: 2, standard: standard, gap: gap))
}
}
// MARK: - The drag past the screen
/// **The re-divide at the session level** (03-board-ui.md § Lane, settled 2026-08-08). A window with
/// no headroom one already flush against the screen's edge, or full screen, and in a fixture one
/// with no `NSWindow` at all has an on-screen fit equal to the count the drag started from, so its
/// very first tick is a re-divide: the strip's width is pinned, the dragged lane takes one more unit
/// of it, and the siblings compress. The clamp that used to sit at the fit is what made a lane on a
/// maximised window refuse to widen at all.
///
/// Same fixture as above the 1× lane, the 3× lane and no trash, so a four-unit strip 460 points
/// wide (100·4 + 12·5) dragged from **one** unit, which with no window makes the fit 1× and puts
/// every tick in the second regime.
@MainActor
@Suite("The lane resize past the screen's edge")
struct LaneResizePastTheScreenTests {
/// A drag of the 1× lane, `translation` points to the right of its start.
private func dragging(_ translation: CGFloat) -> LaneResizeSession {
let session = LaneResizeSession()
session.begin(laneID: lane1, units: 1, standard: standard, gap: gap,
totalUnits: boardUnits, window: nil)
session.update(translation: translation)
return session
}
@Test("With no window to grow, the drag re-divides instead of refusing to tick")
func aPinnedWindowRedividesFromTheFirstTick() {
let session = dragging(200)
// The 460pt strip re-divided: 2× puts the lane's edge at 167.2, 3× at 212, and 7× at
// 301.6, whose trailing gap is the first threshold the 300pt live edge has not cleared.
#expect(session.units == 7, "the fit is 1× here, and the old clamp stopped the tick dead")
#expect(session.liveWidth == 300, "well inside the strip's capacity, so no resistance")
// The siblings compress, which is what a re-divide IS: the standard every other lane is
// drawn at comes down as the dragged one takes more units of the same strip.
#expect(abs(session.standard - 32.8) < 0.0001) // (460 12·11) / 10
#expect(session.standard < standard)
}
@Test("The tick still stops — at the strip's capacity, where the re-divide runs out of strip")
func theRedivideStopsAtTheStripsCapacity() {
// floor((460 12) / 13) = 34 whole units the strip can still divide into, of which this
// lane contributes 31. Past that `standardWidth`'s 1pt floor would break the exact fill.
let session = dragging(10_000)
#expect(session.units == 31)
#expect(session.liveWidth > LaneLayoutMath.slotWidth(units: 31, standard: session.standard, gap: gap),
"the rubber band gives past the true end of travel, and the tick does not follow")
}
@Test("A release past the fit settles on the re-divided slot, not the frozen one")
func theSettleUsesTheRedividedStandard() throws {
let fixture = try makeBoard()
defer { fixture.tearDown() }
let store = try BoardStore(rootURL: fixture.root)
let session = dragging(200)
session.end { id, units in store.setLaneWidth(id, units: units) }
#expect(session.isSettled)
#expect(session.hold == LaneWidthHold(laneID: lane1, units: 7))
// The width the release wrote, measured against the standard THAT count implies the one
// still governing the strip while the hold stands.
#expect(session.liveWidth
== LaneLayoutMath.slotWidth(units: 7, standard: session.standard, gap: gap))
#expect(abs(session.liveWidth - 301.6) < 0.0001)
}
}