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:
@@ -40,6 +40,20 @@ final class DragAutoScroller {
|
||||
/// Invoked after every scroll step. Re-resolves the drop proposal; see the type's note.
|
||||
fileprivate var didScroll: (() -> Void)?
|
||||
|
||||
/// The board's ruler, for the engagement rect's three reaches — they are distances to the lane's
|
||||
/// own furniture, which moves with the zoom level (`DragAutoScrollMath.reachAbove`;
|
||||
/// 03-board-ui.md ▸ Layout — zoom).
|
||||
///
|
||||
/// Set by the lane at the head of each drag rather than observed, because that is exactly when it
|
||||
/// can change: zoom is inert while a session is in flight (`ZoomCommands.isEnabled`), so one read
|
||||
/// per drag is one read per level.
|
||||
///
|
||||
/// Optional, and resolved at use rather than defaulted in `init`, because the initialiser is
|
||||
/// `nonisolated` (the tick-source seam is constructed off the main actor in tests) and the system
|
||||
/// body size is a main-actor read. Un-set means "nobody has told me", which resolves to the
|
||||
/// system's ruler — an un-zoomed board's answer.
|
||||
var bodyPointSize: CGFloat?
|
||||
|
||||
/// Where frames come from. One implementation ships (`DisplayLinkTickSource`); the seam exists
|
||||
/// because a `CADisplayLink` needs a screen and a run loop and so cannot tick in a test bundle
|
||||
/// (`DragAutoScrollDriverTests`).
|
||||
@@ -81,7 +95,9 @@ final class DragAutoScroller {
|
||||
let inWindow = window.convertPoint(fromScreen: NSEvent.mouseLocation)
|
||||
let inClip = clip.convert(inWindow, from: nil)
|
||||
let pointer = CGPoint(x: inClip.x - visible.minX, y: inClip.y - visible.minY)
|
||||
guard DragAutoScrollMath.engagementRect(viewport: visible.size).contains(pointer) else { return }
|
||||
let ruler = bodyPointSize ?? BoardMetrics.bodyPointSize
|
||||
guard DragAutoScrollMath.engagementRect(viewport: visible.size, bodyPointSize: ruler)
|
||||
.contains(pointer) else { return }
|
||||
|
||||
let velocity = DragAutoScrollMath.velocity(pointer: pointer, viewport: visible.size)
|
||||
guard velocity.dx != 0 || velocity.dy != 0 else { return }
|
||||
|
||||
Reference in New Issue
Block a user