fix(shell): never step a stored out-of-range zoom level against the request

A level persisted before the shortcuts existed (the macOS menu roles never
clamped, and the store restores any finite level) was clamped BEFORE the
step, so the first zoom-in from level 7 rendered smaller. Step from the raw
level instead: a press further out leaves an out-of-range level where it is,
a press back in lands on the limit.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
4grayandClaude Fable 5.1 committed 2026-09-18 22:16:39 +02:00
1 parent 67327bfd29
commit 4586616d54
3 files changed
+31 -9

No files matched your search

+4 -1
View File
@@ -473,7 +473,10 @@ Zoom level (Cmd/Ctrl and +/−/0, issue #1109):
(`stepZoomLevel`): 0.5 per press, Electron's own `zoomIn`/`zoomOut` role
step (≈10 %), clamped to levels −4…6 (≈48 %…299 %, inside Chromium's
25–500 %), off-grid levels snapping to the next grid point in the pressed
direction; `Ctrl/Cmd+0` returns to level 0. Persistence needs nothing
direction; `Ctrl/Cmd+0` returns to level 0. A stored level already
outside the limits (the macOS menu roles never clamped) is never moved
against the request: a press further out leaves it, a press back in
lands on the limit. Persistence needs nothing
extra: the main process reads the live level back (point 3). On macOS the
default application menu still carries the `zoomIn`/`zoomOut`/`resetZoom`
roles, but Chromium hands a key equivalent to the web contents first and
@@ -23,13 +23,25 @@ describe('zoom level shortcuts contract', () => {
it('clamps at the app limits, not at Chromium limits', () => {
expect(stepZoomLevel(ZOOM_LEVEL_MAX, 'in')).toBe(ZOOM_LEVEL_MAX);
expect(stepZoomLevel(ZOOM_LEVEL_MIN, 'out')).toBe(ZOOM_LEVEL_MIN);
expect(stepZoomLevel(ZOOM_LEVEL_MAX + 3, 'in')).toBe(ZOOM_LEVEL_MAX);
expect(stepZoomLevel(ZOOM_LEVEL_MAX + 3, 'out')).toBe(
ZOOM_LEVEL_MAX - ZOOM_LEVEL_STEP
);
expect(stepZoomLevel(ZOOM_LEVEL_MAX - 0.5, 'in')).toBe(ZOOM_LEVEL_MAX);
expect(clampZoomLevel(ZOOM_LEVEL_MIN - 1)).toBe(ZOOM_LEVEL_MIN);
});
it('never moves a stored out-of-range level against the request', () => {
// The macOS menu roles never clamped and the store restores any
// finite level, so an upgrade can start beyond the limits.
expect(stepZoomLevel(ZOOM_LEVEL_MAX + 1, 'in')).toBe(
ZOOM_LEVEL_MAX + 1
);
expect(stepZoomLevel(ZOOM_LEVEL_MAX + 1, 'out')).toBe(ZOOM_LEVEL_MAX);
expect(stepZoomLevel(ZOOM_LEVEL_MAX + 0.3, 'out')).toBe(ZOOM_LEVEL_MAX);
expect(stepZoomLevel(ZOOM_LEVEL_MIN - 1, 'out')).toBe(
ZOOM_LEVEL_MIN - 1
);
expect(stepZoomLevel(ZOOM_LEVEL_MIN - 1, 'in')).toBe(ZOOM_LEVEL_MIN);
expect(stepZoomLevel(ZOOM_LEVEL_MIN - 1, 'reset')).toBe(0);
});
it('snaps an off-grid level to the next grid point in the requested direction', () => {
// A level the macOS menu roles or an older store left between steps.
expect(stepZoomLevel(0.3, 'in')).toBe(0.5);
@@ -33,7 +33,10 @@ export function clampZoomLevel(level: number): number {
* The level `action` produces from `current`. Steps are snapped to the step
* grid so a level restored from an older store (or set by the macOS menu
* before the shortcuts existed) does not carry a fractional offset forever.
* A non-finite `current` is treated as the default.
* A level already outside the limits — the macOS menu roles never clamped,
* and the store restores any finite level — is never moved AGAINST the
* request: a press further out leaves it where it is, a press back in lands
* on the limit. A non-finite `current` is treated as the default.
*/
export function stepZoomLevel(
current: number,
@@ -43,8 +46,13 @@ export function stepZoomLevel(
return ZOOM_LEVEL_DEFAULT;
}
const base = clampZoomLevel(current);
const base = Number.isFinite(current) ? current : ZOOM_LEVEL_DEFAULT;
const direction = action === 'in' ? 1 : -1;
if (direction > 0 ? base >= ZOOM_LEVEL_MAX : base <= ZOOM_LEVEL_MIN) {
return base;
}
const steps = base / ZOOM_LEVEL_STEP;
// A level between grid points steps to the next grid point in the
// requested direction instead of skipping past it.
@@ -53,7 +61,6 @@ export function stepZoomLevel(
: direction > 0
? Math.ceil(steps)
: Math.floor(steps);
const next = nextSteps * ZOOM_LEVEL_STEP;
return clampZoomLevel(next);
return clampZoomLevel(nextSteps * ZOOM_LEVEL_STEP);
}