From 099443fddc132e57d2c2df003ca4e7be1b4ad193 Mon Sep 17 00:00:00 2001 From: DurianPankek Date: Thu, 9 Jul 2026 15:25:48 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(window):=20=E4=BF=AE?= =?UTF-8?q?=E5=A4=8D=20mac=20=E7=AA=97=E5=8F=A3=E6=81=A2=E5=A4=8D=E8=B6=8A?= =?UTF-8?q?=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.tool-center.test.ts | 3 +- frontend/src/App.tsx | 14 ++--- .../src/utils/wailsWindowViewport.test.ts | 43 +++++++++++++++ frontend/src/utils/wailsWindowViewport.ts | 52 +++++++++++++++++++ 4 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 frontend/src/utils/wailsWindowViewport.test.ts create mode 100644 frontend/src/utils/wailsWindowViewport.ts diff --git a/frontend/src/App.tool-center.test.ts b/frontend/src/App.tool-center.test.ts index 39fdbddc..79bba34a 100644 --- a/frontend/src/App.tool-center.test.ts +++ b/frontend/src/App.tool-center.test.ts @@ -420,7 +420,8 @@ describe('tool center menu entries', () => { }); it('clamps normal runtime window bounds back into the visible screen after display changes', () => { - expect(appSource).toContain('const readCurrentVisibleViewport = () => ({'); + expect(appSource).toContain('const readCurrentVisibleViewport = () => resolveWailsWindowVisibleViewport('); + expect(appSource).toContain('{ useMonitorLocalOrigin: isMacLikePlatform() }'); expect(appSource).toContain('const repairRuntimeWindowBounds = async () => {'); expect(appSource).toContain('const nextBounds = resolveVisibleStartupWindowBounds(currentBounds, readCurrentVisibleViewport());'); expect(appSource).toContain("void emitWindowDiagnostic('adjust:runtime-window-bounds'"); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1f2861fb..a583b021 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -36,7 +36,7 @@ import { useStore, } from './store'; import { GlobalProxyConfig, SavedConnection, SecurityUpdateIssue, SecurityUpdateStatus } from './types'; -import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance'; +import { blurToFilter, normalizeBlurForPlatform, normalizeOpacityForPlatform, isMacLikePlatform, isWindowsPlatform, resolveAppearanceValues } from './utils/appearance'; import { buildFontFamilyOptions, DEFAULT_MONO_FONT_FAMILY, DEFAULT_UI_FONT_FAMILY, getLinuxCJKFontInstallHint, matchFontFamilyOption, resolveMonoFontFamily, resolveUIFontFamily, sanitizeFontFamilyInput, type FontFamilyOption, type InstalledFontFamily } from './utils/fontFamilies'; import { DENSITY_OPTIONS, @@ -129,6 +129,7 @@ import { } from './utils/shortcuts'; import { resolveTitleBarToggleIconKey, resolveWindowsScaleCheckDelayMs, shouldApplyWindowsScaleFix, shouldResetWebViewZoomForScaleFix, shouldToggleMaximisedWindowForScaleFix, type WindowScaleFixReason, type WindowsScaleCheckTrigger } from './utils/windowStateUi'; import { resolveVisibleStartupWindowBounds } from './utils/windowRestoreBounds'; +import { resolveWailsWindowVisibleViewport } from './utils/wailsWindowViewport'; import { SIDEBAR_UTILITY_ITEM_KEYS, resolveAIEntryPlacement, @@ -378,12 +379,11 @@ const detectNavigatorPlatform = (): string => { return navigator.userAgent || ''; }; -const readCurrentVisibleViewport = () => ({ - availWidth: window.screen?.availWidth || window.innerWidth || 0, - availHeight: window.screen?.availHeight || window.innerHeight || 0, - availLeft: (window.screen as Screen & { availLeft?: number })?.availLeft || 0, - availTop: (window.screen as Screen & { availTop?: number })?.availTop || 0, -}); +const readCurrentVisibleViewport = () => resolveWailsWindowVisibleViewport( + window.screen as Screen & { availLeft?: number; availTop?: number }, + { innerWidth: window.innerWidth, innerHeight: window.innerHeight }, + { useMonitorLocalOrigin: isMacLikePlatform() }, +); const getSystemThemeMode = (): 'light' | 'dark' => { if (typeof window === 'undefined' || typeof window.matchMedia !== 'function') { diff --git a/frontend/src/utils/wailsWindowViewport.test.ts b/frontend/src/utils/wailsWindowViewport.test.ts new file mode 100644 index 00000000..3cbc9551 --- /dev/null +++ b/frontend/src/utils/wailsWindowViewport.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from 'vitest'; + +import { resolveWailsWindowVisibleViewport } from './wailsWindowViewport'; + +describe('wailsWindowViewport', () => { + it('keeps browser work-area offsets for platforms that use absolute screen coordinates', () => { + expect(resolveWailsWindowVisibleViewport( + { availWidth: 1728, availHeight: 1040, availLeft: -1728, availTop: 40 }, + { innerWidth: 1440, innerHeight: 900 }, + )).toEqual({ + availWidth: 1728, + availHeight: 1040, + availLeft: -1728, + availTop: 40, + }); + }); + + it('uses current-monitor local origin for macOS Wails window positioning', () => { + expect(resolveWailsWindowVisibleViewport( + { availWidth: 1728, availHeight: 1040, availLeft: -1728, availTop: 40 }, + { innerWidth: 1440, innerHeight: 900 }, + { useMonitorLocalOrigin: true }, + )).toEqual({ + availWidth: 1728, + availHeight: 1040, + availLeft: 0, + availTop: 0, + }); + }); + + it('falls back to window inner size when screen size is unavailable', () => { + expect(resolveWailsWindowVisibleViewport( + null, + { innerWidth: 1280, innerHeight: 720 }, + { useMonitorLocalOrigin: true }, + )).toEqual({ + availWidth: 1280, + availHeight: 720, + availLeft: 0, + availTop: 0, + }); + }); +}); diff --git a/frontend/src/utils/wailsWindowViewport.ts b/frontend/src/utils/wailsWindowViewport.ts new file mode 100644 index 00000000..4b09f842 --- /dev/null +++ b/frontend/src/utils/wailsWindowViewport.ts @@ -0,0 +1,52 @@ +export type WailsWindowVisibleViewport = { + availWidth: number; + availHeight: number; + availLeft?: number; + availTop?: number; +}; + +type ScreenLike = { + availWidth?: number; + availHeight?: number; + availLeft?: number; + availTop?: number; +} | null | undefined; + +type ViewportFallback = { + innerWidth?: number; + innerHeight?: number; +}; + +const toFiniteInteger = (value: unknown, fallback = 0): number => { + const next = Math.trunc(Number(value)); + return Number.isFinite(next) ? next : fallback; +}; + +/** + * 解析 Wails 主窗口恢复使用的可见区域。 + * Wails 的 WindowSetPosition 在 macOS 上接收当前 monitor 可见区域内的局部坐标, + * 因此 macOS 不能把浏览器 screen.availLeft/availTop 的全局屏幕偏移继续传下去。 + */ +export const resolveWailsWindowVisibleViewport = ( + screenLike: ScreenLike, + fallback: ViewportFallback, + options?: { useMonitorLocalOrigin?: boolean }, +): WailsWindowVisibleViewport => { + const availWidth = toFiniteInteger( + screenLike?.availWidth, + toFiniteInteger(fallback.innerWidth), + ); + const availHeight = toFiniteInteger( + screenLike?.availHeight, + toFiniteInteger(fallback.innerHeight), + ); + const useMonitorLocalOrigin = options?.useMonitorLocalOrigin === true; + + return { + availWidth, + availHeight, + // macOS 修复点:Wails 会自己把局部坐标映射到当前 NSScreen.visibleFrame。 + availLeft: useMonitorLocalOrigin ? 0 : toFiniteInteger(screenLike?.availLeft), + availTop: useMonitorLocalOrigin ? 0 : toFiniteInteger(screenLike?.availTop), + }; +};