🐛 fix(window): 修复 Windows 恢复焦点后界面缩放异常

Fixes #315
This commit is contained in:
Syngnat
2026-04-11 21:53:52 +08:00
parent 83fe3d4ed9
commit 5038ae5c9b
3 changed files with 131 additions and 6 deletions

View File

@@ -20,6 +20,7 @@ import { buildOverlayWorkbenchTheme } from './utils/overlayWorkbenchTheme';
import { getConnectionWorkbenchState } from './utils/startupReadiness';
import { createGlobalProxyDraft, toSaveGlobalProxyInput } from './utils/globalProxyDraft';
import { LEGACY_PERSIST_KEY, readLegacyPersistedSecrets, stripLegacyPersistedSecrets } from './utils/legacyConnectionStorage';
import { getWindowsScaleFixNudgedWidth, hasWindowsViewportScaleDrift } from './utils/windowsScaleFix';
import {
SHORTCUT_ACTION_META,
SHORTCUT_ACTION_ORDER,
@@ -534,7 +535,7 @@ function App() {
const wait = (ms: number) => new Promise<void>((resolve) => window.setTimeout(resolve, ms));
const fixWindowScaleIfNeeded = async () => {
const fixWindowScaleIfNeeded = async (reason: 'activation' | 'ratio-change') => {
if (cancelled || inFlight) return;
const now = Date.now();
if (now - lastFixAt < 700) return;
@@ -545,8 +546,8 @@ function App() {
WindowIsMaximised().catch(() => false),
]);
// 避免在全屏/最大化状态下强制改尺寸;这两种状态通常能自行保持 DPI 同步
if (isFullscreen || isMaximised) {
// 全屏状态下只广播 resize避免破坏用户的全屏上下文
if (isFullscreen) {
window.dispatchEvent(new Event('resize'));
lastFixAt = Date.now();
return;
@@ -555,13 +556,46 @@ function App() {
const size = await WindowGetSize().catch(() => null);
const width = Math.trunc(Number(size?.w || 0));
const height = Math.trunc(Number(size?.h || 0));
const hasViewportScaleDrift = hasWindowsViewportScaleDrift({
windowWidth: width,
innerWidth: window.innerWidth,
devicePixelRatio: Number(window.devicePixelRatio) || 1,
visualViewportScale: window.visualViewport?.scale,
});
if (isMaximised) {
if (reason !== 'ratio-change' && !hasViewportScaleDrift) {
window.dispatchEvent(new Event('resize'));
lastFixAt = Date.now();
return;
}
try {
WindowToggleMaximise();
await wait(48);
WindowToggleMaximise();
await wait(64);
} catch (e) {
console.warn("Wails Window maximise toggle unavailable in fixWindowScaleIfNeeded", e);
}
window.dispatchEvent(new Event('resize'));
lastFixAt = Date.now();
return;
}
if (width <= 0 || height <= 0) {
window.dispatchEvent(new Event('resize'));
lastFixAt = Date.now();
return;
}
const nudgedWidth = width > 480 ? width - 1 : width + 1;
if (reason !== 'ratio-change' && !hasViewportScaleDrift) {
window.dispatchEvent(new Event('resize'));
lastFixAt = Date.now();
return;
}
const nudgedWidth = getWindowsScaleFixNudgedWidth(width);
try {
WindowSetSize(nudgedWidth, height);
await wait(28);
@@ -583,7 +617,7 @@ function App() {
return;
}
lastRatio = currentRatio;
void fixWindowScaleIfNeeded();
void fixWindowScaleIfNeeded('ratio-change');
};
const scheduleActivationFix = () => {
@@ -594,7 +628,7 @@ function App() {
activationTimer = window.setTimeout(() => {
activationTimer = null;
if (cancelled) return;
void fixWindowScaleIfNeeded();
void fixWindowScaleIfNeeded('activation');
}, 80);
};

View File

@@ -0,0 +1,45 @@
import { describe, expect, it } from 'vitest';
import {
computeWindowsViewportScaleRatio,
getWindowsScaleFixNudgedWidth,
hasWindowsViewportScaleDrift,
} from './windowsScaleFix';
describe('windowsScaleFix', () => {
it('treats matching window and viewport metrics as stable', () => {
const ratio = computeWindowsViewportScaleRatio({
windowWidth: 1920,
innerWidth: 1280,
devicePixelRatio: 1.5,
});
expect(ratio).toBeCloseTo(1, 5);
expect(hasWindowsViewportScaleDrift({
windowWidth: 1920,
innerWidth: 1280,
devicePixelRatio: 1.5,
})).toBe(false);
});
it('detects zoom drift from viewport width mismatch', () => {
expect(hasWindowsViewportScaleDrift({
windowWidth: 1920,
innerWidth: 1100,
devicePixelRatio: 1.5,
})).toBe(true);
});
it('detects zoom drift from visual viewport scale', () => {
expect(hasWindowsViewportScaleDrift({
windowWidth: 1600,
innerWidth: 1600,
devicePixelRatio: 1,
visualViewportScale: 1.12,
})).toBe(true);
});
it('returns a one-pixel nudge width for normal windows', () => {
expect(getWindowsScaleFixNudgedWidth(960)).toBe(959);
expect(getWindowsScaleFixNudgedWidth(420)).toBe(421);
});
});

View File

@@ -0,0 +1,46 @@
type WindowsViewportScaleInput = {
windowWidth: number;
innerWidth: number;
devicePixelRatio: number;
visualViewportScale?: number | null;
};
export const computeWindowsViewportScaleRatio = ({
windowWidth,
innerWidth,
devicePixelRatio,
}: WindowsViewportScaleInput): number => {
const normalizedWindowWidth = Number(windowWidth);
const normalizedInnerWidth = Number(innerWidth);
const normalizedDevicePixelRatio = Number(devicePixelRatio);
if (
!Number.isFinite(normalizedWindowWidth) || normalizedWindowWidth <= 0 ||
!Number.isFinite(normalizedInnerWidth) || normalizedInnerWidth <= 0 ||
!Number.isFinite(normalizedDevicePixelRatio) || normalizedDevicePixelRatio <= 0
) {
return 1;
}
return (normalizedWindowWidth / normalizedDevicePixelRatio) / normalizedInnerWidth;
};
export const hasWindowsViewportScaleDrift = (
metrics: WindowsViewportScaleInput,
tolerance = 0.08,
): boolean => {
const normalizedTolerance = Math.max(0.01, Number(tolerance) || 0.08);
const visualViewportScale = Number(metrics.visualViewportScale);
if (Number.isFinite(visualViewportScale) && Math.abs(visualViewportScale - 1) > normalizedTolerance) {
return true;
}
const viewportScaleRatio = computeWindowsViewportScaleRatio(metrics);
return Math.abs(viewportScaleRatio - 1) > normalizedTolerance;
};
export const getWindowsScaleFixNudgedWidth = (width: number): number => {
const normalizedWidth = Math.trunc(Number(width) || 0);
if (normalizedWidth <= 0) {
return 0;
}
return normalizedWidth > 480 ? normalizedWidth - 1 : normalizedWidth + 1;
};