Merge pull request #73161 from NousResearch/bb/default-zoom-out

feat(desktop): default UI zoom to Appearance 90% preset
This commit is contained in:
brooklyn! 2026-07-28 04:08:15 -05:00 committed by GitHub
commit 7ddf3f1500
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 71 additions and 35 deletions

View file

@ -120,6 +120,17 @@ export function createSandbox(prefix: string): Sandbox {
'utf8',
)
// Pin Chromium actual-size zoom (level 0) for the suite. Fresh installs
// ship DEFAULT_ZOOM_LEVEL at the Appearance 90% preset, but Playwright
// click hit-testing and the committed visual baselines were calibrated at
// 100%. Without this file every sandbox would inherit the product default
// and fail pointer interception + snapshot diffs.
fs.writeFileSync(
path.join(userDataDir, 'zoom-state.json'),
JSON.stringify({ zoomLevel: 0 }, null, 2),
'utf8',
)
return {
root,
hermesHome,

View file

@ -5187,7 +5187,7 @@ function buildApplicationMenu() {
label: 'Actual Size',
accelerator: 'CommandOrControl+0',
click: () => {
setAndPersistZoomLevel(mainWindow, 0)
setAndPersistZoomLevel(mainWindow, DEFAULT_ZOOM_LEVEL)
}
},
{
@ -5195,7 +5195,7 @@ function buildApplicationMenu() {
accelerator: 'CommandOrControl+Plus',
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() + 0.1)
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() + ZOOM_STEP)
}
}
},
@ -5204,7 +5204,7 @@ function buildApplicationMenu() {
accelerator: 'CommandOrControl+-',
click: () => {
if (mainWindow && !mainWindow.isDestroyed()) {
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() - 0.1)
setAndPersistZoomLevel(mainWindow, mainWindow.webContents.getZoomLevel() - ZOOM_STEP)
}
}
},
@ -5283,8 +5283,10 @@ function installPreviewShortcut(window) {
// read it back on did-finish-load to re-apply after reloads or crash recovery.
import {
applyZoomLevel,
DEFAULT_ZOOM_LEVEL,
installZoomReassertOnWindowEvents,
percentToZoomLevel,
ZOOM_STEP,
ZOOM_STORAGE_KEY,
zoomLevelToPercent,
zoomWiringForWindowKind
@ -5328,31 +5330,33 @@ function restorePersistedZoomLevel(window) {
return
}
// Fall back to localStorage for installs that predate zoom-state.json,
// migrating the value into the JSON store on first read.
// No JSON yet: paint the shipped default immediately so a fresh install
// doesn't flash Chromium 100%, then try localStorage for pre-JSON installs
// and overwrite if a legacy value is there.
applyZoomLevel(window.webContents, DEFAULT_ZOOM_LEVEL)
window.webContents
.executeJavaScript(
`(() => { try { return localStorage.getItem(${JSON.stringify(ZOOM_STORAGE_KEY)}) } catch { return null } })()`
)
.then(stored => {
if (stored == null || !window || window.isDestroyed()) {
if (!window || window.isDestroyed()) {
return
}
// Notify the renderer too — otherwise the Appearance UI Scale control
// can stay stuck at 100% even though the window zoom was restored.
const applied = applyZoomLevel(window.webContents, Number(stored))
const level = stored == null ? DEFAULT_ZOOM_LEVEL : Number(stored)
const applied = applyZoomLevel(window.webContents, level)
writeZoomState(applied)
})
.catch(error => rememberLog(`[zoom] restore failed: ${error?.message || error}`))
}
function installZoomShortcuts(window) {
// Override Ctrl/Cmd + +/-/0 with half the default zoom step (0.1 vs 0.2).
// The menu items handle this on macOS (where the menu is always present),
// but on Linux/Windows the menu is null and Chromium's default handler
// would use the full 0.2 step, so we intercept here for consistency.
const ZOOM_STEP = 0.1
// Override Ctrl/Cmd + +/-/0 with half Chromium's default zoom step (ZOOM_STEP
// is 0.1 vs Chromium's 0.2). The menu items handle this on macOS (where the
// menu is always present), but on Linux/Windows the menu is null and
// Chromium's default handler would use the full 0.2 step, so we intercept
// here for consistency. Ctrl/Cmd+0 resets to DEFAULT_ZOOM_LEVEL, not Chromium 0.
window.webContents.on('before-input-event', (event, input) => {
const mod = IS_MAC ? input.meta : input.control
@ -5368,7 +5372,7 @@ function installZoomShortcuts(window) {
}
event.preventDefault()
setAndPersistZoomLevel(window, 0)
setAndPersistZoomLevel(window, DEFAULT_ZOOM_LEVEL)
} else if (key === '=' || key === '+') {
// Zoom-in must accept the shift modifier: on US layouts Plus is
// physically Shift+=, so Cmd+Plus arrives as Cmd+Shift+'+' (or '='
@ -9266,7 +9270,8 @@ ipcMain.handle('hermes:window:openInstance', async () => {
// shortcuts and the View menu. Reads and writes target the asking window.
ipcMain.handle('hermes:zoom:get', event => {
const window = BrowserWindow.fromWebContents(event.sender)
const level = window && !window.isDestroyed() ? window.webContents.getZoomLevel() : 0
const level =
window && !window.isDestroyed() ? window.webContents.getZoomLevel() : DEFAULT_ZOOM_LEVEL
return { level, percent: zoomLevelToPercent(level) }
})

View file

@ -11,9 +11,11 @@ import { test, vi } from 'vitest'
import {
applyZoomLevel,
clampZoomLevel,
DEFAULT_ZOOM_LEVEL,
installZoomReassertOnWindowEvents,
percentToZoomLevel,
ZOOM_RESIZE_REASSERT_DELAY_MS,
ZOOM_STEP,
ZOOM_STORAGE_KEY,
zoomLevelToPercent,
zoomReassertWindowEvents,
@ -24,26 +26,32 @@ test('storage key stays stable so persisted zoom survives upgrades', () => {
assert.equal(ZOOM_STORAGE_KEY, 'hermes:desktop:zoomLevel')
})
test('default zoom matches the Appearance 90% preset', () => {
assert.equal(ZOOM_STEP, 0.1)
assert.equal(zoomLevelToPercent(DEFAULT_ZOOM_LEVEL), 90)
assert.equal(DEFAULT_ZOOM_LEVEL, percentToZoomLevel(90))
})
test('clampZoomLevel rejects garbage and enforces bounds', () => {
assert.equal(clampZoomLevel(NaN), 0)
assert.equal(clampZoomLevel(Infinity), 0)
assert.equal(clampZoomLevel(undefined), 0)
assert.equal(clampZoomLevel('2'), 0)
assert.equal(clampZoomLevel(NaN), DEFAULT_ZOOM_LEVEL)
assert.equal(clampZoomLevel(Infinity), DEFAULT_ZOOM_LEVEL)
assert.equal(clampZoomLevel(undefined), DEFAULT_ZOOM_LEVEL)
assert.equal(clampZoomLevel('2'), DEFAULT_ZOOM_LEVEL)
assert.equal(clampZoomLevel(0.3), 0.3)
assert.equal(clampZoomLevel(-42), -9)
assert.equal(clampZoomLevel(42), 9)
})
test('level 0 is exactly 100 percent', () => {
test('level 0 is exactly 100 percent (Chromium actual-size baseline)', () => {
assert.equal(zoomLevelToPercent(0), 100)
assert.equal(percentToZoomLevel(100), 0)
})
test('percentToZoomLevel rejects garbage', () => {
assert.equal(percentToZoomLevel(NaN), 0)
assert.equal(percentToZoomLevel(0), 0)
assert.equal(percentToZoomLevel(-50), 0)
assert.equal(percentToZoomLevel(undefined), 0)
test('percentToZoomLevel rejects garbage by falling back to the shipped default', () => {
assert.equal(percentToZoomLevel(NaN), DEFAULT_ZOOM_LEVEL)
assert.equal(percentToZoomLevel(0), DEFAULT_ZOOM_LEVEL)
assert.equal(percentToZoomLevel(-50), DEFAULT_ZOOM_LEVEL)
assert.equal(percentToZoomLevel(undefined), DEFAULT_ZOOM_LEVEL)
})
test('preset percentages roundtrip within rounding', () => {

View file

@ -2,8 +2,11 @@
* Pure helpers for window zoom. The main process owns webContents.setZoomLevel,
* so the menu items, the Ctrl/Cmd shortcuts, and the settings UI all funnel
* through this one clamped scale. Percent is the user-facing unit (100 = the
* default size); Chromium's internal unit is the zoom level, where
* factor = 1.2 ^ level.
* Chromium actual-size baseline); Chromium's internal unit is the zoom level,
* where factor = 1.2 ^ level.
*
* Our shipped default is the Appearance 90% preset tight enough to feel
* denser than Chromium 100%, and selected in the UI Scale control on first run.
*/
export const ZOOM_STORAGE_KEY = 'hermes:desktop:zoomLevel'
@ -12,9 +15,15 @@ const ZOOM_FACTOR_BASE = 1.2
const MIN_ZOOM_LEVEL = -9
const MAX_ZOOM_LEVEL = 9
/** Half Chromium's default step; matching the shortcuts and View menu. */
export const ZOOM_STEP = 0.1
/** Appearance 90% preset. Fresh installs + Actual Size / Ctrl+0. */
export const DEFAULT_ZOOM_LEVEL = Math.log(0.9) / Math.log(ZOOM_FACTOR_BASE)
export function clampZoomLevel(value) {
if (!Number.isFinite(value)) {
return 0
return DEFAULT_ZOOM_LEVEL
}
return Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
@ -26,7 +35,7 @@ export function zoomLevelToPercent(level) {
export function percentToZoomLevel(percent) {
if (!Number.isFinite(percent) || percent <= 0) {
return 0
return DEFAULT_ZOOM_LEVEL
}
return clampZoomLevel(Math.log(percent / 100) / Math.log(ZOOM_FACTOR_BASE))

View file

@ -64,10 +64,11 @@ function ThemePreview({ name, mode }: { name: string; mode: 'light' | 'dark' })
)
}
// UI scale presets, as zoom percentages. 100 is the browser-default size;
// the ids double as the percent values sent to the main process. A Cmd/Ctrl
// +/- step landing between presets highlights nothing, and the row
// description keeps showing the exact current percent.
// UI scale presets, as zoom percentages. 100 is Chromium's actual-size
// baseline; the shipped default is the 90% preset. Ids double as the percent
// values sent to the main process. A Cmd/Ctrl +/- step landing between
// presets highlights nothing, and the row description keeps showing the
// exact current percent.
const UI_SCALE_PRESETS = ['90', '100', '110', '125', '150', '175'] as const
type UiScalePreset = (typeof UI_SCALE_PRESETS)[number]

View file

@ -10,7 +10,9 @@
import { atom } from 'nanostores'
export const $zoomPercent = atom<number>(100)
// Mirror DEFAULT_ZOOM_LEVEL (90%) so Appearance doesn't flash 100% before
// the main-process zoom.get() resolves. Keep in sync with electron/zoom.ts.
export const $zoomPercent = atom<number>(90)
export function setZoomPercent(percent: number): void {
window.hermesDesktop?.zoom?.setPercent(percent)