mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-13 00:13:33 +08:00
🐛 fix(window): 修复 mac 窗口恢复越界 (#643)
## 问题描述 macOS 多屏环境下,普通窗口恢复/修正位置时可能被定位到当前屏幕外,尤其是在外接屏位于主屏左侧或上方、浏览器 `screen.availLeft/availTop` 出现全局偏移时更容易触发。 ## 复现条件说明 该问题不是所有 macOS 用户都能稳定复现,更依赖特定显示器布局和窗口状态。高概率触发条件包括: - 使用 macOS 多屏。 - 外接屏位于主屏左侧或上方,导致 `screen.availLeft` / `screen.availTop` 为负数或非零。 - GoNavi 处于普通窗口态,而不是最大化或全屏。 - 应用保存过窗口位置,并在重启、切屏、拔插显示器、focus/resize 后触发窗口 bounds 恢复或修正。 单屏 mac、外接屏仅位于主屏右侧、始终最大化/全屏打开,或未保存过异常窗口位置的用户通常不容易触发。 ## 根本原因 Wails 的 `WindowSetPosition(x, y)` 在 macOS 上接收的是当前 monitor 可见区域内的局部坐标,而不是全局桌面坐标。原逻辑把 `window.screen.availLeft/availTop` 作为可见区起点传入恢复算法,导致保存的局部坐标和恢复时的全局偏移混用。 ## 修复方案 - 新增 `resolveWailsWindowVisibleViewport`,集中处理 Wails 主窗口恢复使用的可见区域。 - macOS 下将可见区原点固定为当前 monitor 局部 `(0, 0)`,避免把浏览器 screen 全局偏移继续传给 Wails。 - 补充单元测试覆盖负 `availLeft` 的外接屏场景。 - 更新 App 窗口恢复源码断言测试。 ## 变更文件 | 文件 | 修改说明 | 变更行数 | |---|---|---| | `frontend/src/App.tsx` | macOS 下使用 Wails 局部坐标解析窗口可见区 | +7 -7 | | `frontend/src/utils/wailsWindowViewport.ts` | 新增 Wails 窗口可见区解析工具 | +52 | | `frontend/src/utils/wailsWindowViewport.test.ts` | 覆盖 macOS 局部原点和 fallback 场景 | +43 | | `frontend/src/App.tool-center.test.ts` | 同步窗口恢复源码断言 | +2 -1 | ## 合并注意事项 - **影响功能点**:桌面端普通窗口启动恢复、focus/resize 后窗口 bounds 修正。 - **验证方式**: - `npm test -- --run src/utils/wailsWindowViewport.test.ts src/utils/windowRestoreBounds.test.ts src/utils/windowStartupLayout.test.ts src/App.tool-center.test.ts` - `npm run build` - **回归风险**:低。变更集中在窗口可见区解析;非 macOS 平台保留原 `screen.availLeft/availTop` 处理。 - **回滚方式**:revert 此 PR 即可。
This commit is contained in:
@@ -423,7 +423,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'");
|
||||
|
||||
@@ -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,
|
||||
@@ -136,6 +136,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,
|
||||
@@ -385,12 +386,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') {
|
||||
|
||||
43
frontend/src/utils/wailsWindowViewport.test.ts
Normal file
43
frontend/src/utils/wailsWindowViewport.test.ts
Normal file
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
52
frontend/src/utils/wailsWindowViewport.ts
Normal file
52
frontend/src/utils/wailsWindowViewport.ts
Normal file
@@ -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),
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user