The board learns to zoom — eight rungs on one ruler, and Actual Size is the untouched board
View ▸ Zoom In / Zoom Out / Actual Size (⌘+ / ⌘− / ⌘0): 75%–200% in eight rungs, app-wide and persisted (the Show Comments precedent) — a viewing comfort, not a property of any one board. The level travels as BoardZoomContext in the environment, injected on BoardView alone so the banner strip, search bar, sheets and popovers stay at the system size; the environment is also what carries it through CardFaceView's equality gate, which compares nothing that moves with the level. Every BoardMetrics figure follows zoom.bodyPointSize — card and lane chrome, drag replicas and the count badge, the resize handle, the trash column — and the drop registry carries the ruler for event-time reads, with the autoscroller's three reaches turning font-derived (reachSide named as the stripGap it always equalled). Lanes still divide the window; zoom never moves the window or its floor. The toolbar gains a catalog-only Zoom In/Out pair mirroring the menu rows' predicate; zoom holds shut mid-drag (frozen geometry), each rung announces itself to VoiceOver, and the render suite pins both invariants: a rung repaints every face, a no-op Actual Size repaints nothing. Claude-Session: https://claude.ai/code/session_014PtZdPwqZuqEDLc6wZMtEy
This commit is contained in:
@@ -121,27 +121,64 @@ struct DragAutoScrollMathTests {
|
||||
|
||||
// MARK: Engagement reach
|
||||
|
||||
/// The standard system body size — the ruler every figure below was tuned against.
|
||||
private static let standardBody: CGFloat = 13
|
||||
|
||||
@Test("Engagement reaches over the header but barely sideways")
|
||||
func engagementReach() {
|
||||
let viewport = CGSize(width: 240, height: 400)
|
||||
let reach = DragAutoScrollMath.engagementRect(viewport: viewport)
|
||||
let size = Self.standardBody
|
||||
let above = DragAutoScrollMath.reachAbove(bodyPointSize: size)
|
||||
let below = DragAutoScrollMath.reachBelow(bodyPointSize: size)
|
||||
let side = DragAutoScrollMath.reachSide(bodyPointSize: size)
|
||||
let reach = DragAutoScrollMath.engagementRect(viewport: viewport, bodyPointSize: size)
|
||||
|
||||
#expect(reach.contains(CGPoint(x: 120, y: 200)), "inside the visible area, always")
|
||||
|
||||
// Above it (the lane header) and below it (the strip's padding).
|
||||
#expect(reach.contains(CGPoint(x: 120, y: -DragAutoScrollMath.reachAbove + 1)))
|
||||
#expect(reach.contains(CGPoint(x: 120, y: viewport.height + DragAutoScrollMath.reachBelow - 1)))
|
||||
#expect(!reach.contains(CGPoint(x: 120, y: -DragAutoScrollMath.reachAbove - 1)))
|
||||
#expect(!reach.contains(CGPoint(x: 120, y: viewport.height + DragAutoScrollMath.reachBelow + 1)))
|
||||
#expect(reach.contains(CGPoint(x: 120, y: -above + 1)))
|
||||
#expect(reach.contains(CGPoint(x: 120, y: viewport.height + below - 1)))
|
||||
#expect(!reach.contains(CGPoint(x: 120, y: -above - 1)))
|
||||
#expect(!reach.contains(CGPoint(x: 120, y: viewport.height + below + 1)))
|
||||
|
||||
// Sideways: only a sliver, so the neighbouring lane never engages.
|
||||
#expect(reach.contains(CGPoint(x: -DragAutoScrollMath.reachSide + 1, y: 200)))
|
||||
#expect(!reach.contains(CGPoint(x: -DragAutoScrollMath.reachSide - 1, y: 200)))
|
||||
#expect(!reach.contains(CGPoint(x: viewport.width + DragAutoScrollMath.reachSide + 1, y: 200)))
|
||||
#expect(reach.contains(CGPoint(x: -side + 1, y: 200)))
|
||||
#expect(!reach.contains(CGPoint(x: -side - 1, y: 200)))
|
||||
#expect(!reach.contains(CGPoint(x: viewport.width + side + 1, y: 200)))
|
||||
|
||||
// The sideways reach must stay under half the distance between two lanes' scroll areas, or
|
||||
// two lanes would scroll at once.
|
||||
#expect(DragAutoScrollMath.reachSide < 28 / 2)
|
||||
#expect(side < 28 / 2)
|
||||
}
|
||||
|
||||
@Test("The standard body size draws the reaches it always drew")
|
||||
func reachesAtStandardBodySize() {
|
||||
let size = Self.standardBody
|
||||
#expect(DragAutoScrollMath.reachAbove(bodyPointSize: size) == 48)
|
||||
#expect(DragAutoScrollMath.reachBelow(bodyPointSize: size) == 24)
|
||||
#expect(DragAutoScrollMath.reachSide(bodyPointSize: size) == 12)
|
||||
}
|
||||
|
||||
/// The reaches are distances to the lane's own furniture — a header, a padding, a gap — and all
|
||||
/// three of those grow with the board's zoom (03-board-ui.md ▸ Layout — zoom). A reach fixed in
|
||||
/// points would stop covering the header it is specified against.
|
||||
@Test("Every reach grows with the board's ruler")
|
||||
func reachesFollowTheRuler() {
|
||||
let standard = Self.standardBody
|
||||
let zoomed = BoardZoom.bodyPointSize(system: standard, level: 2.0)
|
||||
|
||||
#expect(DragAutoScrollMath.reachAbove(bodyPointSize: zoomed)
|
||||
> DragAutoScrollMath.reachAbove(bodyPointSize: standard))
|
||||
#expect(DragAutoScrollMath.reachBelow(bodyPointSize: zoomed)
|
||||
> DragAutoScrollMath.reachBelow(bodyPointSize: standard))
|
||||
#expect(DragAutoScrollMath.reachSide(bodyPointSize: zoomed)
|
||||
> DragAutoScrollMath.reachSide(bodyPointSize: standard))
|
||||
|
||||
// And the sideways rule holds on the zoomed ruler too: half the distance between two lanes'
|
||||
// scroll areas is the gap plus a plate padding on each side.
|
||||
let gap = BoardMetrics.stripGap(bodyPointSize: zoomed)
|
||||
let padding = BoardMetrics.lanePlatePadding(bodyPointSize: zoomed)
|
||||
#expect(DragAutoScrollMath.reachSide(bodyPointSize: zoomed) <= (gap + 2 * padding) / 2)
|
||||
}
|
||||
|
||||
// MARK: Stepping the offset
|
||||
|
||||
Reference in New Issue
Block a user