diff --git a/src/main/window/createMainWindow.ts b/src/main/window/createMainWindow.ts index 11543dadf93..e3cbecdf509 100644 --- a/src/main/window/createMainWindow.ts +++ b/src/main/window/createMainWindow.ts @@ -655,10 +655,20 @@ export function createMainWindow( const onPopupMenu = (): void => { Menu.getApplicationMenu()?.popup({ window: mainWindow }) } + // Why: the renderer's WindowControls mounts after ready-to-show, which is + // also when savedMaximized is restored — so window:maximize-changed has + // already fired (or not fired, if maximize() was called pre-mount) before + // the listener attaches. Expose a synchronous getter so the button can + // initialize its icon to match the current state on mount. + const isMaximizedChannel = 'window:isMaximized' + const onIsMaximized = (): boolean => { + return !mainWindow.isDestroyed() && mainWindow.isMaximized() + } ipcMain.on(minimizeChannel, onMinimize) ipcMain.on(maximizeChannel, onMaximize) ipcMain.on(requestCloseChannel, onRequestClose) ipcMain.on(popupMenuChannel, onPopupMenu) + ipcMain.handle(isMaximizedChannel, onIsMaximized) ipcMain.on(confirmCloseChannel, onConfirmClose) mainWindow.on('closed', () => { @@ -671,6 +681,7 @@ export function createMainWindow( ipcMain.removeListener(maximizeChannel, onMaximize) ipcMain.removeListener(requestCloseChannel, onRequestClose) ipcMain.removeListener(popupMenuChannel, onPopupMenu) + ipcMain.removeHandler(isMaximizedChannel) ipcMain.removeListener(confirmCloseChannel, onConfirmClose) ipcMain.removeListener(markdownFocusChannel, onMarkdownEditorFocused) mainWindow.webContents.removeListener('context-menu', onMainContextMenu) diff --git a/src/preload/api-types.ts b/src/preload/api-types.ts index 7899877fc89..4246f84d6ee 100644 --- a/src/preload/api-types.ts +++ b/src/preload/api-types.ts @@ -1101,6 +1101,7 @@ export type PreloadApi = { onFullscreenChanged: (callback: (isFullScreen: boolean) => void) => () => void minimize: () => void maximize: () => void + isMaximized: () => Promise onMaximizeChanged: (callback: (isMaximized: boolean) => void) => () => void requestClose: () => void popupMenu: () => void diff --git a/src/preload/index.ts b/src/preload/index.ts index d64636c93c4..c6747eb2abc 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -1970,6 +1970,10 @@ const api = { maximize: (): void => { ipcRenderer.send('window:maximize') }, + /** Windows only: read the current maximize state on mount, since + * window:maximize-changed only fires on transitions and a window that + * starts maximized would otherwise show the wrong icon. */ + isMaximized: (): Promise => ipcRenderer.invoke('window:isMaximized'), /** Windows only: subscribe to maximize state changes so the renderer-drawn * maximize button can show the correct restore/maximize icon. */ onMaximizeChanged: (callback: (isMaximized: boolean) => void): (() => void) => { diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index e38376ed0c9..a534818c3ca 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -64,7 +64,20 @@ const isWindows = !isMac && navigator.userAgent.includes('Windows') function WindowControls(): React.JSX.Element { const [maximized, setMaximized] = useState(false) useEffect(() => { - return window.api.ui.onMaximizeChanged(setMaximized) + // Why: window:maximize-changed only fires on transitions, so a window + // restored to a maximized state at startup would render the wrong icon + // until the user first clicks the button. Seed from main on mount. + let cancelled = false + void window.api.ui.isMaximized().then((value) => { + if (!cancelled) { + setMaximized(value) + } + }) + const unsubscribe = window.api.ui.onMaximizeChanged(setMaximized) + return () => { + cancelled = true + unsubscribe() + } }, []) return (
@@ -1091,12 +1104,18 @@ function App(): React.JSX.Element { {workspaceActive && !rightSidebarOpen && (
{rightSidebarToggle} + {/* Why: the fixed-position window-controls overlay (138px, + top-right) sits on top of this floating toggle on Windows. + Reserve its width so the toggle stays clickable. */} + {isWindows &&
}
)}
diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index d300f2171b5..df47ba9bbd4 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -433,7 +433,7 @@ z-index: 9999; display: flex; flex-direction: row; - height: 42px; + height: 36px; -webkit-app-region: no-drag; } @@ -442,7 +442,7 @@ align-items: center; justify-content: center; width: 46px; - height: 42px; + height: 36px; background: transparent; border: none; color: var(--muted-foreground); diff --git a/src/renderer/src/components/right-sidebar/index.tsx b/src/renderer/src/components/right-sidebar/index.tsx index 4182c7578fd..b32894f651a 100644 --- a/src/renderer/src/components/right-sidebar/index.tsx +++ b/src/renderer/src/components/right-sidebar/index.tsx @@ -72,6 +72,7 @@ type ActivityBarItem = { } const isMac = navigator.userAgent.includes('Mac') +const isWindows = !isMac && navigator.userAgent.includes('Windows') const mod = isMac ? '\u2318' : 'Ctrl+' const ACTIVITY_ITEMS: ActivityBarItem[] = [ @@ -305,7 +306,10 @@ function RightSidebarInner(): React.JSX.Element {
{activityBarIcons}
- {closeButton} +
+ {closeButton} + {isWindows &&
} +
@@ -320,7 +324,12 @@ function RightSidebarInner(): React.JSX.Element { {visibleItems.find((item) => item.id === effectiveTab)?.title ?? ''} - {closeButton} + +
+ {closeButton} + {isWindows &&
} +
+
)}