Skip to content

View → Zoom (⌘=/⌘−), zoom HUD, native checkmarks in theme menu#22

Merged
jvanderberg merged 2 commits into
tqbf:mainfrom
jvanderberg:feat/zoom
May 3, 2026
Merged

View → Zoom (⌘=/⌘−), zoom HUD, native checkmarks in theme menu#22
jvanderberg merged 2 commits into
tqbf:mainfrom
jvanderberg:feat/zoom

Conversation

@jvanderberg

Copy link
Copy Markdown
Collaborator

Summary

  • Zoom: `ThemeManager.fontScale` is a persisted (`@AppStorage("mdv_font_scale")`) multiplier on top of `MDVTheme.baseFontSize`. `MDVTheme.markdownTheme(scale:)` multiplies body size; headings are em-relative so they scale automatically. Per-element point spacing is left as-is — matches browser zoom feel (text grows, column doesn't change shape).
  • Hotkeys: View → Zoom In (⌘=, also catches ⌘+ since shift is irrelevant under .command), Zoom Out (⌘−), Actual Size (menu only — ⌘0 is reserved for Jump to Placeholder). 10% steps, clamped to `[0.6, 2.5]`. Menu items disable at the floor / ceiling / 100% respectively.
  • HUD: macOS volume/brightness-style transient overlay showing the current percentage, debounced via a `DispatchWorkItem` so repeated zoom keystrokes keep the HUD visible without flicker. Auto-dismisses ~0.9 s after the last change.
  • Theme picker checkmarks: switched from `Label(_:systemImage:)` (which under-renders inside macOS menus) to `Toggle` inside the `Menu`. Toggle in a macOS Menu renders as a native checked menu item — system-drawn checkmark, proper indentation for unselected siblings — for both the "System" entry and each MDVTheme. Setter ignores `false` because there's no "unchecked theme" state; picking a different one is how you switch.

Test plan

  • `./build.sh debug` and `./build.sh release` build clean.
  • ⌘= zooms body type up in 10% steps; ⌘− zooms down; the HUD shows the current %.
  • HUD stays visible while you keep tapping; fades out ~0.9 s after the last keystroke.
  • Actual Size menu item is disabled at 100% and resets the scale otherwise.
  • Setting persists across launches.
  • Headings scale proportionally with body across all themes (Charcoal, Sevilla, Twilight, OpenDyslexic, etc.).
  • Toolbar paintpalette menu shows a system checkmark next to the active theme; "System" shows it when sentinel-selected.

* `ThemeManager` gains a `fontScale` (`@AppStorage("mdv_font_scale")`,
  10% steps clamped to [0.6, 2.5]) plus `zoomIn` / `zoomOut` /
  `resetZoom` helpers. `MDVTheme.markdownTheme(scale:)` multiplies
  body size; headings are em-relative so they scale automatically.
  Per-element point spacing is left alone — matches browser zoom feel.

* View menu items: Zoom In (⌘=, also fires on ⌘+ since shift is
  irrelevant under .command), Zoom Out (⌘−), Actual Size (menu only,
  ⌘0 is reserved for Jump to Placeholder). Disabled at the floor /
  ceiling / 100% respectively.

* macOS-style transient HUD: centered overlay shows the current
  percentage, debounced ~0.9s after the last change so repeated taps
  keep it visible without flicker.

* Theme picker switched from `Label(_:systemImage:)` checkmarks to
  `Toggle` inside the Menu — Toggle in a macOS Menu renders as a
  native checked menu item (system-drawn check, proper indentation
  for unselected siblings). Setter ignores `false` because there's
  no "unchecked theme" state — picking a different one is how you
  switch.
`@State currentTopBlock` was mirrored from the computed `topVisibleBlock`
via `.onChange(of: topVisibleBlock) { currentTopBlock = newValue }`.
`topVisibleBlock` (== `visibleBlocks.min()`) is derived from
`visibleBlocks`, which is mutated during layout by per-block
`.onAppear` / `.onDisappear` callbacks. So writing state inside that
onChange ran during a render-driven invalidation: the write triggered
another body evaluation, ForEach re-iterated, blocks fired their
onAppear/onDisappear under different visibility (e.g. while the
sidebar toggle was simultaneously resizing the column), `visibleBlocks`
churned again, and the loop never settled — main thread pegged at
100% CPU inside `GraphHost.flushTransactions` →
`LazySubviewPlacements.updateValue`.

Reproducible by toggling the inspector while clicking a TOC entry.

The mirror was unnecessary in the first place. All four readers
(`onChange(of: selectedEntry)`, `goBack`, `goForward`,
`pushSameDocSnapshot`) are imperative — invoked from button handlers
or navigation events — and can read the computed `topVisibleBlock`
directly at the moment they need it. Removing the mirror also
eliminates the `currentTopBlock = 0` defer reset that was racing
with `persistScrollPosition`.

Adds a CLAUDE.md note documenting the pitfall and the diagnostic
signature (repeated `flushTransactions` frames in `sample <pid>`)
so it doesn't recur.
@jvanderberg jvanderberg merged commit 7c77ab2 into tqbf:main May 3, 2026
3 checks passed
@jvanderberg jvanderberg deleted the feat/zoom branch May 3, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant