diff --git a/apps/web/src/index.css b/apps/web/src/index.css index 0f9942e3295ee..0cddb76a05ba2 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -151,16 +151,19 @@ html[data-mobile-composer-route-transition="true"]::view-transition-old(t3-mobil } } +/* env() goes undefined in a minimized window, so Windows supplies --wco-fallback-* below. */ .wco { - --native-titlebar-height: env(titlebar-area-height, 0px); - --workspace-topbar-height: max(40px, env(titlebar-area-height, 52px)); + --native-titlebar-height: env(titlebar-area-height, var(--wco-fallback-height, 0px)); + --workspace-topbar-height: max(40px, env(titlebar-area-height, var(--wco-fallback-height, 52px))); --workspace-controls-top: env(titlebar-area-y, 0px); --workspace-controls-left: calc(env(titlebar-area-x, 0px) + 0.75rem); --workspace-controls-right: calc( - 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + 0.75rem + 100vw - env(titlebar-area-width, calc(100vw - var(--wco-fallback-controls-width, 0px))) - + env(titlebar-area-x, 0px) + 0.75rem ); --workspace-native-controls-inset: calc( - 100vw - env(titlebar-area-width, 100vw) - env(titlebar-area-x, 0px) + 0.75rem + 100vw - env(titlebar-area-width, calc(100vw - var(--wco-fallback-controls-width, 0px))) - + env(titlebar-area-x, 0px) + 0.75rem ); } @@ -1756,6 +1759,9 @@ body { .electron-windows { --desktop-window-right-resize-inset: 6px; + /* Matches DesktopWindow TITLEBAR_HEIGHT and the width of the native window buttons at 100% zoom. */ + --wco-fallback-height: 40px; + --wco-fallback-controls-width: 137px; } /* App-chrome grain. Baked into each surface's own background (behind diff --git a/apps/web/src/lib/windowControlsOverlay.browser.test.ts b/apps/web/src/lib/windowControlsOverlay.browser.test.ts new file mode 100644 index 0000000000000..18386647d3f26 --- /dev/null +++ b/apps/web/src/lib/windowControlsOverlay.browser.test.ts @@ -0,0 +1,60 @@ +import { expect, it, vi } from "vite-plus/test"; + +import "~/index.css"; +import { + syncDocumentElectronPlatformClasses, + syncDocumentWindowControlsOverlayClass, +} from "./windowControlsOverlay"; + +vi.mock("~/env", async (importOriginal) => ({ + ...(await importOriginal()), + isElectron: true, +})); + +function paintedTitlebarLayout() { + const probe = document.createElement("div"); + probe.style.cssText = + "height: var(--workspace-topbar-height); width: var(--workspace-native-controls-inset)"; + document.body.append(probe); + try { + const { height, width } = probe.getBoundingClientRect(); + return { height, width }; + } finally { + probe.remove(); + } +} + +// The test page has no overlay, so env(titlebar-area-*) is undefined just as in a minimized window. +it("keeps the titlebar layout while a minimized Windows window reports an empty overlay", () => { + const overlay = Object.assign(new EventTarget(), { visible: true }); + let fullscreen = false; + const bridgeWindow = window as { desktopBridge?: unknown }; + bridgeWindow.desktopBridge = { getWindowFullscreenState: () => fullscreen }; + using _platform = vi.spyOn(navigator, "platform", "get").mockReturnValue("Win32"); + using _overlay = vi + .spyOn( + navigator as unknown as { windowControlsOverlay: unknown }, + "windowControlsOverlay", + "get", + ) + .mockReturnValue(overlay); + const root = document.documentElement; + const stopPlatformClasses = syncDocumentElectronPlatformClasses("Win32"); + const stop = syncDocumentWindowControlsOverlayClass(); + try { + overlay.visible = false; + overlay.dispatchEvent(new Event("geometrychange")); + + expect(root.classList.contains("wco")).toBe(true); + expect(paintedTitlebarLayout()).toEqual({ height: 40, width: 137 + 12 }); + + fullscreen = true; + window.dispatchEvent(new Event("resize")); + expect(root.classList.contains("wco")).toBe(false); + } finally { + delete bridgeWindow.desktopBridge; + stop(); + stopPlatformClasses(); + root.classList.remove("wco"); + } +}); diff --git a/apps/web/src/lib/windowControlsOverlay.ts b/apps/web/src/lib/windowControlsOverlay.ts index 9b2e90e6a754f..d17fa744ff5fa 100644 --- a/apps/web/src/lib/windowControlsOverlay.ts +++ b/apps/web/src/lib/windowControlsOverlay.ts @@ -53,12 +53,30 @@ export function syncDocumentWindowControlsOverlayClass(): () => void { } const overlay = getWindowControlsOverlay(); + // Windows also empties the overlay while minimized; dropping the class then shifts the layout + // on restore. Fullscreen empties it for real, so only fullscreen may drop it there. + const keepsClassUnlessFullscreen = isElectron && isWindowsPlatform(navigator.platform); const update = () => { - document.documentElement.classList.toggle(WCO_CLASS_NAME, overlay !== null && overlay.visible); + const visible = overlay !== null && overlay.visible; + if ( + !visible && + keepsClassUnlessFullscreen && + !window.desktopBridge?.getWindowFullscreenState() + ) { + return; + } + document.documentElement.classList.toggle(WCO_CLASS_NAME, visible); }; update(); - return onWindowControlsOverlayGeometryChange(update); + const stopGeometry = onWindowControlsOverlayGeometryChange(update); + if (!keepsClassUnlessFullscreen) return stopGeometry; + // The overlay can hide before the window reports fullscreen; the fullscreen resize settles it. + window.addEventListener("resize", update); + return () => { + stopGeometry(); + window.removeEventListener("resize", update); + }; } function subscribeTitlebarArea(listener: () => void): () => void {