Refactor window management for macOS and Windows
All checks were successful
farmcontrol/farmcontrol-ui/pipeline/head This commit looks good

- 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.
This commit is contained in:
Tom Butcher 2026-08-03 13:53:40 +01:00
parent cfb8c6ede1
commit 2a72697129
3 changed files with 106 additions and 12 deletions

View File

@ -205,8 +205,12 @@ export function createWindow() {
width: 1200, width: 1200,
height: 800, height: 800,
frame: false, frame: false,
titleBarStyle: 'hiddenInset', ...(process.platform === 'darwin'
trafficLightPosition: { x: 14, y: 12 }, ? {
titleBarStyle: 'hiddenInset',
trafficLightPosition: { x: 14, y: 12 }
}
: {}),
backgroundColor: '#141414', backgroundColor: '#141414',
icon: path.join(__dirname, './logo512.png'), icon: path.join(__dirname, './logo512.png'),
webPreferences: { webPreferences: {

View File

@ -7,7 +7,8 @@ import {
import { sendToRenderer, setMessageSender } from './notify.js' import { sendToRenderer, setMessageSender } from './notify.js'
import { import {
clampWindowToWorkArea, clampWindowToWorkArea,
isWindowWorkAreaMaximized isWindowWorkAreaMaximized,
syncMaximizedWindowFrame
} from './windows-work-area.js' } from './windows-work-area.js'
const isMacOS = process.platform === 'darwin' const isMacOS = process.platform === 'darwin'
@ -158,6 +159,11 @@ function applyStartupWindowState(window) {
} }
function syncWindowsWebviewLayout(window) { function syncWindowsWebviewLayout(window) {
if (isWindowWorkAreaMaximized(window)) {
syncMaximizedWindowFrame(window)
return
}
if (!window?.getSize || !window?.setSize) { if (!window?.getSize || !window?.setSize) {
return return
} }
@ -172,7 +178,7 @@ function syncWindowsWebviewLayout(window) {
} }
function handleWindowsWindowChange(window) { function handleWindowsWindowChange(window) {
if (clampWindowToWorkArea(window)) { if (clampWindowToWorkArea(window) || isWindowWorkAreaMaximized(window)) {
syncWindowsWebviewLayout(window) syncWindowsWebviewLayout(window)
} }
@ -246,10 +252,15 @@ export async function createMainWindow(rpc) {
title: 'Farm Control', title: 'Farm Control',
url, url,
rpc, rpc,
titleBarStyle: 'hiddenInset',
...(isMacOS ...(isMacOS
? { transparent: true, trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET } ? {
: {}), titleBarStyle: 'hiddenInset',
transparent: true,
trafficLightOffset: MAC_TRAFFIC_LIGHT_OFFSET
}
: {
titleBarStyle: 'hidden'
}),
frame: { frame: {
width: 1200, width: 1200,
height: 800, height: 800,

View File

@ -1,6 +1,7 @@
import { Screen } from 'electrobun/bun' import { Screen } from 'electrobun/bun'
const FRAME_TOLERANCE_PX = 4 const FRAME_TOLERANCE_PX = 4
const BASE_DWM_BORDER_PX = 7
function framesMatch(a, b, tolerance = FRAME_TOLERANCE_PX) { function framesMatch(a, b, tolerance = FRAME_TOLERANCE_PX) {
return ( return (
@ -34,6 +35,33 @@ function getDisplayForFrame(frame) {
return match ?? Screen.getPrimaryDisplay() 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) { function frameCoversMonitor(frame, bounds) {
return ( return (
frame.x <= bounds.x + FRAME_TOLERANCE_PX && frame.x <= bounds.x + FRAME_TOLERANCE_PX &&
@ -49,8 +77,14 @@ export function isWindowWorkAreaMaximized(window) {
} }
const frame = window.getFrame() const frame = window.getFrame()
const { workArea } = getDisplayForFrame(frame) const display = getDisplayForFrame(frame)
return framesMatch(frame, workArea) const maximizedFrame = getMaximizedFrame(display.workArea, display)
return (
framesMatch(frame, display.workArea) ||
framesMatch(frame, maximizedFrame) ||
frameCoversWorkArea(frame, display.workArea)
)
} }
export function clampWindowToWorkArea(window) { export function clampWindowToWorkArea(window) {
@ -69,15 +103,60 @@ export function clampWindowToWorkArea(window) {
const display = getDisplayForFrame(frame) const display = getDisplayForFrame(frame)
const { bounds, workArea } = display const { bounds, workArea } = display
const maximizedFrame = getMaximizedFrame(workArea, display)
if (framesMatch(frame, workArea)) { if (framesMatch(frame, maximizedFrame)) {
return false return false
} }
if (!frameCoversMonitor(frame, bounds)) { if (
!frameCoversWorkArea(frame, workArea) &&
!frameCoversMonitor(frame, bounds)
) {
return false 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 return true
} }