🐛 fix(desktop): adjust mac fullscreen titlebar spacing (#15693)

This commit is contained in:
Innei
2026-06-11 22:02:48 +08:00
committed by GitHub
parent f6c23e3654
commit bfdfd3bca3
12 changed files with 135 additions and 17 deletions
@@ -91,6 +91,13 @@ export default class BrowserWindowsCtr extends ControllerModule {
});
}
@IpcMethod()
isWindowFullScreen() {
return this.withSenderIdentifier((identifier) => {
return this.app.browserManager.isWindowFullScreen(identifier);
});
}
@IpcMethod()
setWindowAlwaysOnTop(flag: boolean) {
this.withSenderIdentifier((identifier) => {
@@ -29,6 +29,7 @@ const mockCloseWindow = vi.fn();
const mockMinimizeWindow = vi.fn();
const mockMaximizeWindow = vi.fn();
const mockIsWindowMaximized = vi.fn();
const mockIsWindowFullScreen = vi.fn();
const mockRetrieveByIdentifier = vi.fn();
const mockStartSession = vi.fn();
const testSenderIdentifierString: string = 'test-window-event-id';
@@ -58,6 +59,7 @@ const mockApp = {
minimizeWindow: mockMinimizeWindow,
maximizeWindow: mockMaximizeWindow,
isWindowMaximized: mockIsWindowMaximized,
isWindowFullScreen: mockIsWindowFullScreen,
retrieveByIdentifier: mockRetrieveByIdentifier.mockImplementation(
(identifier: AppBrowsersIdentifiers | string) => {
if (identifier === 'some-other-window') {
@@ -166,6 +168,20 @@ describe('BrowserWindowsCtr', () => {
});
});
describe('isWindowFullScreen', () => {
it('should return fullscreen state for the sender window', () => {
mockIsWindowFullScreen.mockReturnValueOnce(true);
const sender = {} as any;
const context = { sender, event: { sender } as any } as IpcContext;
const result = runWithIpcContext(context, () => browserWindowsCtr.isWindowFullScreen());
expect(mockGetIdentifierByWebContents).toHaveBeenCalledWith(context.sender);
expect(mockIsWindowFullScreen).toHaveBeenCalledWith(testSenderIdentifierString);
expect(result).toBe(true);
});
});
describe('interceptRoute', () => {
const baseParams = { source: 'link-click' as const };
@@ -219,6 +219,7 @@ export default class Browser {
this.setupReadyToShowListener(browserWindow);
this.setupCloseListener(browserWindow);
this.setupFocusListener(browserWindow);
this.setupFullscreenListener(browserWindow);
this.setupTopLevelNavigationListener(browserWindow);
this.setupWillPreventUnloadListener(browserWindow);
this.setupContextMenu(browserWindow);
@@ -309,6 +310,18 @@ export default class Browser {
});
}
private setupFullscreenListener(browserWindow: BrowserWindow): void {
logger.debug(`[${this.identifier}] Setting up fullscreen event listeners.`);
browserWindow.on('enter-full-screen', () => {
this.broadcast('windowFullscreenChanged', { isFullScreen: true });
});
browserWindow.on('leave-full-screen', () => {
this.broadcast('windowFullscreenChanged', { isFullScreen: false });
});
}
/**
* Setup context menu with platform-specific features
* Delegates to MenuManager for consistent platform behavior
@@ -368,6 +368,11 @@ export class BrowserManager {
return browser?.browserWindow.isMaximized() ?? false;
}
isWindowFullScreen(identifier: string) {
const browser = this.browsers.get(identifier);
return browser?.browserWindow.isFullScreen() ?? false;
}
setWindowSize(identifier: string, size: { height?: number; width?: number }) {
const browser = this.browsers.get(identifier);
browser?.setWindowSize(size);
@@ -541,6 +541,30 @@ describe('Browser', () => {
});
});
describe('fullscreen events', () => {
it('should broadcast fullscreen state changes', () => {
const enterHandler = mockBrowserWindow.on.mock.calls.find(
(call) => call[0] === 'enter-full-screen',
)?.[1];
const leaveHandler = mockBrowserWindow.on.mock.calls.find(
(call) => call[0] === 'leave-full-screen',
)?.[1];
expect(enterHandler).toBeDefined();
expect(leaveHandler).toBeDefined();
enterHandler();
expect(mockBrowserWindow.webContents.send).toHaveBeenCalledWith('windowFullscreenChanged', {
isFullScreen: true,
});
leaveHandler();
expect(mockBrowserWindow.webContents.send).toHaveBeenCalledWith('windowFullscreenChanged', {
isFullScreen: false,
});
});
});
describe('close', () => {
it('should close window', () => {
browser.close();
@@ -527,12 +527,7 @@ export class FlatListBuilder {
if (!nextContinuation) return;
if (this.shouldDrainParentContinuations(nextContinuation.parentId, processedIds)) {
this.buildFlatListRecursive(
nextContinuation.parentId,
flatList,
processedIds,
allMessages,
);
this.buildFlatListRecursive(nextContinuation.parentId, flatList, processedIds, allMessages);
continue;
}
@@ -586,7 +581,9 @@ export class FlatListBuilder {
if (this.isAgentCouncilMode(parentMessage)) return true;
const taskChildren = children.filter((childId) => this.messageMap.get(childId)?.role === 'task');
const taskChildren = children.filter(
(childId) => this.messageMap.get(childId)?.role === 'task',
);
return taskChildren.length > 1;
}
+1 -1
View File
@@ -13,7 +13,7 @@
"lexical": "^0.42.0"
},
"peerDependencies": {
"@lobehub/editor": "^4",
"@lobehub/editor": "^4.17.1",
"debug": "*"
}
}
@@ -5,4 +5,5 @@ export interface SystemBroadcastEvents {
systemThemeChanged: (data: { themeMode: ThemeAppearance }) => void;
themeChanged: (data: { themeMode: ThemeMode }) => void;
windowFocused: () => void;
windowFullscreenChanged: (data: { isFullScreen: boolean }) => void;
}
@@ -1,5 +1,6 @@
'use client';
import { useWatchBroadcast } from '@lobechat/electron-client-ipc';
import { ActionIcon, Flexbox, Popover, Tooltip } from '@lobehub/ui';
import { createStaticStyles } from 'antd-style';
import { ArrowLeft, ArrowRight, Clock } from 'lucide-react';
@@ -7,6 +8,7 @@ import { memo, useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import ToggleLeftPanelButton from '@/features/NavPanel/ToggleLeftPanelButton';
import { electronSystemService } from '@/services/electron/system';
import { useGlobalStore } from '@/store/global';
import type { GlobalState } from '@/store/global/initialState';
import { systemStatusSelectors } from '@/store/global/selectors';
@@ -14,17 +16,11 @@ import { electronStylish } from '@/styles/electron';
import { isMacOS } from '@/utils/platform';
import { useNavigationHistory } from '../navigation/useNavigationHistory';
import { TITLE_BAR_HORIZONTAL_PADDING } from './layout';
import { getMacTrafficLightPadding } from './layout';
import RecentlyViewed from './RecentlyViewed';
const isMac = isMacOS();
// Reserve space for macOS traffic lights so the toggle sits to their right.
// Matches the popup TitleBar's MAC_TRAFFIC_LIGHT_WIDTH (80) minus the titlebar's
// own horizontal padding, which already offsets the left edge.
const MAC_TRAFFIC_LIGHT_WIDTH = 80;
const macTrafficLightPadding = MAC_TRAFFIC_LIGHT_WIDTH - TITLE_BAR_HORIZONTAL_PADDING;
// A persistent titlebar toggle must not share the sidebar toggle's id, or it
// would create a duplicate DOM id and get caught by NavPanelDraggable's hover CSS.
const NAV_TOGGLE_ID = 'titlebar_toggle_left_panel_button';
@@ -52,9 +48,35 @@ const NavigationBar = memo(() => {
const { t } = useTranslation('electron');
const { canGoBack, canGoForward, goBack, goForward } = useNavigationHistory();
const [historyOpen, setHistoryOpen] = useState(false);
const [isWindowFullScreen, setIsWindowFullScreen] = useState(false);
const leftPanelWidth = useNavPanelWidth();
useWatchBroadcast('windowFullscreenChanged', ({ isFullScreen }) => {
if (isMac) setIsWindowFullScreen(isFullScreen);
});
useEffect(() => {
if (!isMac) return;
let disposed = false;
const syncFullScreenState = async () => {
try {
const isFullScreen = await electronSystemService.isWindowFullScreen();
if (!disposed) setIsWindowFullScreen(isFullScreen);
} catch {
if (!disposed) setIsWindowFullScreen(false);
}
};
void syncFullScreenState();
return () => {
disposed = true;
};
}, []);
// Toggle history popover
const toggleHistoryOpen = useCallback(() => {
setHistoryOpen((prev) => !prev);
@@ -80,6 +102,7 @@ const NavigationBar = memo(() => {
const tooltipContent = t('navigation.recentView');
const isLeftPanelVisible = leftPanelWidth > 0;
const macTrafficLightPadding = getMacTrafficLightPadding(isMac, isWindowFullScreen);
return (
<Flexbox
@@ -89,7 +112,7 @@ const NavigationBar = memo(() => {
gap={8}
justify={isMac ? 'space-between' : 'end'}
style={{
paddingLeft: isMac ? macTrafficLightPadding : 0,
paddingLeft: macTrafficLightPadding,
paddingRight: 8,
// Expanded: span the sidebar width so the right group hugs its right edge.
// Collapsed (macOS): shrink to content so the controls cluster at the left edge.
+22 -1
View File
@@ -1,6 +1,11 @@
import { describe, expect, it } from 'vitest';
import { getTitleBarLayoutConfig, TITLE_BAR_HORIZONTAL_PADDING } from './layout';
import {
getMacTrafficLightPadding,
getTitleBarLayoutConfig,
MAC_TRAFFIC_LIGHT_WIDTH,
TITLE_BAR_HORIZONTAL_PADDING,
} from './layout';
describe('getTitleBarLayoutConfig', () => {
it('reserves right-side space for native Windows controls', () => {
@@ -27,3 +32,19 @@ describe('getTitleBarLayoutConfig', () => {
});
});
});
describe('getMacTrafficLightPadding', () => {
it('reserves traffic-light space for regular macOS windows', () => {
expect(getMacTrafficLightPadding(true, false)).toBe(
MAC_TRAFFIC_LIGHT_WIDTH - TITLE_BAR_HORIZONTAL_PADDING,
);
});
it('removes traffic-light space for macOS fullscreen windows', () => {
expect(getMacTrafficLightPadding(true, true)).toBe(0);
});
it('does not reserve traffic-light space outside macOS', () => {
expect(getMacTrafficLightPadding(false, false)).toBe(0);
});
});
+7
View File
@@ -1,5 +1,12 @@
export const TITLE_BAR_HORIZONTAL_PADDING = 12;
export const WINDOWS_NATIVE_CONTROL_WIDTH = 150;
export const MAC_TRAFFIC_LIGHT_WIDTH = 80;
export const getMacTrafficLightPadding = (isMac: boolean, isFullScreen: boolean): number => {
if (!isMac || isFullScreen) return 0;
return MAC_TRAFFIC_LIGHT_WIDTH - TITLE_BAR_HORIZONTAL_PADDING;
};
export interface TitleBarLayoutConfig {
padding: string;
+4
View File
@@ -36,6 +36,10 @@ class ElectronSystemService {
return this.ipc.windows.isWindowMaximized();
}
async isWindowFullScreen(): Promise<boolean> {
return this.ipc.windows.isWindowFullScreen();
}
async minimizeWindow(): Promise<void> {
return this.ipc.windows.minimizeWindow();
}