From 04d00ce50dcddca3839af465a0a2b204436dcf4d Mon Sep 17 00:00:00 2001 From: supercombogamer Date: Sat, 7 Mar 2026 12:16:21 -0500 Subject: [PATCH 1/2] feat(desktop): add auto-zoom scaling for high-DPI displays Auto-detect display resolution and scale the UI proportionally so the app is usable on 4K/1440p monitors without affecting 1080p users. - Compute zoom factor from the display's DIP short edge relative to 1080p, snapped to 0.25 increments (max 3.0x) - Apply zoom on window creation, display change, move/resize across monitors, unmaximize, and leave-fullscreen - Clamp window bounds to the display work area after zoom changes - Scale minimum window size with zoom, capped to the work area - Replace built-in viewMenu with custom View menu supporting Ctrl+=/Ctrl+Plus (zoom in), Ctrl+- (zoom out), Ctrl+0 (reset) - Guard against re-entrant setBounds->move loops and skip zoom adjustments while fullscreen or maximized --- apps/desktop/src/main.ts | 169 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 163 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 945f1d279..afa91c9c2 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -4,7 +4,7 @@ import * as FS from "node:fs"; import * as OS from "node:os"; import * as Path from "node:path"; -import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, protocol, shell } from "electron"; +import { app, BrowserWindow, dialog, ipcMain, Menu, nativeImage, protocol, screen, shell } from "electron"; import type { MenuItemConstructorOptions } from "electron"; import * as Effect from "effect/Effect"; import type { DesktopUpdateActionResult, DesktopUpdateState } from "@t3tools/contracts"; @@ -60,6 +60,14 @@ const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000; const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000; const DESKTOP_UPDATE_CHANNEL = "latest"; const DESKTOP_UPDATE_ALLOW_PRERELEASE = false; +const BASE_DEFAULT_WIDTH = 1100; +const BASE_DEFAULT_HEIGHT = 780; +const BASE_MIN_WIDTH = 840; +const BASE_MIN_HEIGHT = 620; +const REFERENCE_SHORT_EDGE = 1080; +const ZOOM_STEP = 0.25; +const MIN_ZOOM_FACTOR = 0.5; +const MAX_ZOOM_FACTOR = 3.0; type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"]; @@ -538,7 +546,41 @@ function configureApplicationMenu(): void { ], }, { role: "editMenu" }, - { role: "viewMenu" }, + { + label: "View", + submenu: [ + { role: "reload" }, + { role: "forceReload" }, + { role: "toggleDevTools" }, + { type: "separator" }, + { + label: "Reset Zoom", + accelerator: "CmdOrCtrl+0", + click: () => { + const win = getFocusedBrowserWindow(); + if (win) applyAutoZoom(win); + }, + }, + { + label: "Zoom In", + accelerator: "CmdOrCtrl+=", + click: () => adjustZoom(ZOOM_STEP), + }, + { + label: "Zoom In", + accelerator: "CmdOrCtrl+Plus", + visible: false, + click: () => adjustZoom(ZOOM_STEP), + }, + { + label: "Zoom Out", + accelerator: "CmdOrCtrl+-", + click: () => adjustZoom(-ZOOM_STEP), + }, + { type: "separator" }, + { role: "togglefullscreen" }, + ], + }, { role: "windowMenu" }, { role: "help", @@ -1094,12 +1136,68 @@ function getIconOption(): { icon: string } | Record { return iconPath ? { icon: iconPath } : {}; } +function getFocusedBrowserWindow(): BrowserWindow | null { + return BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0] ?? null; +} + +function adjustZoom(delta: number): void { + const win = getFocusedBrowserWindow(); + if (!win || win.isDestroyed()) return; + const current = win.webContents.getZoomFactor(); + win.webContents.setZoomFactor(Math.min(Math.max(current + delta, MIN_ZOOM_FACTOR), MAX_ZOOM_FACTOR)); +} + +function computeAutoZoomFactor(display: Electron.Display): number { + const shortEdge = Math.min(display.size.width, display.size.height); + if (shortEdge <= REFERENCE_SHORT_EDGE) return 1.0; + const ratio = shortEdge / REFERENCE_SHORT_EDGE; + return Math.min(Math.round(ratio / ZOOM_STEP) * ZOOM_STEP, MAX_ZOOM_FACTOR); +} + +function applyAutoZoom(window: BrowserWindow): void { + if (window.isDestroyed()) return; + if (window.isFullScreen() || window.isMaximized()) return; + + const display = screen.getDisplayMatching(window.getBounds()); + const zoomFactor = computeAutoZoomFactor(display); + window.webContents.setZoomFactor(zoomFactor); + + const workArea = display.workArea; + window.setMinimumSize( + Math.min(Math.round(BASE_MIN_WIDTH * zoomFactor), workArea.width), + Math.min(Math.round(BASE_MIN_HEIGHT * zoomFactor), workArea.height), + ); + + const bounds = window.getBounds(); + const clamped = { ...bounds }; + if (clamped.width > workArea.width) clamped.width = workArea.width; + if (clamped.height > workArea.height) clamped.height = workArea.height; + if (clamped.x < workArea.x) clamped.x = workArea.x; + if (clamped.y < workArea.y) clamped.y = workArea.y; + if (clamped.x + clamped.width > workArea.x + workArea.width) + clamped.x = workArea.x + workArea.width - clamped.width; + if (clamped.y + clamped.height > workArea.y + workArea.height) + clamped.y = workArea.y + workArea.height - clamped.height; + + if ( + clamped.x !== bounds.x || + clamped.y !== bounds.y || + clamped.width !== bounds.width || + clamped.height !== bounds.height + ) { + window.setBounds(clamped); + } +} + function createWindow(): BrowserWindow { + const targetDisplay = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()); + const zoomFactor = computeAutoZoomFactor(targetDisplay); + const window = new BrowserWindow({ - width: 1100, - height: 780, - minWidth: 840, - minHeight: 620, + width: Math.round(BASE_DEFAULT_WIDTH * zoomFactor), + height: Math.round(BASE_DEFAULT_HEIGHT * zoomFactor), + minWidth: Math.round(BASE_MIN_WIDTH * zoomFactor), + minHeight: Math.round(BASE_MIN_HEIGHT * zoomFactor), show: false, autoHideMenuBar: true, ...getIconOption(), @@ -1121,9 +1219,62 @@ function createWindow(): BrowserWindow { }); window.webContents.on("did-finish-load", () => { window.setTitle(APP_DISPLAY_NAME); + applyAutoZoom(window); emitUpdateState(); }); + + let lastDisplayId = screen.getDisplayMatching(window.getBounds()).id; + let applyingZoom = false; + window.on("move", () => { + if (window.isDestroyed() || applyingZoom) return; + const currentDisplay = screen.getDisplayMatching(window.getBounds()); + if (currentDisplay.id !== lastDisplayId) { + lastDisplayId = currentDisplay.id; + applyingZoom = true; + try { + applyAutoZoom(window); + } finally { + applyingZoom = false; + } + } + }); + + window.on("unmaximize", () => { + if (window.isDestroyed() || applyingZoom) return; + applyingZoom = true; + try { + applyAutoZoom(window); + lastDisplayId = screen.getDisplayMatching(window.getBounds()).id; + } finally { + applyingZoom = false; + } + }); + window.on("leave-full-screen", () => { + if (window.isDestroyed() || applyingZoom) return; + applyingZoom = true; + try { + applyAutoZoom(window); + lastDisplayId = screen.getDisplayMatching(window.getBounds()).id; + } finally { + applyingZoom = false; + } + }); + window.on("resize", () => { + if (window.isDestroyed() || applyingZoom) return; + const currentDisplay = screen.getDisplayMatching(window.getBounds()); + if (currentDisplay.id !== lastDisplayId) { + lastDisplayId = currentDisplay.id; + applyingZoom = true; + try { + applyAutoZoom(window); + } finally { + applyingZoom = false; + } + } + }); + window.once("ready-to-show", () => { + applyAutoZoom(window); window.show(); }); @@ -1182,6 +1333,12 @@ app configureApplicationMenu(); registerDesktopProtocol(); configureAutoUpdater(); + + screen.on("display-metrics-changed", () => { + for (const win of BrowserWindow.getAllWindows()) { + applyAutoZoom(win); + } + }); void bootstrap().catch((error) => { handleFatalStartupError("bootstrap", error); }); From aaa51de5e724c73af648fd47f71bb284484309c6 Mon Sep 17 00:00:00 2001 From: supercombogamer Date: Sun, 8 Mar 2026 23:25:04 -0400 Subject: [PATCH 2/2] feat(desktop): persist zoom as delta, skip auto-zoom on macOS, harden zoom logic - Store user zoom preference as a delta from auto-computed zoom so it transfers correctly across monitors with different DPIs - Skip auto-zoom on macOS (Retina handles DPI natively) - Clamp delta in adjustZoom to prevent unbounded accumulation - Add NaN guard on zoom delta read/write - Debounce display-metrics-changed handler - Move reentrancy guard into applyAutoZoom (global Set, protects all call sites) - Reset Zoom now applies to all windows - Set zoom factor even when maximized/fullscreen - Extract computeEffectiveZoom helper --- apps/desktop/src/main.ts | 207 +++++++++++++++++++++++---------------- 1 file changed, 123 insertions(+), 84 deletions(-) diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index afa91c9c2..fa6275daa 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -68,6 +68,40 @@ const REFERENCE_SHORT_EDGE = 1080; const ZOOM_STEP = 0.25; const MIN_ZOOM_FACTOR = 0.5; const MAX_ZOOM_FACTOR = 3.0; +const ZOOM_PREFS_FILE = Path.join(STATE_DIR, "zoom-preferences.json"); + +// Stored as a delta from auto-computed zoom so it transfers correctly across monitors with different DPIs. +function loadZoomDelta(): number | null { + try { + const data = JSON.parse(FS.readFileSync(ZOOM_PREFS_FILE, "utf-8")); + if (typeof data.zoomDelta === "number" && Number.isFinite(data.zoomDelta)) { + return data.zoomDelta; + } + } catch { + // missing or invalid file + } + return null; +} + +let cachedZoomDelta = loadZoomDelta(); + +function writeZoomDelta(zoomDelta: number): void { + if (!Number.isFinite(zoomDelta)) return; + cachedZoomDelta = zoomDelta; + try { + FS.mkdirSync(Path.dirname(ZOOM_PREFS_FILE), { recursive: true }); + FS.writeFileSync(ZOOM_PREFS_FILE, JSON.stringify({ zoomDelta }), "utf-8"); + } catch { + // best-effort + } +} + +function clearZoomDelta(): void { + cachedZoomDelta = null; + try { + FS.unlinkSync(ZOOM_PREFS_FILE); + } catch {} +} type DesktopUpdateErrorContext = DesktopUpdateState["errorContext"]; @@ -557,8 +591,10 @@ function configureApplicationMenu(): void { label: "Reset Zoom", accelerator: "CmdOrCtrl+0", click: () => { - const win = getFocusedBrowserWindow(); - if (win) applyAutoZoom(win); + clearZoomDelta(); + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) applyAutoZoom(win); + } }, }, { @@ -1140,58 +1176,81 @@ function getFocusedBrowserWindow(): BrowserWindow | null { return BrowserWindow.getFocusedWindow() ?? mainWindow ?? BrowserWindow.getAllWindows()[0] ?? null; } -function adjustZoom(delta: number): void { - const win = getFocusedBrowserWindow(); - if (!win || win.isDestroyed()) return; - const current = win.webContents.getZoomFactor(); - win.webContents.setZoomFactor(Math.min(Math.max(current + delta, MIN_ZOOM_FACTOR), MAX_ZOOM_FACTOR)); -} - function computeAutoZoomFactor(display: Electron.Display): number { + if (process.platform === "darwin") return 1.0; const shortEdge = Math.min(display.size.width, display.size.height); if (shortEdge <= REFERENCE_SHORT_EDGE) return 1.0; const ratio = shortEdge / REFERENCE_SHORT_EDGE; return Math.min(Math.round(ratio / ZOOM_STEP) * ZOOM_STEP, MAX_ZOOM_FACTOR); } +function computeEffectiveZoom(display: Electron.Display): number { + const autoZoom = computeAutoZoomFactor(display); + const delta = cachedZoomDelta ?? 0; + return Math.min(Math.max(autoZoom + delta, MIN_ZOOM_FACTOR), MAX_ZOOM_FACTOR); +} + +function adjustZoom(delta: number): void { + const win = getFocusedBrowserWindow(); + if (!win || win.isDestroyed()) return; + const display = screen.getDisplayMatching(win.getBounds()); + const autoZoom = computeAutoZoomFactor(display); + const currentDelta = cachedZoomDelta ?? 0; + const newDelta = currentDelta + delta; + const clampedDelta = Math.min(Math.max(newDelta, MIN_ZOOM_FACTOR - autoZoom), MAX_ZOOM_FACTOR - autoZoom); + if (clampedDelta === currentDelta) return; + writeZoomDelta(clampedDelta); + applyAutoZoom(win); +} + +// Reentrancy guard — setBounds/setMinimumSize can synchronously trigger move/resize events. +const applyingZoomWindows = new Set(); + function applyAutoZoom(window: BrowserWindow): void { if (window.isDestroyed()) return; - if (window.isFullScreen() || window.isMaximized()) return; + if (applyingZoomWindows.has(window.id)) return; + applyingZoomWindows.add(window.id); + try { + const display = screen.getDisplayMatching(window.getBounds()); + const zoomFactor = computeEffectiveZoom(display); + window.webContents.setZoomFactor(zoomFactor); - const display = screen.getDisplayMatching(window.getBounds()); - const zoomFactor = computeAutoZoomFactor(display); - window.webContents.setZoomFactor(zoomFactor); + // WM owns the geometry when maximized/fullscreen. + if (window.isFullScreen() || window.isMaximized()) return; - const workArea = display.workArea; - window.setMinimumSize( - Math.min(Math.round(BASE_MIN_WIDTH * zoomFactor), workArea.width), - Math.min(Math.round(BASE_MIN_HEIGHT * zoomFactor), workArea.height), - ); + const workArea = display.workArea; + window.setMinimumSize( + Math.min(Math.round(BASE_MIN_WIDTH * zoomFactor), workArea.width), + Math.min(Math.round(BASE_MIN_HEIGHT * zoomFactor), workArea.height), + ); + + const bounds = window.getBounds(); + const clamped = { ...bounds }; + if (clamped.width > workArea.width) clamped.width = workArea.width; + if (clamped.height > workArea.height) clamped.height = workArea.height; + if (clamped.x < workArea.x) clamped.x = workArea.x; + if (clamped.y < workArea.y) clamped.y = workArea.y; + if (clamped.x + clamped.width > workArea.x + workArea.width) + clamped.x = workArea.x + workArea.width - clamped.width; + if (clamped.y + clamped.height > workArea.y + workArea.height) + clamped.y = workArea.y + workArea.height - clamped.height; - const bounds = window.getBounds(); - const clamped = { ...bounds }; - if (clamped.width > workArea.width) clamped.width = workArea.width; - if (clamped.height > workArea.height) clamped.height = workArea.height; - if (clamped.x < workArea.x) clamped.x = workArea.x; - if (clamped.y < workArea.y) clamped.y = workArea.y; - if (clamped.x + clamped.width > workArea.x + workArea.width) - clamped.x = workArea.x + workArea.width - clamped.width; - if (clamped.y + clamped.height > workArea.y + workArea.height) - clamped.y = workArea.y + workArea.height - clamped.height; - - if ( - clamped.x !== bounds.x || - clamped.y !== bounds.y || - clamped.width !== bounds.width || - clamped.height !== bounds.height - ) { - window.setBounds(clamped); + if ( + clamped.x !== bounds.x || + clamped.y !== bounds.y || + clamped.width !== bounds.width || + clamped.height !== bounds.height + ) { + window.setBounds(clamped); + } + } finally { + applyingZoomWindows.delete(window.id); } } function createWindow(): BrowserWindow { const targetDisplay = screen.getDisplayNearestPoint(screen.getCursorScreenPoint()); - const zoomFactor = computeAutoZoomFactor(targetDisplay); + const zoomFactor = computeEffectiveZoom(targetDisplay); const window = new BrowserWindow({ width: Math.round(BASE_DEFAULT_WIDTH * zoomFactor), @@ -1219,59 +1278,30 @@ function createWindow(): BrowserWindow { }); window.webContents.on("did-finish-load", () => { window.setTitle(APP_DISPLAY_NAME); - applyAutoZoom(window); emitUpdateState(); }); let lastDisplayId = screen.getDisplayMatching(window.getBounds()).id; - let applyingZoom = false; - window.on("move", () => { - if (window.isDestroyed() || applyingZoom) return; - const currentDisplay = screen.getDisplayMatching(window.getBounds()); - if (currentDisplay.id !== lastDisplayId) { - lastDisplayId = currentDisplay.id; - applyingZoom = true; - try { - applyAutoZoom(window); - } finally { - applyingZoom = false; - } - } - }); - window.on("unmaximize", () => { - if (window.isDestroyed() || applyingZoom) return; - applyingZoom = true; - try { - applyAutoZoom(window); - lastDisplayId = screen.getDisplayMatching(window.getBounds()).id; - } finally { - applyingZoom = false; - } - }); - window.on("leave-full-screen", () => { - if (window.isDestroyed() || applyingZoom) return; - applyingZoom = true; - try { - applyAutoZoom(window); - lastDisplayId = screen.getDisplayMatching(window.getBounds()).id; - } finally { - applyingZoom = false; - } - }); - window.on("resize", () => { - if (window.isDestroyed() || applyingZoom) return; + const onDisplayChange = () => { + if (window.isDestroyed()) return; const currentDisplay = screen.getDisplayMatching(window.getBounds()); if (currentDisplay.id !== lastDisplayId) { lastDisplayId = currentDisplay.id; - applyingZoom = true; - try { - applyAutoZoom(window); - } finally { - applyingZoom = false; - } + applyAutoZoom(window); } - }); + }; + + const onExitFullState = () => { + if (window.isDestroyed()) return; + applyAutoZoom(window); + lastDisplayId = screen.getDisplayMatching(window.getBounds()).id; + }; + + window.on("move", onDisplayChange); + window.on("resize", onDisplayChange); + window.on("unmaximize", onExitFullState); + window.on("leave-full-screen", onExitFullState); window.once("ready-to-show", () => { applyAutoZoom(window); @@ -1334,10 +1364,19 @@ app registerDesktopProtocol(); configureAutoUpdater(); + let displayMetricsTimer: ReturnType | null = null; screen.on("display-metrics-changed", () => { - for (const win of BrowserWindow.getAllWindows()) { - applyAutoZoom(win); - } + if (displayMetricsTimer) clearTimeout(displayMetricsTimer); + displayMetricsTimer = setTimeout(() => { + // On Windows/Linux, clear user override so zoom recomputes for the new display config. + // On macOS, preserve the user's delta since the OS handles DPI scaling natively. + if (process.platform !== "darwin") { + clearZoomDelta(); + } + for (const win of BrowserWindow.getAllWindows()) { + if (!win.isDestroyed()) applyAutoZoom(win); + } + }, 300); }); void bootstrap().catch((error) => { handleFatalStartupError("bootstrap", error);