From 62defbe1b5688d0ac9b611030a83d24f5982c4a8 Mon Sep 17 00:00:00 2001 From: Ethan Anderson <150651098+mite404@users.noreply.github.com> Date: Thu, 19 Mar 2026 00:19:45 -0400 Subject: [PATCH] fix: UI scaling and content cropping MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Intercepts `Cmd+=`/`Cmd+-`/`Cmd+0` via `before-input-event` before Chromium sees them, so the main process fully owns zoom. Using `zoom-changed` was unreliable — Chromium applies its own preset step on top, causing the actual zoom factor to exceed what the window was sized for and clipping content. The top of the chat window and the left btns were cropped at higher zoom levels. This was particularly evident with 'Full width' toggled on. **Changes:** - Added `applyZoom(zoom)` — resizes and repositions the window proportionally to the zoom factor, clamping the left edge to the screen boundary to prevent macOS from clipping panel windows that start off-screen - Updated `showWindow()` to read the current zoom factor and apply it when repositioning, so the window doesn't snap back to 1× on summon - Bumped `BAR_WIDTH` from 1040 → 1100 to give the renderer more horizontal breathing room at default zoom - Added `.zed` to `.gitignore so I could turn off 'format on save' and 'remove trailing whitespace' for this project else too many lines were changed --- .gitignore | 1 + src/main/index.ts | 62 +++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 683f5303..c4ad4c30 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ Desktop.ini *.swp *.swo *~ +.zed # Claude Code project-scoped local settings .claude/settings.local.json diff --git a/src/main/index.ts b/src/main/index.ts index 10472226..2556d8da 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -30,7 +30,7 @@ const controlPlane = new ControlPlane(INTERACTIVE_PTY) // Keep native width fixed to avoid renderer animation vs setBounds race. // The UI itself still launches in compact mode; extra width is transparent/click-through. -const BAR_WIDTH = 1040 +const BAR_WIDTH = 1100 const PILL_HEIGHT = 720 // Fixed native window height — extra room for expanded UI + shadow buffers const PILL_BOTTOM_MARGIN = 24 @@ -141,6 +141,33 @@ function createWindow(): void { } }) + // Intercept zoom shortcuts before Chromium sees them so we fully own the zoom + // factor and window size. zoom-changed is not used because event.preventDefault() + // on it is unreliable — Chromium may still apply its own preset step (1.0→1.25 + // etc.) on top of ours, making the actual zoom factor exceed what the window was + // sized for and shrinking the CSS viewport below BAR_WIDTH, which clips content. + mainWindow.webContents.on('before-input-event', (event, input) => { + if (!input.meta) return + if (input.key === '=' || input.key === '+') { + // zoom in + event.preventDefault() + const next = Math.min(+(( mainWindow?.webContents.getZoomFactor() ?? 1) + 0.1).toFixed(1), 3.0) + mainWindow?.webContents.setZoomFactor(next) + applyZoom(next) + } else if (input.key === '-') { + // zoom out + event.preventDefault() + const next = Math.max(+((mainWindow?.webContents.getZoomFactor() ?? 1) - 0.1).toFixed(1), 0.5) + mainWindow?.webContents.setZoomFactor(next) + applyZoom(next) + } else if (input.key === '0') { + // reset zoom + event.preventDefault() + mainWindow?.webContents.setZoomFactor(1) + applyZoom(1) + } + }) + let forceQuit = false app.on('before-quit', () => { forceQuit = true }) mainWindow.on('close', (e) => { @@ -157,6 +184,27 @@ function createWindow(): void { } } +/** Resize and reposition the window for a given zoom factor. */ +function applyZoom(zoom: number): void { + if (!mainWindow || mainWindow.isDestroyed()) return + const bounds = mainWindow.getBounds() + const display = screen.getDisplayMatching(bounds) + const { x: dx, y: dy } = display.workArea + const { width: sw, height: sh } = display.workAreaSize + const w = Math.round(BAR_WIDTH * zoom) + const h = Math.round(PILL_HEIGHT * zoom) + // Clamp left edge to screen boundary — at high zoom the window can be wider + // than the screen, and macOS clips panel windows that start off-screen left. + // Overflow on the right is fine since that area is transparent. + const x = Math.max(dx, dx + Math.round((sw - w) / 2)) + mainWindow.setBounds({ + x, + y: dy + sh - h - PILL_BOTTOM_MARGIN, + width: w, + height: h, + }) +} + function showWindow(source = 'unknown'): void { if (!mainWindow) return const toggleId = ++toggleSequence @@ -166,11 +214,15 @@ function showWindow(source = 'unknown'): void { const display = screen.getDisplayNearestPoint(cursor) const { width: sw, height: sh } = display.workAreaSize const { x: dx, y: dy } = display.workArea + // Preserve the user's zoom level when repositioning + const zoom = mainWindow.webContents.getZoomFactor() + const w = Math.round(BAR_WIDTH * zoom) + const h = Math.round(PILL_HEIGHT * zoom) mainWindow.setBounds({ - x: dx + Math.round((sw - BAR_WIDTH) / 2), - y: dy + sh - PILL_HEIGHT - PILL_BOTTOM_MARGIN, - width: BAR_WIDTH, - height: PILL_HEIGHT, + x: dx + Math.round((sw - w) / 2), + y: dy + sh - h - PILL_BOTTOM_MARGIN, + width: w, + height: h, }) // Always re-assert space membership — the flag can be lost after hide/show cycles