@@ -4,7 +4,7 @@ import * as FS from "node:fs";
44import * as OS from "node:os" ;
55import * as Path from "node:path" ;
66
7- import { app , BrowserWindow , dialog , ipcMain , Menu , nativeImage , protocol , shell } from "electron" ;
7+ import { app , BrowserWindow , dialog , ipcMain , Menu , nativeImage , protocol , screen , shell } from "electron" ;
88import type { MenuItemConstructorOptions } from "electron" ;
99import * as Effect from "effect/Effect" ;
1010import type { DesktopUpdateActionResult , DesktopUpdateState } from "@t3tools/contracts" ;
@@ -60,6 +60,14 @@ const AUTO_UPDATE_STARTUP_DELAY_MS = 15_000;
6060const AUTO_UPDATE_POLL_INTERVAL_MS = 4 * 60 * 60 * 1000 ;
6161const DESKTOP_UPDATE_CHANNEL = "latest" ;
6262const DESKTOP_UPDATE_ALLOW_PRERELEASE = false ;
63+ const BASE_DEFAULT_WIDTH = 1100 ;
64+ const BASE_DEFAULT_HEIGHT = 780 ;
65+ const BASE_MIN_WIDTH = 840 ;
66+ const BASE_MIN_HEIGHT = 620 ;
67+ const REFERENCE_SHORT_EDGE = 1080 ;
68+ const ZOOM_STEP = 0.25 ;
69+ const MIN_ZOOM_FACTOR = 0.5 ;
70+ const MAX_ZOOM_FACTOR = 3.0 ;
6371
6472type DesktopUpdateErrorContext = DesktopUpdateState [ "errorContext" ] ;
6573
@@ -538,7 +546,41 @@ function configureApplicationMenu(): void {
538546 ] ,
539547 } ,
540548 { role : "editMenu" } ,
541- { role : "viewMenu" } ,
549+ {
550+ label : "View" ,
551+ submenu : [
552+ { role : "reload" } ,
553+ { role : "forceReload" } ,
554+ { role : "toggleDevTools" } ,
555+ { type : "separator" } ,
556+ {
557+ label : "Reset Zoom" ,
558+ accelerator : "CmdOrCtrl+0" ,
559+ click : ( ) => {
560+ const win = getFocusedBrowserWindow ( ) ;
561+ if ( win ) applyAutoZoom ( win ) ;
562+ } ,
563+ } ,
564+ {
565+ label : "Zoom In" ,
566+ accelerator : "CmdOrCtrl+=" ,
567+ click : ( ) => adjustZoom ( ZOOM_STEP ) ,
568+ } ,
569+ {
570+ label : "Zoom In" ,
571+ accelerator : "CmdOrCtrl+Plus" ,
572+ visible : false ,
573+ click : ( ) => adjustZoom ( ZOOM_STEP ) ,
574+ } ,
575+ {
576+ label : "Zoom Out" ,
577+ accelerator : "CmdOrCtrl+-" ,
578+ click : ( ) => adjustZoom ( - ZOOM_STEP ) ,
579+ } ,
580+ { type : "separator" } ,
581+ { role : "togglefullscreen" } ,
582+ ] ,
583+ } ,
542584 { role : "windowMenu" } ,
543585 {
544586 role : "help" ,
@@ -1094,12 +1136,68 @@ function getIconOption(): { icon: string } | Record<string, never> {
10941136 return iconPath ? { icon : iconPath } : { } ;
10951137}
10961138
1139+ function getFocusedBrowserWindow ( ) : BrowserWindow | null {
1140+ return BrowserWindow . getFocusedWindow ( ) ?? mainWindow ?? BrowserWindow . getAllWindows ( ) [ 0 ] ?? null ;
1141+ }
1142+
1143+ function adjustZoom ( delta : number ) : void {
1144+ const win = getFocusedBrowserWindow ( ) ;
1145+ if ( ! win || win . isDestroyed ( ) ) return ;
1146+ const current = win . webContents . getZoomFactor ( ) ;
1147+ win . webContents . setZoomFactor ( Math . min ( Math . max ( current + delta , MIN_ZOOM_FACTOR ) , MAX_ZOOM_FACTOR ) ) ;
1148+ }
1149+
1150+ function computeAutoZoomFactor ( display : Electron . Display ) : number {
1151+ const shortEdge = Math . min ( display . size . width , display . size . height ) ;
1152+ if ( shortEdge <= REFERENCE_SHORT_EDGE ) return 1.0 ;
1153+ const ratio = shortEdge / REFERENCE_SHORT_EDGE ;
1154+ return Math . min ( Math . round ( ratio / ZOOM_STEP ) * ZOOM_STEP , MAX_ZOOM_FACTOR ) ;
1155+ }
1156+
1157+ function applyAutoZoom ( window : BrowserWindow ) : void {
1158+ if ( window . isDestroyed ( ) ) return ;
1159+ if ( window . isFullScreen ( ) || window . isMaximized ( ) ) return ;
1160+
1161+ const display = screen . getDisplayMatching ( window . getBounds ( ) ) ;
1162+ const zoomFactor = computeAutoZoomFactor ( display ) ;
1163+ window . webContents . setZoomFactor ( zoomFactor ) ;
1164+
1165+ const workArea = display . workArea ;
1166+ window . setMinimumSize (
1167+ Math . min ( Math . round ( BASE_MIN_WIDTH * zoomFactor ) , workArea . width ) ,
1168+ Math . min ( Math . round ( BASE_MIN_HEIGHT * zoomFactor ) , workArea . height ) ,
1169+ ) ;
1170+
1171+ const bounds = window . getBounds ( ) ;
1172+ const clamped = { ...bounds } ;
1173+ if ( clamped . width > workArea . width ) clamped . width = workArea . width ;
1174+ if ( clamped . height > workArea . height ) clamped . height = workArea . height ;
1175+ if ( clamped . x < workArea . x ) clamped . x = workArea . x ;
1176+ if ( clamped . y < workArea . y ) clamped . y = workArea . y ;
1177+ if ( clamped . x + clamped . width > workArea . x + workArea . width )
1178+ clamped . x = workArea . x + workArea . width - clamped . width ;
1179+ if ( clamped . y + clamped . height > workArea . y + workArea . height )
1180+ clamped . y = workArea . y + workArea . height - clamped . height ;
1181+
1182+ if (
1183+ clamped . x !== bounds . x ||
1184+ clamped . y !== bounds . y ||
1185+ clamped . width !== bounds . width ||
1186+ clamped . height !== bounds . height
1187+ ) {
1188+ window . setBounds ( clamped ) ;
1189+ }
1190+ }
1191+
10971192function createWindow ( ) : BrowserWindow {
1193+ const targetDisplay = screen . getDisplayNearestPoint ( screen . getCursorScreenPoint ( ) ) ;
1194+ const zoomFactor = computeAutoZoomFactor ( targetDisplay ) ;
1195+
10981196 const window = new BrowserWindow ( {
1099- width : 1100 ,
1100- height : 780 ,
1101- minWidth : 840 ,
1102- minHeight : 620 ,
1197+ width : Math . round ( BASE_DEFAULT_WIDTH * zoomFactor ) ,
1198+ height : Math . round ( BASE_DEFAULT_HEIGHT * zoomFactor ) ,
1199+ minWidth : Math . round ( BASE_MIN_WIDTH * zoomFactor ) ,
1200+ minHeight : Math . round ( BASE_MIN_HEIGHT * zoomFactor ) ,
11031201 show : false ,
11041202 autoHideMenuBar : true ,
11051203 ...getIconOption ( ) ,
@@ -1121,9 +1219,62 @@ function createWindow(): BrowserWindow {
11211219 } ) ;
11221220 window . webContents . on ( "did-finish-load" , ( ) => {
11231221 window . setTitle ( APP_DISPLAY_NAME ) ;
1222+ applyAutoZoom ( window ) ;
11241223 emitUpdateState ( ) ;
11251224 } ) ;
1225+
1226+ let lastDisplayId = screen . getDisplayMatching ( window . getBounds ( ) ) . id ;
1227+ let applyingZoom = false ;
1228+ window . on ( "move" , ( ) => {
1229+ if ( window . isDestroyed ( ) || applyingZoom ) return ;
1230+ const currentDisplay = screen . getDisplayMatching ( window . getBounds ( ) ) ;
1231+ if ( currentDisplay . id !== lastDisplayId ) {
1232+ lastDisplayId = currentDisplay . id ;
1233+ applyingZoom = true ;
1234+ try {
1235+ applyAutoZoom ( window ) ;
1236+ } finally {
1237+ applyingZoom = false ;
1238+ }
1239+ }
1240+ } ) ;
1241+
1242+ window . on ( "unmaximize" , ( ) => {
1243+ if ( window . isDestroyed ( ) || applyingZoom ) return ;
1244+ applyingZoom = true ;
1245+ try {
1246+ applyAutoZoom ( window ) ;
1247+ lastDisplayId = screen . getDisplayMatching ( window . getBounds ( ) ) . id ;
1248+ } finally {
1249+ applyingZoom = false ;
1250+ }
1251+ } ) ;
1252+ window . on ( "leave-full-screen" , ( ) => {
1253+ if ( window . isDestroyed ( ) || applyingZoom ) return ;
1254+ applyingZoom = true ;
1255+ try {
1256+ applyAutoZoom ( window ) ;
1257+ lastDisplayId = screen . getDisplayMatching ( window . getBounds ( ) ) . id ;
1258+ } finally {
1259+ applyingZoom = false ;
1260+ }
1261+ } ) ;
1262+ window . on ( "resize" , ( ) => {
1263+ if ( window . isDestroyed ( ) || applyingZoom ) return ;
1264+ const currentDisplay = screen . getDisplayMatching ( window . getBounds ( ) ) ;
1265+ if ( currentDisplay . id !== lastDisplayId ) {
1266+ lastDisplayId = currentDisplay . id ;
1267+ applyingZoom = true ;
1268+ try {
1269+ applyAutoZoom ( window ) ;
1270+ } finally {
1271+ applyingZoom = false ;
1272+ }
1273+ }
1274+ } ) ;
1275+
11261276 window . once ( "ready-to-show" , ( ) => {
1277+ applyAutoZoom ( window ) ;
11271278 window . show ( ) ;
11281279 } ) ;
11291280
@@ -1182,6 +1333,12 @@ app
11821333 configureApplicationMenu ( ) ;
11831334 registerDesktopProtocol ( ) ;
11841335 configureAutoUpdater ( ) ;
1336+
1337+ screen . on ( "display-metrics-changed" , ( ) => {
1338+ for ( const win of BrowserWindow . getAllWindows ( ) ) {
1339+ applyAutoZoom ( win ) ;
1340+ }
1341+ } ) ;
11851342 void bootstrap ( ) . catch ( ( error ) => {
11861343 handleFatalStartupError ( "bootstrap" , error ) ;
11871344 } ) ;
0 commit comments