fix(desktop): preserve UI scale after resize

This commit is contained in:
kosta 2026-07-18 03:38:44 -04:00 committed by Teknium
parent 5f95b251d6
commit 036b659527
3 changed files with 85 additions and 18 deletions

View file

@ -7173,9 +7173,9 @@ function wireCommonWindowHandlers(win, { zoom = true }: { zoom?: boolean } = {})
if (zoom) {
installZoomShortcuts(win)
// Re-apply persisted zoom on show/restore/cross-display move (Windows can
// drop webContents zoom after minimize or a monitor-scale change) and on
// EVERY full load — not once. The crash-recovery path calls
// 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).

View file

@ -6,16 +6,17 @@
import assert from 'node:assert/strict'
import { test } from 'vitest'
import { test, vi } from 'vitest'
import {
applyZoomLevel,
clampZoomLevel,
installZoomReassertOnWindowEvents,
percentToZoomLevel,
ZOOM_REASSERT_WINDOW_EVENTS,
ZOOM_RESIZE_REASSERT_DELAY_MS,
ZOOM_STORAGE_KEY,
zoomLevelToPercent,
zoomReassertWindowEvents,
zoomWiringForWindowKind
} from './zoom'
@ -64,7 +65,7 @@ test('extreme percentages clamp to the level bounds', () => {
assert.equal(percentToZoomLevel(1_000_000), 9)
})
test('installZoomReassertOnWindowEvents wires show, restore, and cross-display moves', () => {
test('installZoomReassertOnWindowEvents wires show, restore, resize, and cross-display moves on macOS and Windows', () => {
const handlers = new Map()
const win = {
@ -75,15 +76,62 @@ test('installZoomReassertOnWindowEvents wires show, restore, and cross-display m
}
let calls = 0
installZoomReassertOnWindowEvents(win, () => {
calls += 1
})
installZoomReassertOnWindowEvents(
win,
() => {
calls += 1
},
'win32'
)
assert.deepEqual([...handlers.keys()], [...ZOOM_REASSERT_WINDOW_EVENTS])
assert.deepEqual([...handlers.keys()], zoomReassertWindowEvents('win32'))
handlers.get('show')()
handlers.get('restore')()
handlers.get('resized')()
handlers.get('moved')()
assert.equal(calls, 3)
assert.equal(calls, 4)
})
test('installZoomReassertOnWindowEvents debounces Linux resize and move events at the trailing edge', () => {
vi.useFakeTimers()
try {
const handlers = new Map()
let destroyed = false
const win = {
isDestroyed: () => destroyed,
on(event, listener) {
handlers.set(event, listener)
}
}
let calls = 0
installZoomReassertOnWindowEvents(
win,
() => {
calls += 1
},
'linux'
)
assert.deepEqual([...handlers.keys()], zoomReassertWindowEvents('linux'))
handlers.get('resize')()
vi.advanceTimersByTime(ZOOM_RESIZE_REASSERT_DELAY_MS / 2)
handlers.get('move')()
vi.advanceTimersByTime(ZOOM_RESIZE_REASSERT_DELAY_MS / 2)
assert.equal(calls, 0)
vi.advanceTimersByTime(ZOOM_RESIZE_REASSERT_DELAY_MS / 2)
assert.equal(calls, 1)
handlers.get('resize')()
destroyed = true
vi.advanceTimersByTime(ZOOM_RESIZE_REASSERT_DELAY_MS)
assert.equal(calls, 1)
} finally {
vi.useRealTimers()
}
})
test('installZoomReassertOnWindowEvents skips destroyed windows', () => {

View file

@ -48,23 +48,42 @@ export function applyZoomLevel(webContents, level) {
return clamped
}
// Chromium on Windows can drop webContents zoom when a BrowserWindow is minimized
// and restored or crosses onto a monitor with different display scaling. Re-apply
// the persisted level after each completed lifecycle transition.
export const ZOOM_REASSERT_WINDOW_EVENTS = ['show', 'restore', 'moved']
// Chromium can drop webContents zoom when a BrowserWindow is resized, minimized
// and restored, or crosses onto a monitor with different display scaling. macOS
// and Windows provide trailing `resized`/`moved` events; Linux only provides the
// noisy `resize`/`move` pair, so debounce those fallbacks before re-applying the
// persisted level.
export const ZOOM_RESIZE_REASSERT_DELAY_MS = 100
export function installZoomReassertOnWindowEvents(win, reassert) {
export function zoomReassertWindowEvents(platform = process.platform) {
return platform === 'linux' ? ['show', 'restore', 'resize', 'move'] : ['show', 'restore', 'resized', 'moved']
}
export function installZoomReassertOnWindowEvents(win, reassert, platform = process.platform) {
if (!win?.on) {
return
}
for (const event of ZOOM_REASSERT_WINDOW_EVENTS) {
let resizeTimer
for (const event of zoomReassertWindowEvents(platform)) {
win.on(event, () => {
if (win.isDestroyed?.()) {
return
}
reassert()
if (event !== 'resize' && event !== 'move') {
reassert()
return
}
clearTimeout(resizeTimer)
resizeTimer = setTimeout(() => {
if (!win.isDestroyed?.()) {
reassert()
}
}, ZOOM_RESIZE_REASSERT_DELAY_MS)
})
}
}