Files
iptvnator/libs/shared/interfaces/src/lib/zoom-level.util.ts
T
4grayandClaude Fable 5.1 c016c73f86 feat(shell): zoom shortcuts on Windows and Linux (#1109) (#1623)
* feat(shell): zoom shortcuts on Windows and Linux (#1109)

Cmd/Ctrl and +/−/0 (numpad included) now zoom the app on every platform.
Windows/Linux run without a menu (`setMenu(null)`), so the shortcuts are a
renderer key binding in `WorkspaceKeyboardShortcutsService` calling a new
synchronous, preload-local bridge method `adjustZoomLevel`, which steps the
frame-bound temporary level through `webFrame.setZoomLevel` — never a
main-process `webContents.setZoomLevel`, whose per-URL entry the app's
`file://` path routing resets. Step and limits live in
`libs/shared/interfaces` (`stepZoomLevel`: 0.5 per press like Electron's
zoomIn/zoomOut roles, clamped to levels −4…6). On macOS the renderer sees the
key before the application menu, and `preventDefault()` keeps the menu role
from stepping a second time (Electron only performs the menu key equivalent
in its unhandled-keyboard-event hook).

Persistence is unchanged: the main process still reads the live level back
on close, quit and reload. The zoom E2E now drives the real shortcuts (in,
out, numpad, reset). Help dialog entries added and translated for all
locales; contract updated in docs/architecture/workspace-shell.md.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* 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>

* docs(shell): state the zoom bridge's return contract precisely

`adjustZoomLevel` steps by `stepZoomLevel`'s rules; a stored out-of-range
level is never moved against the request, so the returned level is not
itself guaranteed to be within `ZOOM_LEVEL_MIN..ZOOM_LEVEL_MAX`.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

* docs(release): add a release note for the zoom shortcuts

The Release note gate requires an added `.changes/*.md` for runtime changes;
the shortcuts are a user-visible feature of their own, so they get their own
note and the persistence note stays about persistence.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-19 10:47:32 +02:00

67 lines
2.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* App zoom shortcut contract (Cmd/Ctrl and +/−/0), shared by the renderer
* key binding (`WorkspaceKeyboardShortcutsService`) and the preload, which
* applies the result through `webFrame.setZoomLevel` (issue #1109: the level
* must stay a frame-bound temporary zoom; see
* `docs/architecture/workspace-shell.md`, "Zoom level").
*
* A Chromium zoom level is the exponent of a 1.2 factor: level 1 renders at
* 120 %, level −1 at ~83 %. The step matches Electron's `zoomIn`/`zoomOut`
* menu roles (0.5, about 10 % per press), so the level the macOS menu used
* to produce and the level the shortcuts produce are the same series.
*/
export type ZoomLevelAction = 'in' | 'out' | 'reset';
export const ZOOM_LEVEL_DEFAULT = 0;
/** Electron's `zoomIn`/`zoomOut` role step. */
export const ZOOM_LEVEL_STEP = 0.5;
/** ≈48 % — below this the workspace chrome is unreadable. */
export const ZOOM_LEVEL_MIN = -4;
/** ≈299 % — well inside Chromium's own 25–500 % limits. */
export const ZOOM_LEVEL_MAX = 6;
export function clampZoomLevel(level: number): number {
if (!Number.isFinite(level)) {
return ZOOM_LEVEL_DEFAULT;
}
return Math.min(ZOOM_LEVEL_MAX, Math.max(ZOOM_LEVEL_MIN, level));
}
/**
* 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 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,
action: ZoomLevelAction
): number {
if (action === 'reset') {
return ZOOM_LEVEL_DEFAULT;
}
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.
const nextSteps = Number.isInteger(steps)
? steps + direction
: direction > 0
? Math.ceil(steps)
: Math.floor(steps);
return clampZoomLevel(nextSteps * ZOOM_LEVEL_STEP);
}