From 2a726971296332e3e7ffe220a6733951b138df9b Mon Sep 17 00:00:00 2001 From: Tom Butcher Date: Mon, 3 Aug 2026 13:53:40 +0100 Subject: [PATCH] Refactor window management for macOS and Windows - Updated `createWindow` and `createMainWindow` functions to conditionally set title bar styles and traffic light positions based on the operating system. - Enhanced window layout synchronization by introducing `syncMaximizedWindowFrame` and improving the logic in `isWindowWorkAreaMaximized` to account for maximized window frames. - Added functions to calculate DWM border padding and adjusted frame handling to ensure proper window sizing and positioning across different platforms. --- public/mainWindow.js | 8 ++- src/desktop/window.js | 21 ++++++-- src/desktop/windows-work-area.js | 89 ++++++++++++++++++++++++++++++-- 3 files changed, 106 insertions(+), 12 deletions(-) diff --git a/public/mainWindow.js b/public/mainWindow.js index 82de743..d4c43ea 100644 --- a/public/mainWindow.js +++ b/public/mainWindow.js @@ -205,8 +205,12 @@ export function createWindow() { width: 1200, height: 800, frame: false, - titleBarStyle: 'hiddenInset', - trafficLightPosition: { x: 14, y: 12 }, + ...(process.platform === 'darwin' + ? { + titleBarStyle: 'hiddenInset', + trafficLightPosition: { x: 14, y: 12 } + } + : {}), backgroundColor: '#141414', icon: path.join(__dirname, './logo512.png'), webPreferences: { diff --git a/src/desktop/window.js b/src/desktop/window.js index bf75a10..da84f92 100644 --- a/src/desktop/window.js +++ b/src/desktop/window.js @@ -7,7 +7,8 @@ import { import { sendToRenderer, setMessageSender } from './notify.js' import { clampWindowToWorkArea, - isWindowWorkAreaMaximized + isWindowWorkAreaMaximized, + syncMaximizedWindowFrame } from './windows-work-area.js' const isMacOS = process.platform === 'darwin' @@ -158,6 +159,11 @@ function applyStartupWindowState(window) { } function syncWindowsWebviewLayout(window) { + if (isWindowWorkAreaMaximized(window)) { + syncMaximizedWindowFrame(window) + return + } + if (!window?.getSize || !window?.setSize) { return } @@ -172,7 +178,7 @@ function syncWindowsWebviewLayout(window) { } function handleWindowsWindowChange(window) { - if (clampWindowToWorkArea(window)) { + if (clampWindowToWorkArea(window) || isWindowWorkAreaMaximized(window)) { syncWindowsWebviewLayout(window) } @@ -246,10 +252,15 @@ export async function createMainWindow(rpc) { title: 'Farm Control', url, rpc, - titleBarStyle: 'hiddenInset', ...(isMacOS - ? { transparent: true, trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET } - : {}), + ? { + titleBarStyle: 'hiddenInset', + transparent: true, + trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET + } + : { + titleBarStyle: 'hidden' + }), frame: { width: 1200, height: 800, diff --git a/src/desktop/windows-work-area.js b/src/desktop/windows-work-area.js index 7645e81..1e9fbdb 100644 --- a/src/desktop/windows-work-area.js +++ b/src/desktop/windows-work-area.js @@ -1,6 +1,7 @@ import { Screen } from 'electrobun/bun' const FRAME_TOLERANCE_PX = 4 +const BASE_DWM_BORDER_PX = 7 function framesMatch(a, b, tolerance = FRAME_TOLERANCE_PX) { return ( @@ -34,6 +35,33 @@ function getDisplayForFrame(frame) { return match ?? Screen.getPrimaryDisplay() } +function getDwmBorderPadding(display) { + const scaleFactor = display?.scaleFactor ?? 1 + return Math.round(BASE_DWM_BORDER_PX * scaleFactor) +} + +function getMaximizedFrame(workArea, display) { + const borderPx = getDwmBorderPadding(display) + + // Windows keeps invisible resize borders on the left, right, and bottom when + // maximized. Extend the outer frame so the webview fills the visible area. + return { + x: workArea.x - borderPx, + y: workArea.y, + width: workArea.width + borderPx * 2, + height: workArea.height + borderPx + } +} + +function frameCoversWorkArea(frame, workArea) { + return ( + frame.x <= workArea.x + FRAME_TOLERANCE_PX && + frame.y <= workArea.y + FRAME_TOLERANCE_PX && + frame.x + frame.width >= workArea.x + workArea.width - FRAME_TOLERANCE_PX && + frame.y + frame.height >= workArea.y + workArea.height - FRAME_TOLERANCE_PX + ) +} + function frameCoversMonitor(frame, bounds) { return ( frame.x <= bounds.x + FRAME_TOLERANCE_PX && @@ -49,8 +77,14 @@ export function isWindowWorkAreaMaximized(window) { } const frame = window.getFrame() - const { workArea } = getDisplayForFrame(frame) - return framesMatch(frame, workArea) + const display = getDisplayForFrame(frame) + const maximizedFrame = getMaximizedFrame(display.workArea, display) + + return ( + framesMatch(frame, display.workArea) || + framesMatch(frame, maximizedFrame) || + frameCoversWorkArea(frame, display.workArea) + ) } export function clampWindowToWorkArea(window) { @@ -69,15 +103,60 @@ export function clampWindowToWorkArea(window) { const display = getDisplayForFrame(frame) const { bounds, workArea } = display + const maximizedFrame = getMaximizedFrame(workArea, display) - if (framesMatch(frame, workArea)) { + if (framesMatch(frame, maximizedFrame)) { return false } - if (!frameCoversMonitor(frame, bounds)) { + if ( + !frameCoversWorkArea(frame, workArea) && + !frameCoversMonitor(frame, bounds) + ) { return false } - window.setFrame(workArea.x, workArea.y, workArea.width, workArea.height) + window.setFrame( + maximizedFrame.x, + maximizedFrame.y, + maximizedFrame.width, + maximizedFrame.height + ) + return true +} + +export function syncMaximizedWindowFrame(window) { + if (!window?.getFrame || !window?.setFrame) { + return false + } + + if (window.isFullScreen?.()) { + return false + } + + const frame = window.getFrame() + if (!frame.width || !frame.height) { + return false + } + + const display = getDisplayForFrame(frame) + if ( + !frameCoversWorkArea(frame, display.workArea) && + !frameCoversMonitor(frame, display.bounds) + ) { + return false + } + + const maximizedFrame = getMaximizedFrame(display.workArea, display) + if (framesMatch(frame, maximizedFrame)) { + return false + } + + window.setFrame( + maximizedFrame.x, + maximizedFrame.y, + maximizedFrame.width, + maximizedFrame.height + ) return true }