From 5b9a6a343c17dcbb2153ebe86a0cc29aee43caad Mon Sep 17 00:00:00 2001 From: Razee4315 Date: Tue, 16 Jun 2026 21:39:18 +0500 Subject: [PATCH] fix(fullscreen): kill the Windows black bar and smooth the F11 toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first cut of F11 fullscreen had three problems on Windows with our frameless (decorations:false) window: - Entering fullscreen while maximized left a black bar where the taskbar was / overflowed the right edge (a known tao bug). Now we unmaximize first and restore the maximized state on exit. - F11 wouldn't exit fullscreen because isFullscreen() returns unreliable values for frameless windows. We now track the state ourselves. - The square title-bar button (and title-bar double-click) toggled maximize underneath an active fullscreen, producing the broken state. While fullscreen they now exit fullscreen, and the button shows an "exit fullscreen" icon. Also masks the mid-transition reflow: the unmaximize->fullscreen step resizes the window twice, so the content visibly snapped. An opaque cover drops in instantly, the OS resizes behind it, then it fades out — the change reads as a smooth dip instead of a jump. Adds the core:window:allow-unmaximize capability. --- src-tauri/capabilities/default.json | 1 + src/App.tsx | 54 +++++++++++++++++++++++++---- src/components/TitleBar.tsx | 23 +++++++++--- 3 files changed, 67 insertions(+), 11 deletions(-) diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index 9e122eb..8d2f9e4 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -10,6 +10,7 @@ "core:window:default", "core:window:allow-minimize", "core:window:allow-maximize", + "core:window:allow-unmaximize", "core:window:allow-toggle-maximize", "core:window:allow-set-fullscreen", "core:window:allow-close", diff --git a/src/App.tsx b/src/App.tsx index 6a3fd56..5bc4411 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -854,17 +854,48 @@ function AppContent() { const handleToggleAI = useCallback(() => setShowAIPanel((v) => !v), []); // Toggle OS fullscreen (F11). The custom title bar deliberately stays - // visible so there's always an obvious way back (the same controls, plus - // F11 again); a one-time hint reinforces it for non-technical users. One - // Tauri call covers Windows, Linux, and macOS. FULLSCREEN-01. + // visible so there's always an obvious way back (its square button turns + // into "exit fullscreen", plus F11 again); a one-time hint reinforces it. + // + // Two Windows-specific footguns, both worked around here. (1) On a frameless + // (decorations:false) window, entering fullscreen while MAXIMIZED leaves a + // black bar where the taskbar was / overflows the right edge — a known tao + // bug. So we drop maximize first and restore it on exit. (2) isFullscreen() + // returns unreliable values for frameless windows, so F11 "wouldn't exit"; + // we track the state ourselves instead of querying it. FULLSCREEN-01. + const [isFullscreen, setIsFullscreen] = useState(false); + const isFullscreenRef = useRef(false); + const wasMaximizedRef = useRef(false); + // Drops an opaque cover over the webview while the window resizes. The + // unmaximize→fullscreen step (and its reverse) physically resizes the window + // twice, so the content visibly reflows mid-transition — a jarring "snap". + // We snap the cover on instantly, let the OS settle behind it, then fade it + // out, so the change reads as a smooth dip instead of a jump. + const [fsTransition, setFsTransition] = useState(false); const toggleFullscreen = useCallback(async () => { try { const w = Window.getCurrent(); - const isFs = await w.isFullscreen(); - await w.setFullscreen(!isFs); - if (!isFs) showToast("Fullscreen on — press F11 to exit", "info"); + const next = !isFullscreenRef.current; + setFsTransition(true); + // Yield one frame so the cover actually paints before the window starts + // resizing underneath it — otherwise the first reflow frame leaks through. + await new Promise((r) => requestAnimationFrame(() => r(null))); + if (next) { + wasMaximizedRef.current = await w.isMaximized(); + if (wasMaximizedRef.current) await w.unmaximize(); + await w.setFullscreen(true); + showToast("Fullscreen on — press F11 to exit", "info"); + } else { + await w.setFullscreen(false); + if (wasMaximizedRef.current) await w.maximize(); + } + isFullscreenRef.current = next; + setIsFullscreen(next); } catch { /* browser dev mode — no Tauri window */ + } finally { + // Reveal once the resize has settled; the cover fades out via CSS. + window.setTimeout(() => setFsTransition(false), 200); } }, [showToast]); @@ -1255,6 +1286,8 @@ function AppContent() { onExportError={handleExportError} onToggleAI={aiEnabled ? handleToggleAI : undefined} aiActive={showAIPanel} + isFullscreen={isFullscreen} + onToggleFullscreen={toggleFullscreen} /> {/* Startup update check; invisible unless an update is actually available. */} @@ -1459,6 +1492,15 @@ function AppContent() { )} + {/* Fullscreen transition cover. Snaps opaque instantly (no fade-in) to + hide the mid-resize reflow, then fades out over 300ms to reveal the + settled layout. Sits above everything; pointer-events-none so it never + eats a click once it's transparent. */} +