mirror of
https://github.com/Syngnat/GoNavi.git
synced 2026-05-22 17:00:21 +08:00
🐛 fix(window): 用 CSS zoom nudge 修复任务栏恢复字体变大且不引入重复最大化
- 撤回上次错误的 toggle 改动:恢复 9848b8b2 的 restore 不重新最大化取舍,避免用户在任务栏点击恢复时看到窗口"被弹两次"
- 新增 applyWindowsViewportZoomNudge:通过短暂将 documentElement.style.zoom 设为 1.0001 并在两帧内重置,强制 Chromium 重算 layout metrics 修复字体变大,零可见动画、不动窗口
- maximised + drift + restore 路径从仅 dispatch resize 改为先 zoom nudge 再 dispatch resize
- 锁定 windowStateUi.test.ts 中 shouldToggleMaximisedWindowForScaleFix('restore', true)=false 取舍并补注释禁止再次反转
- windowsScaleFix.test.ts 加 jsdom 环境,新增双帧 zoom nudge 行为测试
This commit is contained in:
@@ -58,7 +58,7 @@ import {
|
||||
type SecurityUpdateRepairSource,
|
||||
type SecurityUpdateSettingsFocusTarget,
|
||||
} from './utils/securityUpdateRepairFlow';
|
||||
import { getWindowsScaleFixNudgedWidth, hasWindowsViewportScaleDrift } from './utils/windowsScaleFix';
|
||||
import { applyWindowsViewportZoomNudge, getWindowsScaleFixNudgedWidth, hasWindowsViewportScaleDrift } from './utils/windowsScaleFix';
|
||||
import {
|
||||
SHORTCUT_ACTION_META,
|
||||
SHORTCUT_ACTION_ORDER,
|
||||
@@ -670,6 +670,12 @@ function App() {
|
||||
|
||||
if (isMaximised) {
|
||||
if (!shouldToggleMaximisedWindowForScaleFix(reason, hasViewportScaleDrift)) {
|
||||
// restore 场景刻意不走 Unmaximise→Maximise(避免可见的重复最大化抖动)。
|
||||
// 改用 CSS zoom nudge 强制 Chromium 重算 layout,让 viewport drift 后残留的
|
||||
// 字体度量恢复正确——同样能修字体,且无动画。
|
||||
if (hasViewportScaleDrift) {
|
||||
applyWindowsViewportZoomNudge();
|
||||
}
|
||||
window.dispatchEvent(new Event('resize'));
|
||||
lastFixAt = Date.now();
|
||||
return;
|
||||
|
||||
@@ -20,13 +20,10 @@ describe('windowStateUi', () => {
|
||||
it('applies the Windows scale fix when a minimized taskbar window is restored with viewport drift', () => {
|
||||
expect(shouldApplyWindowsScaleFix('restore', true)).toBe(true);
|
||||
expect(shouldApplyWindowsScaleFix('restore', false)).toBe(false);
|
||||
});
|
||||
|
||||
it('toggles maximised windows on restore so taskbar-restored fonts return to the correct size', () => {
|
||||
// maximised 状态下 OS 拒绝 SetSize nudge,唯一可行的修复是切一次 maximise;
|
||||
// 重复触发由 inFlight 互斥 + 700ms 冷却 + ratio-change 合并到 activationTimer 防御。
|
||||
expect(shouldToggleMaximisedWindowForScaleFix('restore', true)).toBe(true);
|
||||
expect(shouldToggleMaximisedWindowForScaleFix('restore', false)).toBe(false);
|
||||
// 关键:restore 场景刻意不再触发 maximised 窗口的 toggle —— Unmaximise → Maximise 在
|
||||
// 任务栏恢复的真实交互里会被用户肉眼看见为"重复最大化"动画,比偶发字体变大更糟。
|
||||
// 这是 9848b8b2 已有的取舍,禁止再次被"修复"成 true。
|
||||
expect(shouldToggleMaximisedWindowForScaleFix('restore', true)).toBe(false);
|
||||
});
|
||||
|
||||
it('debounces resize-triggered Windows scale checks until window transitions settle', () => {
|
||||
|
||||
@@ -8,14 +8,17 @@ export const shouldApplyWindowsScaleFix = (
|
||||
hasViewportScaleDrift: boolean,
|
||||
): boolean => (reason === 'ratio-change' || reason === 'restore') && hasViewportScaleDrift;
|
||||
|
||||
// maximised 窗口在 Windows 上无法通过 SetSize nudge 修复 viewport drift(OS 拒绝 resize 已 maximized 窗口),
|
||||
// 唯一能让 WebView2 重新计算缩放的办法是 Unmaximise → Maximise 切换一次。restore 场景(任务栏点击恢复)
|
||||
// 必须允许这条路径,否则用户从最小化状态恢复后字体会保持错误大小。重复触发由 inFlight 互斥与 lastFixAt
|
||||
// 冷却 + checkDevicePixelRatio 在 minimisedSeen 上下文转发到 activationTimer 共同防御,无需额外禁用 toggle。
|
||||
// 关于 restore 场景为何刻意不走 toggle(见 9848b8b2):
|
||||
// maximised 窗口在 Windows 上无法通过 SetSize nudge 修复 viewport drift(OS 拒绝 resize),
|
||||
// 唯一能让 WebView2 重新计算缩放的办法是 Unmaximise → Maximise,但在任务栏图标点击恢复的
|
||||
// 真实场景下,用户会肉眼看到窗口"被弹两次"的重复最大化动画——比偶发字体变大更糟。
|
||||
// 取舍:restore 时只 dispatch resize 让 React 重算布局,宁可字体保持当前缩放,
|
||||
// 也不要可见的重复最大化抖动。ratio-change(DPR 变化,例如把窗口拖到另一块显示器)则
|
||||
// 允许 toggle,因为那种场景下用户预期会有视觉过渡。
|
||||
export const shouldToggleMaximisedWindowForScaleFix = (
|
||||
reason: WindowScaleFixReason,
|
||||
hasViewportScaleDrift: boolean,
|
||||
): boolean => (reason === 'ratio-change' || reason === 'restore') && hasViewportScaleDrift;
|
||||
): boolean => reason === 'ratio-change' && hasViewportScaleDrift;
|
||||
|
||||
export const resolveWindowsScaleCheckDelayMs = (trigger: WindowsScaleCheckTrigger): number =>
|
||||
trigger === 'resize' ? 240 : 0;
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
// @vitest-environment jsdom
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
applyWindowsViewportZoomNudge,
|
||||
computeWindowsViewportScaleRatio,
|
||||
getWindowsScaleFixNudgedWidth,
|
||||
hasWindowsViewportScaleDrift,
|
||||
@@ -42,4 +44,32 @@ describe('windowsScaleFix', () => {
|
||||
expect(getWindowsScaleFixNudgedWidth(960)).toBe(959);
|
||||
expect(getWindowsScaleFixNudgedWidth(420)).toBe(421);
|
||||
});
|
||||
|
||||
it('applies and resets the CSS zoom nudge across two animation frames', () => {
|
||||
const rafCallbacks: FrameRequestCallback[] = [];
|
||||
const rafSpy = vi.spyOn(window, 'requestAnimationFrame').mockImplementation((cb) => {
|
||||
rafCallbacks.push(cb);
|
||||
return rafCallbacks.length;
|
||||
});
|
||||
|
||||
const root = document.documentElement;
|
||||
const style = root.style as CSSStyleDeclaration & { zoom?: string };
|
||||
style.zoom = '';
|
||||
|
||||
applyWindowsViewportZoomNudge();
|
||||
// 第一阶段:zoom 已被设置为 1.0001
|
||||
expect(style.zoom).toBe('1.0001');
|
||||
expect(rafCallbacks).toHaveLength(1);
|
||||
|
||||
// 第一帧:调度第二帧 reset
|
||||
rafCallbacks[0]?.(0);
|
||||
expect(style.zoom).toBe('1.0001');
|
||||
expect(rafCallbacks).toHaveLength(2);
|
||||
|
||||
// 第二帧:恢复 zoom
|
||||
rafCallbacks[1]?.(0);
|
||||
expect(style.zoom).toBe('');
|
||||
|
||||
rafSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -44,3 +44,34 @@ export const getWindowsScaleFixNudgedWidth = (width: number): number => {
|
||||
}
|
||||
return normalizedWidth > 480 ? normalizedWidth - 1 : normalizedWidth + 1;
|
||||
};
|
||||
|
||||
// applyWindowsViewportZoomNudge 通过短暂改变 documentElement 的 CSS zoom 触发 Chromium
|
||||
// 重新计算 layout metrics。用于 maximised 窗口在 restore 场景下 viewport drift 修复的
|
||||
// 无动画路径:不重新最大化(避免 9848b8b2 修复的可见"重复最大化"抖动),也不调 WebView2
|
||||
// COM API(避免 Windows 平台特定代码)。
|
||||
//
|
||||
// 为什么这样能修:Chromium 在切换 zoom factor 时会重算所有 px 度量与 currentColor/font-size
|
||||
// 派生值,drift 后残留的旧度量被丢弃。1.0001 与 1 在视觉上不可分辨但属于 invalidation 阈值
|
||||
// 之外,强制触发完整 layout 重排。
|
||||
//
|
||||
// 用 requestAnimationFrame 两帧而不是立即 reset,让 Chromium 在第一帧完成 nudge layout、
|
||||
// 第二帧恢复——避免单帧合成被合并掉。
|
||||
export const applyWindowsViewportZoomNudge = (): void => {
|
||||
if (typeof document === 'undefined' || !document.documentElement) {
|
||||
return;
|
||||
}
|
||||
const root = document.documentElement;
|
||||
const style = root.style as CSSStyleDeclaration & { zoom?: string };
|
||||
const previousZoom = style.zoom ?? '';
|
||||
style.zoom = '1.0001';
|
||||
const reset = () => {
|
||||
style.zoom = previousZoom;
|
||||
};
|
||||
if (typeof window === 'undefined' || typeof window.requestAnimationFrame !== 'function') {
|
||||
reset();
|
||||
return;
|
||||
}
|
||||
window.requestAnimationFrame(() => {
|
||||
window.requestAnimationFrame(reset);
|
||||
});
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user