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
This commit is contained in:
Abhishek S Lal
2025-12-17 21:40:24 +05:30
committed by GitHub
parent 73124fd715
commit 78ee99eab9
5 changed files with 193 additions and 36 deletions

View File

@@ -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;
}
}
`;

View File

@@ -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 (
<StyledWrapper className={`app-titlebar ${isFullScreen ? 'fullscreen' : ''}`}>
<StyledWrapper className={`app-titlebar ${osClass} ${isFullScreen ? 'fullscreen' : ''}`}>
{createWorkspaceModalOpen && (
<CreateWorkspace onClose={() => setCreateWorkspaceModalOpen(false)} />
)}
@@ -222,27 +276,55 @@ const AppTitleBar = () => {
{/* Right section: Action buttons */}
<div className="titlebar-right">
{/* Toggle sidebar */}
<ActionIcon
onClick={handleToggleSidebar}
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
size="lg"
data-testid="toggle-sidebar-button"
>
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
</ActionIcon>
<div className="titlebar-actions">
{/* Toggle sidebar */}
<ActionIcon
onClick={handleToggleSidebar}
label={sidebarCollapsed ? 'Show sidebar' : 'Hide sidebar'}
size="lg"
data-testid="toggle-sidebar-button"
>
<IconSidebarToggle collapsed={sidebarCollapsed} size={16} strokeWidth={1.5} />
</ActionIcon>
{/* Toggle devtools */}
<ActionIcon
onClick={handleToggleDevtools}
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
size="lg"
data-testid="toggle-devtools-button"
>
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
</ActionIcon>
{/* Toggle devtools */}
<ActionIcon
onClick={handleToggleDevtools}
label={isConsoleOpen ? 'Hide devtools' : 'Show devtools'}
size="lg"
data-testid="toggle-devtools-button"
>
<IconBottombarToggle collapsed={!isConsoleOpen} size={16} strokeWidth={1.5} />
</ActionIcon>
<ResponseLayoutToggle />
<ResponseLayoutToggle />
</div>
{isWindows && (
<div className="window-controls">
<button
className="window-control-btn minimize"
onClick={handleMinimize}
aria-label="Minimize"
>
<IconMinus size={16} stroke={1} />
</button>
<button
className="window-control-btn maximize"
onClick={handleMaximize}
aria-label={isMaximized ? 'Restore' : 'Maximize'}
>
{isMaximized ? <IconCopy size={14} stroke={1} /> : <IconSquare size={14} stroke={1} />}
</button>
<button
className="window-control-btn close"
onClick={handleClose}
aria-label="Close"
>
<IconX size={16} stroke={1} />
</button>
</div>
)}
</div>
</div>
</StyledWrapper>

View File

@@ -91,7 +91,7 @@ const ResponseLayoutToggle = forwardRef(({ children }, ref) => {
>
{children ? children : (
<StyledWrapper className="flex items-center w-full">
<ActionIcon className="p-1">
<ActionIcon size="lg" className="p-1">
{orientation === 'vertical' ? (
<IconLayoutColumns size={16} strokeWidth={2} />
) : (

View File

@@ -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;

View File

@@ -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', () => {