fix: stop random hotkeys from snapping desktop zoom to 1

The main process was resetting webContents zoom to 1 on every
\zoom-changed\ event, which fires not just for native Chromium zoom
gestures but also for the renderer's own setZoomFactor IPC calls. Paired
with a keydown listener that re-sent the current zoom on every
ctrl-combination (ctrl+backspace, ctrl+z, ctrl+v, ...), this created a
self-triggered race that intermittently snapped the factor back to 1.

Make the renderer the single source of zoom truth: keyboard, wheel, and
menu all drive the same Solid signal, preventDefault blocks Chromium's
built-in accelerators before they race, and the main process disables
pinch zoom and no longer listens to zoom-changed.
This commit is contained in:
LukeParkerDev
2026-04-17 11:15:24 +10:00
parent 12fa782137
commit c510661ef3
4 changed files with 73 additions and 23 deletions

View File

@@ -75,9 +75,9 @@ export function createMenu(deps: Deps) {
{ role: "reload" },
{ role: "toggleDevTools" },
{ type: "separator" },
{ role: "resetZoom" },
{ role: "zoomIn" },
{ role: "zoomOut" },
{ label: "Actual Size", accelerator: "Cmd+0", click: () => deps.trigger("zoom.reset") },
{ label: "Zoom In", accelerator: "Cmd+=", click: () => deps.trigger("zoom.in") },
{ label: "Zoom Out", accelerator: "Cmd+-", click: () => deps.trigger("zoom.out") },
{ type: "separator" },
{ role: "togglefullscreen" },
],

View File

@@ -159,7 +159,9 @@ function injectGlobals(win: BrowserWindow, globals: Globals) {
function wireZoom(win: BrowserWindow) {
win.webContents.setZoomFactor(1)
win.webContents.on("zoom-changed", () => {
win.webContents.setZoomFactor(1)
})
// Disable Chromium's touch/pinch zoom. Keyboard and wheel zoom are handled
// in the renderer so the Solid `webviewZoom` signal stays the single source
// of truth; a stray `zoom-changed` handler here would race with the renderer
// and intermittently snap the factor back to 1.
void win.webContents.setVisualZoomLevelLimits(1, 1).catch(() => undefined)
}

View File

@@ -23,7 +23,7 @@ import { render } from "solid-js/web"
import pkg from "../../package.json"
import { initI18n, t } from "./i18n"
import { UPDATER_ENABLED } from "./updater"
import { webviewZoom } from "./webview-zoom"
import { webviewZoom, zoomIn, zoomOut, zoomReset } from "./webview-zoom"
import "./styles.css"
import { Button } from "@opencode-ai/ui/button"
import { Splash } from "@opencode-ai/ui/logo"
@@ -265,6 +265,9 @@ const createPlatform = (): Platform => {
let menuTrigger = null as null | ((id: string) => void)
window.api.onMenuCommand((id) => {
if (id === "zoom.in") return zoomIn()
if (id === "zoom.out") return zoomOut()
if (id === "zoom.reset") return zoomReset()
menuTrigger?.(id)
})
listenForDeepLinks()

View File

@@ -11,28 +11,73 @@ const OS_NAME = (() => {
return "unknown"
})()
const MIN_ZOOM = 0.2
const MAX_ZOOM = 10
const KEY_STEP = 0.2
const WHEEL_STEP = 0.1
const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM), MAX_ZOOM)
const [webviewZoom, setWebviewZoom] = createSignal(1)
const MAX_ZOOM_LEVEL = 10
const MIN_ZOOM_LEVEL = 0.2
const clamp = (value: number) => Math.min(Math.max(value, MIN_ZOOM_LEVEL), MAX_ZOOM_LEVEL)
const applyZoom = (next: number) => {
setWebviewZoom(next)
void window.api.setZoomFactor(next)
const apply = (next: number) => {
const clamped = clamp(next)
if (Math.abs(clamped - webviewZoom()) < 1e-6) return
setWebviewZoom(clamped)
void window.api.setZoomFactor(clamped).catch(() => undefined)
}
export const zoomIn = () => apply(webviewZoom() + KEY_STEP)
export const zoomOut = () => apply(webviewZoom() - KEY_STEP)
export const zoomReset = () => apply(1)
// Seed the signal from the main process so renderer and webContents agree
// across cold starts, reloads, and HMR refreshes (which would otherwise
// reinitialize the signal to 1 while webContents kept its prior factor).
void window.api
.getZoomFactor()
.then((initial) => {
if (typeof initial === "number" && Number.isFinite(initial)) {
setWebviewZoom(clamp(initial))
}
})
.catch(() => undefined)
// Keyboard accelerators. preventDefault stops Chromium's built-in zoom
// accelerators from firing in parallel (which previously caused races).
window.addEventListener("keydown", (event) => {
if (!(OS_NAME === "macos" ? event.metaKey : event.ctrlKey)) return
const mod = OS_NAME === "macos" ? event.metaKey : event.ctrlKey
if (!mod || event.altKey) return
let newZoom = webviewZoom()
if (event.key === "-") newZoom -= 0.2
if (event.key === "=" || event.key === "+") newZoom += 0.2
if (event.key === "0") newZoom = 1
applyZoom(clamp(newZoom))
if (event.key === "-" || event.key === "_") {
event.preventDefault()
zoomOut()
return
}
if (event.key === "=" || event.key === "+") {
event.preventDefault()
zoomIn()
return
}
if (event.key === "0") {
event.preventDefault()
zoomReset()
return
}
})
// Wheel zoom. Chromium synthesizes `wheel` with `ctrlKey: true` for trackpad
// pinch on every platform, so checking ctrlKey uniformly covers pinch-to-zoom
// as well as real ctrl+scroll / cmd+scroll.
window.addEventListener(
"wheel",
(event) => {
if (!event.ctrlKey && !event.metaKey) return
event.preventDefault()
const step = event.deltaY > 0 ? -WHEEL_STEP : WHEEL_STEP
apply(webviewZoom() + step)
},
{ passive: false },
)
export { webviewZoom }