mirror of
https://github.com/usebruno/bruno.git
synced 2026-06-23 12:45:38 +00:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
) : (
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
Reference in New Issue
Block a user