diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1cd6b3c..502beca 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -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((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); }; diff --git a/frontend/src/utils/windowsScaleFix.test.ts b/frontend/src/utils/windowsScaleFix.test.ts new file mode 100644 index 0000000..6cb78dc --- /dev/null +++ b/frontend/src/utils/windowsScaleFix.test.ts @@ -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); + }); +}); diff --git a/frontend/src/utils/windowsScaleFix.ts b/frontend/src/utils/windowsScaleFix.ts new file mode 100644 index 0000000..fe40ff6 --- /dev/null +++ b/frontend/src/utils/windowsScaleFix.ts @@ -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; +};