mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-07-03 16:51:27 +08:00
🐛 fix(window): 适配分辨率变化后的窗口边界
- 运行期检测普通窗口是否超出当前可用屏幕并自动收回 - 启动恢复时同步裁剪超过当前屏幕的窗口尺寸 Fixes #594
This commit is contained in:
@@ -350,9 +350,13 @@ describe('tool center menu entries', () => {
|
||||
|
||||
it('captures window state on startup and lifecycle events instead of waiting only for the polling interval', () => {
|
||||
expect(appSource).toContain('const scheduleWindowStateSave = (delayMs = 120) => {');
|
||||
expect(appSource).toContain('const scheduleWindowBoundsRepair = (delayMs = 80) => {');
|
||||
expect(appSource).toContain('if (hydrated) {');
|
||||
expect(appSource).toContain('scheduleWindowBoundsRepair(360);');
|
||||
expect(appSource).toContain('scheduleWindowStateSave(320);');
|
||||
expect(appSource).toContain('const unsubscribeHydration = useStore.persist.onFinishHydration(() => {');
|
||||
expect(appSource).toContain('scheduleWindowBoundsRepair();');
|
||||
expect(appSource).toContain('scheduleWindowStateSave(260);');
|
||||
expect(appSource).toContain("window.addEventListener('resize', handleWindowRuntimeChange);");
|
||||
expect(appSource).toContain("window.addEventListener('focus', handleWindowRuntimeChange);");
|
||||
expect(appSource).toContain("window.addEventListener('pageshow', handleWindowRuntimeChange);");
|
||||
@@ -360,6 +364,15 @@ describe('tool center menu entries', () => {
|
||||
expect(appSource).toContain("window.addEventListener('beforeunload', handleWindowLifecycleFlush, { capture: true });");
|
||||
});
|
||||
|
||||
it('clamps normal runtime window bounds back into the visible screen after display changes', () => {
|
||||
expect(appSource).toContain('const readCurrentVisibleViewport = () => ({');
|
||||
expect(appSource).toContain('const repairRuntimeWindowBounds = async () => {');
|
||||
expect(appSource).toContain('const nextBounds = resolveVisibleStartupWindowBounds(currentBounds, readCurrentVisibleViewport());');
|
||||
expect(appSource).toContain("void emitWindowDiagnostic('adjust:runtime-window-bounds'");
|
||||
expect(appSource).toContain('WindowSetSize(nextBounds.width, nextBounds.height);');
|
||||
expect(appSource).toContain('WindowSetPosition(nextBounds.x, nextBounds.y);');
|
||||
});
|
||||
|
||||
it('keeps titlebar double-click on maximise while shortcuts may enter macOS fullscreen', () => {
|
||||
expect(appSource).toContain('const handleTitleBarWindowToggle = async (options?: { allowMacNativeFullscreen?: boolean }) => {');
|
||||
expect(appSource).toContain('const allowMacNativeFullscreen = options?.allowMacNativeFullscreen === true;');
|
||||
|
||||
@@ -154,6 +154,13 @@ 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 mergeSavedConnections = (current: SavedConnection[], imported: SavedConnection[]): SavedConnection[] => {
|
||||
const merged = new Map<string, SavedConnection>();
|
||||
@@ -747,12 +754,7 @@ function App() {
|
||||
const bounds = state.windowBounds;
|
||||
if (!bounds || bounds.width < 400 || bounds.height < 300) return;
|
||||
try {
|
||||
const nextBounds = resolveVisibleStartupWindowBounds(bounds, {
|
||||
availWidth: window.screen?.availWidth || 0,
|
||||
availHeight: window.screen?.availHeight || 0,
|
||||
availLeft: (window.screen as Screen & { availLeft?: number })?.availLeft || 0,
|
||||
availTop: (window.screen as Screen & { availTop?: number })?.availTop || 0,
|
||||
});
|
||||
const nextBounds = resolveVisibleStartupWindowBounds(bounds, readCurrentVisibleViewport());
|
||||
if (
|
||||
nextBounds.x !== bounds.x ||
|
||||
nextBounds.y !== bounds.y ||
|
||||
@@ -797,6 +799,7 @@ function App() {
|
||||
let cancelled = false;
|
||||
let hydrated = useStore.persist.hasHydrated();
|
||||
let eventSaveTimer: number | null = null;
|
||||
let boundsRepairTimer: number | null = null;
|
||||
let lastSaved = '';
|
||||
|
||||
const saveWindowState = async () => {
|
||||
@@ -859,13 +862,79 @@ function App() {
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
const repairRuntimeWindowBounds = async () => {
|
||||
if (cancelled || !hydrated) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [isFs, isMax] = await Promise.all([
|
||||
safeWindowRuntimeCall(() => WindowIsFullscreen(), false),
|
||||
safeWindowRuntimeCall(() => WindowIsMaximised(), false),
|
||||
]);
|
||||
if (isFs || isMax) {
|
||||
return;
|
||||
}
|
||||
const [size, pos] = await Promise.all([
|
||||
safeWindowRuntimeCall(() => WindowGetSize(), null),
|
||||
safeWindowRuntimeCall(() => WindowGetPosition(), null),
|
||||
]);
|
||||
if (!size || !pos) {
|
||||
return;
|
||||
}
|
||||
const currentBounds = {
|
||||
width: Math.trunc(Number(size.w || 0)),
|
||||
height: Math.trunc(Number(size.h || 0)),
|
||||
x: Math.trunc(Number(pos.x || 0)),
|
||||
y: Math.trunc(Number(pos.y || 0)),
|
||||
};
|
||||
if (currentBounds.width <= 0 || currentBounds.height <= 0) {
|
||||
return;
|
||||
}
|
||||
const nextBounds = resolveVisibleStartupWindowBounds(currentBounds, readCurrentVisibleViewport());
|
||||
if (
|
||||
nextBounds.x === currentBounds.x &&
|
||||
nextBounds.y === currentBounds.y &&
|
||||
nextBounds.width === currentBounds.width &&
|
||||
nextBounds.height === currentBounds.height
|
||||
) {
|
||||
return;
|
||||
}
|
||||
void emitWindowDiagnostic('adjust:runtime-window-bounds', {
|
||||
from: currentBounds,
|
||||
to: nextBounds,
|
||||
});
|
||||
WindowSetSize(nextBounds.width, nextBounds.height);
|
||||
WindowSetPosition(nextBounds.x, nextBounds.y);
|
||||
lastSaved = `${nextBounds.width},${nextBounds.height},${nextBounds.x},${nextBounds.y}`;
|
||||
useStore.getState().setWindowBounds(nextBounds);
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
} catch {
|
||||
// Wails runtime window APIs are best-effort here.
|
||||
}
|
||||
};
|
||||
|
||||
const scheduleWindowBoundsRepair = (delayMs = 80) => {
|
||||
if (cancelled || !hydrated) {
|
||||
return;
|
||||
}
|
||||
if (boundsRepairTimer !== null) {
|
||||
window.clearTimeout(boundsRepairTimer);
|
||||
}
|
||||
boundsRepairTimer = window.setTimeout(() => {
|
||||
boundsRepairTimer = null;
|
||||
void repairRuntimeWindowBounds();
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
const handleWindowRuntimeChange = () => {
|
||||
scheduleWindowStateSave();
|
||||
scheduleWindowBoundsRepair();
|
||||
scheduleWindowStateSave(260);
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (document.visibilityState === 'visible') {
|
||||
scheduleWindowStateSave(120);
|
||||
scheduleWindowBoundsRepair();
|
||||
scheduleWindowStateSave(260);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -874,6 +943,7 @@ function App() {
|
||||
};
|
||||
|
||||
if (hydrated) {
|
||||
scheduleWindowBoundsRepair(360);
|
||||
scheduleWindowStateSave(320);
|
||||
}
|
||||
const unsubscribeHydration = useStore.persist.onFinishHydration(() => {
|
||||
@@ -881,6 +951,7 @@ function App() {
|
||||
return;
|
||||
}
|
||||
hydrated = true;
|
||||
scheduleWindowBoundsRepair(360);
|
||||
scheduleWindowStateSave(320);
|
||||
});
|
||||
|
||||
@@ -898,6 +969,9 @@ function App() {
|
||||
if (eventSaveTimer !== null) {
|
||||
window.clearTimeout(eventSaveTimer);
|
||||
}
|
||||
if (boundsRepairTimer !== null) {
|
||||
window.clearTimeout(boundsRepairTimer);
|
||||
}
|
||||
window.clearInterval(timer);
|
||||
window.removeEventListener('resize', handleWindowRuntimeChange);
|
||||
window.removeEventListener('focus', handleWindowRuntimeChange);
|
||||
|
||||
@@ -23,4 +23,18 @@ describe('windowRestoreBounds', () => {
|
||||
{ availWidth: 1600, availHeight: 900, availLeft: 0, availTop: 0 },
|
||||
)).toEqual({ width: 900, height: 640, x: 350, y: 130 });
|
||||
});
|
||||
|
||||
it('shrinks a restored window that is larger than the current visible screen', () => {
|
||||
expect(resolveVisibleStartupWindowBounds(
|
||||
{ width: 2560, height: 1440, x: 0, y: 0 },
|
||||
{ availWidth: 1440, availHeight: 860, availLeft: 0, availTop: 25 },
|
||||
)).toEqual({ width: 1440, height: 860, x: 0, y: 25 });
|
||||
});
|
||||
|
||||
it('keeps oversized restored bounds inside an offset visible screen', () => {
|
||||
expect(resolveVisibleStartupWindowBounds(
|
||||
{ width: 2400, height: 1200, x: 1800, y: 60 },
|
||||
{ availWidth: 1728, availHeight: 1040, availLeft: 1728, availTop: 40 },
|
||||
)).toEqual({ width: 1728, height: 1040, x: 1728, y: 40 });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,31 @@ type VisibleViewport = {
|
||||
|
||||
const MIN_VISIBLE_WIDTH = 160;
|
||||
const MIN_VISIBLE_HEIGHT = 120;
|
||||
const MIN_RESTORED_WIDTH = 400;
|
||||
const MIN_RESTORED_HEIGHT = 300;
|
||||
|
||||
const resolveVisibleDimension = (
|
||||
rawDimension: number,
|
||||
visibleDimension: number,
|
||||
minimumDimension: number,
|
||||
): number => {
|
||||
const dimension = Math.trunc(Number(rawDimension) || 0);
|
||||
if (visibleDimension <= 0 || dimension <= 0) {
|
||||
return dimension;
|
||||
}
|
||||
const minimum = Math.min(minimumDimension, visibleDimension);
|
||||
return Math.min(Math.max(dimension, minimum), visibleDimension);
|
||||
};
|
||||
|
||||
const clampPosition = (
|
||||
position: number,
|
||||
dimension: number,
|
||||
visibleStart: number,
|
||||
visibleDimension: number,
|
||||
): number => {
|
||||
const maxPosition = visibleStart + Math.max(0, visibleDimension - dimension);
|
||||
return Math.min(Math.max(position, visibleStart), maxPosition);
|
||||
};
|
||||
|
||||
export const resolveVisibleStartupWindowBounds = (
|
||||
bounds: WindowRestoreBounds,
|
||||
@@ -29,19 +54,36 @@ export const resolveVisibleStartupWindowBounds = (
|
||||
const visibleTop = Math.trunc(Number(viewport.availTop) || 0);
|
||||
const visibleRight = visibleLeft + visibleWidth;
|
||||
const visibleBottom = visibleTop + visibleHeight;
|
||||
const nextWidth = resolveVisibleDimension(bounds.width, visibleWidth, MIN_RESTORED_WIDTH);
|
||||
const nextHeight = resolveVisibleDimension(bounds.height, visibleHeight, MIN_RESTORED_HEIGHT);
|
||||
const resizedBounds: WindowRestoreBounds = {
|
||||
...bounds,
|
||||
width: nextWidth,
|
||||
height: nextHeight,
|
||||
};
|
||||
|
||||
const overlapWidth = Math.min(bounds.x + bounds.width, visibleRight) - Math.max(bounds.x, visibleLeft);
|
||||
const overlapHeight = Math.min(bounds.y + bounds.height, visibleBottom) - Math.max(bounds.y, visibleTop);
|
||||
const overlapWidth = Math.min(resizedBounds.x + resizedBounds.width, visibleRight) - Math.max(resizedBounds.x, visibleLeft);
|
||||
const overlapHeight = Math.min(resizedBounds.y + resizedBounds.height, visibleBottom) - Math.max(resizedBounds.y, visibleTop);
|
||||
const sizeChanged = resizedBounds.width !== bounds.width || resizedBounds.height !== bounds.height;
|
||||
if (
|
||||
!sizeChanged &&
|
||||
overlapWidth >= Math.min(MIN_VISIBLE_WIDTH, bounds.width) &&
|
||||
overlapHeight >= Math.min(MIN_VISIBLE_HEIGHT, bounds.height)
|
||||
) {
|
||||
return bounds;
|
||||
}
|
||||
|
||||
if (sizeChanged && overlapWidth > 0 && overlapHeight > 0) {
|
||||
return {
|
||||
...resizedBounds,
|
||||
x: clampPosition(resizedBounds.x, resizedBounds.width, visibleLeft, visibleWidth),
|
||||
y: clampPosition(resizedBounds.y, resizedBounds.height, visibleTop, visibleHeight),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...bounds,
|
||||
x: visibleLeft + Math.max(0, Math.trunc((visibleWidth - bounds.width) / 2)),
|
||||
y: visibleTop + Math.max(0, Math.trunc((visibleHeight - bounds.height) / 2)),
|
||||
...resizedBounds,
|
||||
x: visibleLeft + Math.max(0, Math.trunc((visibleWidth - resizedBounds.width) / 2)),
|
||||
y: visibleTop + Math.max(0, Math.trunc((visibleHeight - resizedBounds.height) / 2)),
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user