From 78ee99eab92141fe689b4cd8fa4755c12859c7ba Mon Sep 17 00:00:00 2001 From: Abhishek S Lal Date: Wed, 17 Dec 2025 21:40:24 +0530 Subject: [PATCH] Fix/app titlebar windows (#6437) * style: Update padding and margin in StyledWrapper for improved layout; adjust ActionIcon size in ResponseLayoutToggle for better UI consistency; enhance title bar color handling in Electron app * feat: Enhance AppTitleBar with Windows-specific controls and OS detection * refactor: Improve OS detection and error handling in AppTitleBar; streamline maximize state management * feat: Implement IPC communication for maximize/unmaximize events in AppTitleBar; enhance state management in Electron main process --- .../components/AppTitleBar/StyledWrapper.js | 64 +++++++-- .../src/components/AppTitleBar/index.js | 124 +++++++++++++++--- .../ResponseLayoutToggle/index.js | 2 +- .../src/pages/Bruno/StyledWrapper.js | 1 + packages/bruno-electron/src/index.js | 38 +++++- 5 files changed, 193 insertions(+), 36 deletions(-) diff --git a/packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js b/packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js index 1b22ea215..bfef446ec 100644 --- a/packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js +++ b/packages/bruno-app/src/components/AppTitleBar/StyledWrapper.js @@ -5,7 +5,6 @@ const Wrapper = styled.div` display: flex; align-items: center; background: ${(props) => props.theme.sidebar.bg}; - border-bottom: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg}; -webkit-app-region: drag; user-select: none; @@ -22,7 +21,7 @@ const Wrapper = styled.div` /* When in full screen, no traffic lights so reduce padding */ &.fullscreen .titlebar-content { - padding-left: 4px; + padding-left: 6px; } /* Remove drag region from interactive elements */ @@ -103,6 +102,13 @@ const Wrapper = styled.div` align-items: center; justify-content: flex-end; flex-shrink: 0; + -webkit-app-region: no-drag; + } + + /* App action buttons container */ + .titlebar-actions { + display: flex; + align-items: center; } /* Workspace Dropdown Styles */ @@ -181,16 +187,54 @@ const Wrapper = styled.div` } /* Adjust for non-macOS platforms */ - body:not(.os-mac) & { - .titlebar-content { - padding-left: 12px; - } + &:not(.os-mac) .titlebar-content { + padding-left: 12px; } - /* Leave room for Windows caption buttons when the overlay is enabled */ - body.os-windows & { - .titlebar-content { - padding-right: 120px; + /* Windows-specific styles */ + &.os-windows .titlebar-content { + padding-right: 0px; + padding-left: 0px; + } + + &.os-windows .titlebar-left { + margin-left: 6px; + } + + /* Custom window control buttons for Windows - always interactive, above modal overlay */ + .window-controls { + display: flex; + align-items: stretch; + height: 36px; + margin-left: 8px; + position: relative; + z-index: 1000; + } + + .window-control-btn { + display: flex; + align-items: center; + justify-content: center; + width: 46px; + height: 100%; + border: none; + background: transparent; + color: ${(props) => props.theme.text}; + cursor: pointer; + transition: background-color 0.1s ease; + -webkit-app-region: no-drag; + + &:hover { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + + &:active { + background: ${(props) => props.theme.sidebar.collection.item.hoverBg}; + } + + &.close:hover { + background: #e81123; + color: white; } } `; diff --git a/packages/bruno-app/src/components/AppTitleBar/index.js b/packages/bruno-app/src/components/AppTitleBar/index.js index 0c9ec1f8d..9c66c4937 100644 --- a/packages/bruno-app/src/components/AppTitleBar/index.js +++ b/packages/bruno-app/src/components/AppTitleBar/index.js @@ -1,5 +1,5 @@ import React from 'react'; -import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconUpload, IconSettings } from '@tabler/icons'; +import { IconCheck, IconChevronDown, IconFolder, IconHome, IconPin, IconPinned, IconPlus, IconUpload, IconSettings, IconMinus, IconSquare, IconX, IconCopy } from '@tabler/icons'; import { forwardRef, useCallback, useEffect, useMemo, useState } from 'react'; import toast from 'react-hot-toast'; import { useDispatch, useSelector } from 'react-redux'; @@ -20,10 +20,20 @@ import IconBottombarToggle from 'components/Icons/IconBottombarToggle/index'; import StyledWrapper from './StyledWrapper'; import { toTitleCase } from 'utils/common/index'; import ResponseLayoutToggle from 'components/ResponsePane/ResponseLayoutToggle'; +import { isMacOS, isWindowsOS } from 'utils/common/platform'; + +const getOsClass = () => { + if (isMacOS()) return 'os-mac'; + if (isWindowsOS()) return 'os-windows'; + return 'os-other'; +}; const AppTitleBar = () => { const dispatch = useDispatch(); const [isFullScreen, setIsFullScreen] = useState(false); + const [isMaximized, setIsMaximized] = useState(false); + const osClass = getOsClass(); + const isWindows = osClass === 'os-windows'; // Listen for fullscreen changes useEffect(() => { @@ -44,6 +54,50 @@ const AppTitleBar = () => { }; }, []); + // Check initial maximized state and listen for changes (Windows only) + useEffect(() => { + if (!isWindows) return; + const { ipcRenderer } = window; + if (!ipcRenderer) return; + + // Get initial state + ipcRenderer.invoke('renderer:window-is-maximized') + .then((maximized) => { + setIsMaximized(maximized); + }) + .catch((error) => { + console.error('Error getting initial maximized state:', error); + }); + + // Listen for maximize/unmaximize events from main process + const removeMaximizedListener = ipcRenderer.on('main:window-maximized', () => { + setIsMaximized(true); + }); + + const removeUnmaximizedListener = ipcRenderer.on('main:window-unmaximized', () => { + setIsMaximized(false); + }); + + return () => { + removeMaximizedListener(); + removeUnmaximizedListener(); + }; + }, [isWindows]); + + // Window control handlers (Windows only) - these always work, even with modals open + const handleMinimize = useCallback(() => { + window.ipcRenderer?.send('renderer:window-minimize'); + }, []); + + const handleMaximize = useCallback(() => { + window.ipcRenderer?.send('renderer:window-maximize'); + // State will be updated via IPC events from main process (main:window-maximized/main:window-unmaximized) + }, []); + + const handleClose = useCallback(() => { + window.ipcRenderer?.send('renderer:window-close'); + }, []); + // Get workspace info const { workspaces, activeWorkspaceUid } = useSelector((state) => state.workspaces); const preferences = useSelector((state) => state.app.preferences); @@ -183,7 +237,7 @@ const AppTitleBar = () => { }, [sortedWorkspaces, activeWorkspaceUid, preferences, handlePinWorkspace]); return ( - + {createWorkspaceModalOpen && ( setCreateWorkspaceModalOpen(false)} /> )} @@ -222,27 +276,55 @@ const AppTitleBar = () => { {/* Right section: Action buttons */}
- {/* Toggle sidebar */} - - - +
+ {/* Toggle sidebar */} + + + - {/* Toggle devtools */} - - - + {/* Toggle devtools */} + + + - + +
+ + {isWindows && ( +
+ + + +
+ )}
diff --git a/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js index 310a49296..cfd75a88b 100644 --- a/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js +++ b/packages/bruno-app/src/components/ResponsePane/ResponseLayoutToggle/index.js @@ -91,7 +91,7 @@ const ResponseLayoutToggle = forwardRef(({ children }, ref) => { > {children ? children : ( - + {orientation === 'vertical' ? ( ) : ( diff --git a/packages/bruno-app/src/pages/Bruno/StyledWrapper.js b/packages/bruno-app/src/pages/Bruno/StyledWrapper.js index 9ce1816eb..36f522b07 100644 --- a/packages/bruno-app/src/pages/Bruno/StyledWrapper.js +++ b/packages/bruno-app/src/pages/Bruno/StyledWrapper.js @@ -5,6 +5,7 @@ const Wrapper = styled.div` width: 100%; height: 100%; flex: 1; + border-top: 1px solid ${(props) => props.theme.sidebar.collection.item.hoverBg}; &.is-dragging { cursor: col-resize !important; diff --git a/packages/bruno-electron/src/index.js b/packages/bruno-electron/src/index.js index 7b7dfc180..b25bf2cf3 100644 --- a/packages/bruno-electron/src/index.js +++ b/packages/bruno-electron/src/index.js @@ -13,7 +13,7 @@ if (isDev) { } const { format } = require('url'); -const { BrowserWindow, app, session, Menu, globalShortcut, ipcMain } = require('electron'); +const { BrowserWindow, app, session, Menu, globalShortcut, ipcMain, nativeTheme } = require('electron'); const { setContentSecurityPolicy } = require('electron-util'); if (isDev && process.env.ELECTRON_USER_DATA_PATH) { @@ -157,7 +157,6 @@ app.on('ready', async () => { icon: path.join(__dirname, 'about/256x256.png'), // Custom title bar – ensure React titlebar occupies the window chrome on all OSes titleBarStyle: isMac ? 'hiddenInset' : isWindows ? 'hidden' : 'default', - titleBarOverlay: isWindows ? { height: 36 } : undefined, trafficLightPosition: isMac ? { x: 12, y: 10 } : undefined // we will bring this back // see https://github.com/usebruno/bruno/issues/440 @@ -168,6 +167,31 @@ app.on('ready', async () => { mainWindow.maximize(); } + // Window control IPC handlers (Windows custom titlebar) + ipcMain.on('renderer:window-minimize', () => { + if (!isWindows) return; + mainWindow.minimize(); + }); + + ipcMain.on('renderer:window-maximize', () => { + if (!isWindows) return; + if (mainWindow.isMaximized()) { + mainWindow.unmaximize(); + } else { + mainWindow.maximize(); + } + }); + + ipcMain.on('renderer:window-close', () => { + if (!isWindows) return; + mainWindow.close(); + }); + + ipcMain.handle('renderer:window-is-maximized', () => { + if (!isWindows) return false; + return mainWindow.isMaximized(); + }); + mainWindow.once('ready-to-show', () => { mainWindow.show(); }); @@ -204,8 +228,14 @@ app.on('ready', async () => { mainWindow.on('resize', handleBoundsChange); mainWindow.on('move', handleBoundsChange); - mainWindow.on('maximize', () => saveMaximized(true)); - mainWindow.on('unmaximize', () => saveMaximized(false)); + mainWindow.on('maximize', () => { + saveMaximized(true); + mainWindow.webContents.send('main:window-maximized'); + }); + mainWindow.on('unmaximize', () => { + saveMaximized(false); + mainWindow.webContents.send('main:window-unmaximized'); + }); // Full screen events for title bar padding adjustment mainWindow.on('enter-full-screen', () => {