fix(desktop): keep UI scale across in-page route navigation
Desktop is a HashRouter over one file:// document, so every route is a distinct URL to Chromium's per-URL zoom store. A route the user never zoomed on has no record at all and resolves to the host default (100%) — that is every fresh session and every never-visited settings tab. In-page navigation fires neither did-finish-load nor any window event, so nothing re-asserted the persisted level. The window dropped to 100% while the Appearance control kept reading the chosen scale, because the renderer only learns of zoom changes through 'hermes:zoom:changed', which never fired. Touching the setting sent a fresh apply, which is why it appeared to fix itself. Re-assert the persisted level on main-frame did-navigate-in-page. Verified on real Electron 40.10.2 / Chromium 144 (win32): a recordless hash route reports 100% at the event, so the existing drift-guard sees the drop and re-applies, and still no-ops when the route's record already matches. Fixes #48658 Fixes #38854 Fixes #79863 Co-authored-by: Brooklyn Nicholson <brooklyn.bb.nicholson@gmail.com>
This commit is contained in:
@@ -6557,6 +6557,7 @@ function installPreviewShortcut(window) {
|
||||
import {
|
||||
applyZoomLevel,
|
||||
DEFAULT_ZOOM_LEVEL,
|
||||
installZoomReassertOnNavigation,
|
||||
installZoomReassertOnWindowEvents,
|
||||
percentToZoomLevel,
|
||||
ZOOM_STEP,
|
||||
@@ -11202,13 +11203,15 @@ function wireCommonWindowHandlers(win, { zoom = true }: { zoom?: boolean } = {})
|
||||
if (zoom) {
|
||||
installZoomShortcuts(win)
|
||||
// Re-apply persisted zoom on show/restore/resize/cross-display move
|
||||
// (Chromium can drop webContents zoom after these window transitions) and
|
||||
// on EVERY full load — not once. The crash-recovery path calls
|
||||
// webContents.reload(), which fires did-finish-load again after a `once`
|
||||
// listener is spent, so zoom was silently lost on renderer crash
|
||||
// recovery and any in-place reload/navigation (#46429).
|
||||
installZoomReassertOnWindowEvents(win, () => restorePersistedZoomLevel(win))
|
||||
win.webContents.on('did-finish-load', () => restorePersistedZoomLevel(win))
|
||||
// (Chromium can drop webContents zoom after these window transitions), on
|
||||
// EVERY full load — not once, since crash recovery reloads and would
|
||||
// outlive a spent `once` listener (#46429) — and after in-page navigation,
|
||||
// where Chromium applies the target hash route's own per-URL zoom record
|
||||
// (see installZoomReassertOnNavigation; #48658, #38854, #79863).
|
||||
const reassertZoom = () => restorePersistedZoomLevel(win)
|
||||
|
||||
installZoomReassertOnWindowEvents(win, reassertZoom)
|
||||
installZoomReassertOnNavigation(win.webContents, reassertZoom)
|
||||
}
|
||||
|
||||
installContextMenuBridge(win)
|
||||
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
applyZoomLevel,
|
||||
clampZoomLevel,
|
||||
DEFAULT_ZOOM_LEVEL,
|
||||
installZoomReassertOnNavigation,
|
||||
installZoomReassertOnWindowEvents,
|
||||
isDebouncedReassertEvent,
|
||||
percentToZoomLevel,
|
||||
@@ -292,6 +293,34 @@ test('installZoomReassertOnWindowEvents skips destroyed windows', () => {
|
||||
assert.equal(calls, 0)
|
||||
})
|
||||
|
||||
test('installZoomReassertOnNavigation covers full loads and main-frame in-page routes', () => {
|
||||
const handlers = new Map()
|
||||
let destroyed = false
|
||||
let calls = 0
|
||||
|
||||
const webContents = {
|
||||
isDestroyed: () => destroyed,
|
||||
on(event, listener) {
|
||||
handlers.set(event, listener)
|
||||
}
|
||||
}
|
||||
|
||||
installZoomReassertOnNavigation(webContents, () => {
|
||||
calls += 1
|
||||
})
|
||||
|
||||
assert.deepEqual([...handlers.keys()], ['did-finish-load', 'did-navigate-in-page'])
|
||||
|
||||
handlers.get('did-finish-load')()
|
||||
handlers.get('did-navigate-in-page')({}, 'file:///app/index.html#/new', true)
|
||||
handlers.get('did-navigate-in-page')({}, 'file:///app/frame.html#anchor', false)
|
||||
assert.equal(calls, 2)
|
||||
|
||||
destroyed = true
|
||||
handlers.get('did-navigate-in-page')({}, 'file:///app/index.html#/session/next', true)
|
||||
assert.equal(calls, 2)
|
||||
})
|
||||
|
||||
// Zoom-wiring contract: chat windows keep global UI zoom while fixed-size
|
||||
// helper windows opt out. Tested via the extracted config — no source-text regex.
|
||||
test('chat windows opt into zoom', () => {
|
||||
|
||||
@@ -154,6 +154,45 @@ export function installZoomReassertOnWindowEvents(win, reassert, platform = proc
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Chromium persists zoom PER URL, and a hash route is a distinct URL. Desktop
|
||||
* is a HashRouter over one `file://index.html`, so every session/settings route
|
||||
* carries its own `partition.per_host_zoom_levels` record, and an in-page
|
||||
* navigation applies the target route's record over whatever the window is
|
||||
* showing. A route the user never zoomed on has no record at all, so it
|
||||
* resolves to the host default — level 0, i.e. 100%.
|
||||
*
|
||||
* In-page navigation fires neither `did-finish-load` nor any window event, so
|
||||
* nothing re-asserted the persisted level: opening a fresh session dropped the
|
||||
* window to 100% while the Appearance control kept reading the chosen scale (it
|
||||
* only learns of changes through `hermes:zoom:changed`, which never fired —
|
||||
* which is why touching the setting appeared to fix it). #48658, #38854, #79863.
|
||||
*
|
||||
* Verified on real Electron 40.10.2 / Chromium 144 (win32): at
|
||||
* `did-navigate-in-page` the frame already reports the target route's level, so
|
||||
* restorePersistedZoomLevel's drift-guard sees the drop and re-applies — and
|
||||
* still no-ops when the route's record already matches. Subframe hash changes
|
||||
* must not touch the chat window's UI scale.
|
||||
*/
|
||||
export function installZoomReassertOnNavigation(webContents, reassert) {
|
||||
if (!webContents?.on) {
|
||||
return
|
||||
}
|
||||
|
||||
const reassertIfAlive = () => {
|
||||
if (!webContents.isDestroyed?.()) {
|
||||
reassert()
|
||||
}
|
||||
}
|
||||
|
||||
webContents.on('did-finish-load', reassertIfAlive)
|
||||
webContents.on('did-navigate-in-page', (_event, _url, isMainFrame) => {
|
||||
if (isMainFrame) {
|
||||
reassertIfAlive()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Zoom-wiring decision per window kind. Chat windows (main + session + the HUD)
|
||||
* keep global UI zoom; the pet overlay and the Quick Entry composer opt out
|
||||
|
||||
Reference in New Issue
Block a user