View → Zoom (⌘=/⌘−), zoom HUD, native checkmarks in theme menu#22
Merged
Conversation
* `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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Test plan