diff --git a/src/tray-panel/TrayPanel.tsx b/src/tray-panel/TrayPanel.tsx index c32c003fd..62f04fcf0 100644 --- a/src/tray-panel/TrayPanel.tsx +++ b/src/tray-panel/TrayPanel.tsx @@ -485,9 +485,17 @@ export function TrayPanel() { useEffect(() => { const update = () => setPixelRatio(window.devicePixelRatio || 1); + // 跨屏时 DPR 可能晚于显示事件更新,固定画布也不一定触发 resize。 + // 每次 DPR 变化后重新订阅,才能继续检测下一次跨屏或 WebView 缩放。 + const media = window.matchMedia?.(`(resolution: ${pixelRatio}dppx)`); + media?.addEventListener("change", update); window.addEventListener("resize", update); - return () => window.removeEventListener("resize", update); - }, []); + update(); + return () => { + media?.removeEventListener("change", update); + window.removeEventListener("resize", update); + }; + }, [pixelRatio]); const load = useCallback(async () => { try { diff --git a/tests/components/TrayPanel.test.tsx b/tests/components/TrayPanel.test.tsx index d41666616..36c63d74d 100644 --- a/tests/components/TrayPanel.test.tsx +++ b/tests/components/TrayPanel.test.tsx @@ -153,6 +153,7 @@ describe("TrayPanel window layout", () => { afterEach(() => { vi.restoreAllMocks(); + vi.unstubAllGlobals(); window.localStorage.removeItem("cc-switch-tray-tab"); delete document.documentElement.dataset.glass; delete document.documentElement.dataset.layout; @@ -390,6 +391,67 @@ describe("TrayPanel window layout", () => { }, ); + it.each([ + [1, 1.5, 2], + [2, 1.5, 1], + ])( + "updates clipping without resize when DPR arrives after show (%s → %s → %s)", + async (initialScale, secondScale, thirdScale) => { + const ratio = vi.spyOn(window, "devicePixelRatio", "get"); + ratio.mockReturnValue(initialScale); + const mediaQueries: Array< + EventTarget & { media: string; matches: boolean } + > = []; + vi.stubGlobal("matchMedia", (query: string) => { + const media = Object.assign(new EventTarget(), { + media: query, + matches: query === `(resolution: ${window.devicePixelRatio}dppx)`, + }); + mediaQueries.push(media); + return media as MediaQueryList; + }); + const { container } = await openPanel({ + ...windows, + scaleFactor: initialScale, + }); + const main = container.querySelector(".tp-card")!; + + for (const [index, scaleFactor] of [secondScale, thirdScale].entries()) { + const seq = index + 2; + // 原生窗口收到布局后才跨屏:显示事件仍读到旧屏的 DPR。 + await act(async () => { + emitTauriEvent(SHOW_EVENT, { ...windows, seq, scaleFactor }); + }); + await paint(); + const calls = vi.mocked(trayPanelApi.layout).mock.calls.length; + + // 画布尺寸不变,没有 resize;只分发匹配状态确实改变的媒体查询事件。 + await act(async () => { + ratio.mockReturnValue(scaleFactor); + for (const media of [...mediaQueries]) { + const matches = media.media === `(resolution: ${scaleFactor}dppx)`; + if (media.matches !== matches) { + media.matches = matches; + media.dispatchEvent(new Event("change")); + } + } + }); + await paint(); + + expect(main).toHaveStyle({ width: "320px", maxHeight: "620px" }); + expect(trayPanelApi.layout).toHaveBeenCalledTimes(calls + 1); + expect(trayPanelApi.layout).toHaveBeenLastCalledWith( + expect.objectContaining({ + seq, + width: 620, + height: 760, + cards: [{ x: 300, y: 440, width: 320, height: 320 }], + }), + ); + } + }, + ); + it("limits both cards to the available height on small scaled displays", async () => { const { container } = await openPanel({ ...windows, boundsTop: -180 }); fireEvent.click(