diff --git a/src/components/Dashboard/common/DashboardNavigation.jsx b/src/components/Dashboard/common/DashboardNavigation.jsx index eb3890a..2dc40cb 100644 --- a/src/components/Dashboard/common/DashboardNavigation.jsx +++ b/src/components/Dashboard/common/DashboardNavigation.jsx @@ -64,7 +64,7 @@ const DashboardNavigation = () => { icon: }) const isMobile = useMediaQuery({ maxWidth: 768 }) - const { platform, isElectron, setSidebarViewMenu } = + const { platform, isElectron, setSidebarViewMenu, isFullscreen } = useContext(ElectronContext) const { availableUpdate, checkForUpdates } = useAppUpdateContext() const mainMenuItems = useMemo( @@ -349,7 +349,7 @@ const DashboardNavigation = () => { <> {isElectron ? ( {navigationContents} diff --git a/src/desktop/window.js b/src/desktop/window.js index 57ca70a..cd05d52 100644 --- a/src/desktop/window.js +++ b/src/desktop/window.js @@ -184,6 +184,8 @@ function applyWindowsStartupWindowState(window) { setTimeout(() => { window.maximize?.() + // maximize() settles asynchronously; sync state after the frame updates. + setTimeout(() => handleWindowsWindowChange(window), 100) }, WINDOWS_STARTUP_MAXIMIZE_DELAY_MS) } @@ -339,17 +341,23 @@ export function handleWindowControl(action) { case 'minimize': mainWindow.minimize?.() break - case 'maximize': - if (mainWindow.isMaximized?.()) { + case 'maximize': { + const currentlyMaximized = isWindows + ? isWindowWorkAreaMaximized(mainWindow) + : (mainWindow.isMaximized?.() ?? false) + + if (currentlyMaximized) { mainWindow.unmaximize?.() } else { mainWindow.maximize?.() } if (isWindows) { - handleWindowsWindowChange(mainWindow) + // Frame updates after maximize/unmaximize are async on Windows. + setTimeout(() => handleWindowsWindowChange(mainWindow), 100) } break + } case 'fullscreen': if (mainWindow.isFullScreen?.()) { mainWindow.setFullScreen?.(false) diff --git a/src/desktop/windows-work-area.js b/src/desktop/windows-work-area.js index ce49de7..141c5c3 100644 --- a/src/desktop/windows-work-area.js +++ b/src/desktop/windows-work-area.js @@ -1,6 +1,15 @@ import { Screen } from 'electrobun/bun' const FRAME_TOLERANCE_PX = 4 +// Win32 DWM invisible borders sit outside the client work area; Electrobun's +// maximized frame includes those overhangs, so we treat this adjusted rect as +// the canonical "maximized to work area" geometry. +const WIN32_FRAME_OVERHANG = { + x: -8, + y: 0, + width: 16, + height: 8 +} function framesMatch(a, b, tolerance = FRAME_TOLERANCE_PX) { return ( @@ -11,6 +20,15 @@ function framesMatch(a, b, tolerance = FRAME_TOLERANCE_PX) { ) } +function getMaximizedWorkAreaFrame(workArea) { + return { + x: workArea.x + WIN32_FRAME_OVERHANG.x, + y: workArea.y + WIN32_FRAME_OVERHANG.y, + width: workArea.width + WIN32_FRAME_OVERHANG.width, + height: workArea.height + WIN32_FRAME_OVERHANG.height + } +} + function getFrameCenter(frame) { return { x: frame.x + frame.width / 2, @@ -50,7 +68,10 @@ export function isWindowWorkAreaMaximized(window) { const frame = window.getFrame() const { workArea } = getDisplayForFrame(frame) - return framesMatch(frame, workArea) + return ( + framesMatch(frame, workArea) || + framesMatch(frame, getMaximizedWorkAreaFrame(workArea)) + ) } export function clampWindowToWorkArea(window) { @@ -69,8 +90,9 @@ export function clampWindowToWorkArea(window) { const display = getDisplayForFrame(frame) const { bounds, workArea } = display + const maximizedFrame = getMaximizedWorkAreaFrame(workArea) - if (framesMatch(frame, workArea)) { + if (framesMatch(frame, workArea) || framesMatch(frame, maximizedFrame)) { return false } @@ -78,6 +100,11 @@ export function clampWindowToWorkArea(window) { return false } - window.setFrame(workArea.x - 8, workArea.y, workArea.width + 16, workArea.height + 8) + window.setFrame( + maximizedFrame.x, + maximizedFrame.y, + maximizedFrame.width, + maximizedFrame.height + ) return true }