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:
@@ -9,6 +9,8 @@ extension NSToolbarItem.Identifier {
|
||||
static let boardUndo = Self("board.undo")
|
||||
static let boardRedo = Self("board.redo")
|
||||
static let boardShowTrash = Self("board.showTrash")
|
||||
static let boardZoomIn = Self("board.zoomIn")
|
||||
static let boardZoomOut = Self("board.zoomOut")
|
||||
}
|
||||
|
||||
// MARK: - The board window's toolbar
|
||||
@@ -20,8 +22,8 @@ extension NSToolbarItem.Identifier {
|
||||
/// "**Board window default: the search field, nothing else** — trailing, the one default item; the
|
||||
/// titlebar stays clean." The flexible space ahead of it is what "trailing" means to `NSToolbar`.
|
||||
///
|
||||
/// "**Catalog** (available via Customize): New Card, New Lane, Undo, Redo …, Show Trash (toggle
|
||||
/// state matching the View menu checkmark)." Every one of them is the *same command* as its menu row
|
||||
/// "**Catalog** (available via Customize): New Card, New Lane, Zoom In, Zoom Out …, Undo, Redo …,
|
||||
/// Show Trash (toggle state matching the View menu checkmark)." Every one of them is the *same command* as its menu row
|
||||
/// — the predicates below are the rows' own (`BoardStore.newCardTarget`, `acceptsBoardMutations`),
|
||||
/// and the two actions with consequences call the rows' own functions (`beginNewCard`,
|
||||
/// `setTrashVisible`) rather than restating them. That is what makes "toolbars are pure enhancement"
|
||||
@@ -55,7 +57,21 @@ enum BoardToolbar {
|
||||
/// "The search field, nothing else — trailing, the one default item."
|
||||
static let defaultItems: [NSToolbarItem.Identifier] = [.flexibleSpace, .boardSearch]
|
||||
|
||||
static func specs(store: BoardStore, search: BoardSearchPresentation) -> [ToolbarItemSpec] {
|
||||
/// - Parameters:
|
||||
/// - zoom: the app-wide zoom level, in the catalog order the palette shows. It is not the
|
||||
/// store's, unlike every other predicate here, because the level is not a board's
|
||||
/// (03-board-ui.md ▸ Layout — zoom) — and it must be `@Observable` rather than read from
|
||||
/// `UserDefaults` at build time, since `WindowToolbarController.trackValidationState` re-arms
|
||||
/// observation over each spec's `isEnabled` and a plain scalar would leave Zoom In looking live
|
||||
/// at the top rung.
|
||||
/// - session: the app's drag session, for the same guard the menu rows carry
|
||||
/// (`ZoomCommands.isEnabled`).
|
||||
static func specs(
|
||||
store: BoardStore,
|
||||
search: BoardSearchPresentation,
|
||||
zoom: BoardZoomStore,
|
||||
session: DragSession
|
||||
) -> [ToolbarItemSpec] {
|
||||
[
|
||||
.mirroring(
|
||||
menuTitle: "New Card",
|
||||
@@ -75,6 +91,34 @@ enum BoardToolbar {
|
||||
perform: { [weak store] in store?.createLane() }
|
||||
)
|
||||
),
|
||||
// The zoom pair — plain buttons, because that is what the menu rows are. There is no
|
||||
// percentage readout and no popup: a control that *displays* the level would need a
|
||||
// custom view and a new `ToolbarItemSpec.Behavior` case, and the level already has a
|
||||
// spoken voice (`AccessibilityPhrases.zoomLevel`) and a visible one (the board itself).
|
||||
.mirroring(
|
||||
menuTitle: "Zoom In",
|
||||
identifier: .boardZoomIn,
|
||||
symbol: "plus.magnifyingglass",
|
||||
behavior: .button(
|
||||
isEnabled: { [weak store, weak zoom, weak session] in
|
||||
guard let zoom, let session else { return false }
|
||||
return ZoomCommands.isEnabled(store: store, session: session) && zoom.canZoomIn
|
||||
},
|
||||
perform: { [weak zoom] in zoom?.step(.in) }
|
||||
)
|
||||
),
|
||||
.mirroring(
|
||||
menuTitle: "Zoom Out",
|
||||
identifier: .boardZoomOut,
|
||||
symbol: "minus.magnifyingglass",
|
||||
behavior: .button(
|
||||
isEnabled: { [weak store, weak zoom, weak session] in
|
||||
guard let zoom, let session else { return false }
|
||||
return ZoomCommands.isEnabled(store: store, session: session) && zoom.canZoomOut
|
||||
},
|
||||
perform: { [weak zoom] in zoom?.step(.out) }
|
||||
)
|
||||
),
|
||||
.staticLabel(
|
||||
"Undo",
|
||||
identifier: .boardUndo,
|
||||
@@ -132,10 +176,15 @@ enum BoardToolbar {
|
||||
/// The window's toolbar, wired to tell the search presentation where its field currently lives —
|
||||
/// which is the whole input to ⌘F's transient fallback (03: "with the field removed from the
|
||||
/// toolbar, invoking it surfaces the field transiently until the search clears").
|
||||
static func controller(store: BoardStore, search: BoardSearchPresentation) -> WindowToolbarController {
|
||||
static func controller(
|
||||
store: BoardStore,
|
||||
search: BoardSearchPresentation,
|
||||
zoom: BoardZoomStore,
|
||||
session: DragSession
|
||||
) -> WindowToolbarController {
|
||||
let controller = WindowToolbarController(
|
||||
identifier: identifier,
|
||||
specs: specs(store: store, search: search),
|
||||
specs: specs(store: store, search: search, zoom: zoom, session: session),
|
||||
defaults: defaultItems
|
||||
)
|
||||
// Centered against the window, not a flexible-space sandwich (03 ▸ Toolbar's placement
|
||||
|
||||
Reference in New Issue
Block a user