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:
@@ -111,6 +111,11 @@ struct BoardView: View {
|
||||
/// `Accommodations.frost`).
|
||||
@Environment(\.accessibilityReduceTransparency) private var reduceTransparency
|
||||
|
||||
/// **The board's ruler** (03-board-ui.md ▸ Layout — zoom; `BoardZoom`). Injected on this view by
|
||||
/// `BoardWindowHost` and read all the way down the strip; every `BoardMetrics` figure below takes
|
||||
/// `zoom.bodyPointSize` rather than the system's, which is the whole of what View ▸ Zoom In does.
|
||||
@Environment(\.boardZoom) private var zoom
|
||||
|
||||
/// Whether the strip holds keyboard focus, which is what makes the grammar keys arrive. Restored
|
||||
/// deliberately whenever an inline editor closes: the field that had focus is gone, and Return
|
||||
/// must go back to meaning create/rename rather than nothing at all.
|
||||
@@ -124,7 +129,7 @@ struct BoardView: View {
|
||||
/// size widens the gap and therefore *narrows* every lane, since the window's width still divides
|
||||
/// across `units + 1` gaps. The strip never grows and never scrolls; the lanes compress, "the
|
||||
/// degenerate case accepted, not floored" (03-board-ui.md § Layout — full visibility).
|
||||
private var spacing: CGFloat { BoardMetrics.stripGap(bodyPointSize: BoardMetrics.bodyPointSize) }
|
||||
private var spacing: CGFloat { BoardMetrics.stripGap(bodyPointSize: zoom.bodyPointSize) }
|
||||
|
||||
var body: some View {
|
||||
// The strip's own body count (`BoardRenderMetrics`) — DEBUG only, and the discriminator
|
||||
@@ -160,6 +165,22 @@ struct BoardView: View {
|
||||
// rather than into `@State` so the drop delegates read it live at event time rather than
|
||||
// as of the last body evaluation.
|
||||
.onGeometryChange(for: CGRect.self) { $0.frame(in: .global) } action: { laneDrops.stripFrame = $0 }
|
||||
// The board's ruler, into the registry beside the strip's frame and for its reason: the
|
||||
// drop delegates run at event time, outside any body, and `nominalCardHeight` — the
|
||||
// stand-in they tile un-measured rows with — has to be computed on the zoom the board is
|
||||
// actually drawing at (03-board-ui.md ▸ Layout — zoom). `initial: true` because the first
|
||||
// render is already a level, not a change.
|
||||
.onChange(of: zoom.bodyPointSize, initial: true) {
|
||||
laneDrops.bodyPointSize = zoom.bodyPointSize
|
||||
// And the resting layouts built on the old ruler go with it. `RestingLayoutCache`'s
|
||||
// entry key is snapshot generation, heights generation, board root and hidden set —
|
||||
// deliberately not the point size, because until zoom existed the point size could
|
||||
// not move. This is what keeps that key honest rather than adding a fifth term to it:
|
||||
// a level change is rare, a cache miss costs one rebuild, and `ZoomCommands` already
|
||||
// holds the rows shut while a drag is in flight, so in practice there is nothing
|
||||
// standing here to clear.
|
||||
dropContext.session.restingLayouts.clear()
|
||||
}
|
||||
// **The strip's drop target** — the backdrop, the gaps, the outer margin, and the trash
|
||||
// column's footprint, which is never a landing spot of its own (04-interactions.md ▸ The
|
||||
// trash) and so simply falls through to here. It accepts *every* session type — ours and
|
||||
@@ -324,7 +345,7 @@ struct BoardView: View {
|
||||
// One of the drag's N contiguous shadows, at the exact width the arriving lane
|
||||
// will occupy — its units measured against *this* strip's standard, which is
|
||||
// what makes the drop land precisely where the shadow shows.
|
||||
DragShadow(cornerRadius: BoardMetrics.laneCornerRadius(bodyPointSize: BoardMetrics.bodyPointSize))
|
||||
DragShadow(cornerRadius: BoardMetrics.laneCornerRadius(bodyPointSize: zoom.bodyPointSize))
|
||||
.frame(width: LaneLayoutMath.slotWidth(units: units, standard: standard, gap: spacing))
|
||||
.frame(maxHeight: .infinity)
|
||||
}
|
||||
@@ -568,7 +589,7 @@ struct BoardView: View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
if resizing {
|
||||
DragShadow(
|
||||
cornerRadius: BoardMetrics.laneCornerRadius(bodyPointSize: BoardMetrics.bodyPointSize),
|
||||
cornerRadius: BoardMetrics.laneCornerRadius(bodyPointSize: zoom.bodyPointSize),
|
||||
dashed: false
|
||||
)
|
||||
.frame(width: slotWidth)
|
||||
|
||||
Reference in New Issue
Block a user