From b0d8ac6489a5ed674feda026e3ca339c75f4578e Mon Sep 17 00:00:00 2001 From: Eduard Gert Date: Fri, 29 May 2026 17:21:45 +0200 Subject: [PATCH] fix auto size detection --- .../src/components/dialog/ConfirmDialog.tsx | 5 +- .../frontend/src/hooks/useAutoSizeWindow.ts | 51 +++++++++++++++++-- .../auto-update/UpdateInProgressDialog.tsx | 4 +- .../login/LoginWaitingForBrowserDialog.tsx | 2 +- .../session/SessionAboutToExpireDialog.tsx | 2 +- .../modules/session/SessionExpiredDialog.tsx | 2 +- 6 files changed, 53 insertions(+), 13 deletions(-) diff --git a/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx b/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx index 56a2516a0..22c1aa7e4 100644 --- a/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx +++ b/client/ui/frontend/src/components/dialog/ConfirmDialog.tsx @@ -1,6 +1,5 @@ import { ReactNode, forwardRef } from "react"; import {cn} from "@/lib/cn.ts"; -import {isMacOS} from "@/lib/platform.ts"; // ConfirmDialog is the shared layout wrapper used by dialog-style window // surfaces (SessionExpired, SessionAboutToExpire, …). Purely a layout @@ -20,12 +19,12 @@ export const ConfirmDialog = forwardRef( return (
{children}
diff --git a/client/ui/frontend/src/hooks/useAutoSizeWindow.ts b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts index 34f0ec26e..99e854dc0 100644 --- a/client/ui/frontend/src/hooks/useAutoSizeWindow.ts +++ b/client/ui/frontend/src/hooks/useAutoSizeWindow.ts @@ -1,6 +1,6 @@ import { useLayoutEffect, useRef } from "react"; import { Window } from "@wailsio/runtime"; -import {isMacOS} from "@/lib/platform.ts"; +import i18next from "@/lib/i18n"; // useAutoSizeWindow resizes the current Wails window so its height matches // the measured height of the content element the returned ref is attached @@ -15,17 +15,42 @@ import {isMacOS} from "@/lib/platform.ts"; // Re-measures via ResizeObserver so adding/removing content (e.g. the // SessionAboutToExpire title swapping at countdown zero) keeps the chrome // tight to the content with no scrollbar. +// +// Also re-measures on i18next `languageChanged`. The ResizeObserver in +// theory catches the same reflow when translated strings replace each +// other (DE/HU strings often wrap to more lines than EN), but in practice +// the observer can settle on a stale size before React's commit and the +// font's glyph metrics finish updating. An explicit double-rAF after the +// language flip guarantees the final layout is the one we measure. export function useAutoSizeWindow(width: number) { const ref = useRef(null); useLayoutEffect(() => { const el = ref.current; if (!el) return; let shown = false; + let raf1 = 0; + let raf2 = 0; const apply = () => { - let h = Math.ceil(el.getBoundingClientRect().height); - h = isMacOS() ? h : h - el.getBoundingClientRect().height; + const h = Math.ceil(el.getBoundingClientRect().height); if (h <= 0) return; - void Window.SetSize(width, h) + // Wails Window.SetSize takes the *frame* size on every platform + // (Windows: SetWindowPos, macOS: setFrame:, Linux: GTK frame). + // The OS title bar lives inside the frame, so we have to add the + // chrome height before calling SetSize, or the title bar eats + // pixels from the bottom and the rendered content gets clipped. + // + // window.outerHeight / window.innerHeight are useless here: + // WebView2 (and WKWebView) report the WebView's own outer == inner + // because the WebView itself has no chrome — the OS title bar is + // outside the WebView's window object entirely. The only way to + // recover the chrome height is to compare the OS frame height + // (Wails-side Window.Size()) against the WebView viewport + // (window.innerHeight). + void Window.Size() + .then((frame) => { + const chrome = Math.max(0, frame.height - window.innerHeight); + return Window.SetSize(width, h + chrome); + }) .then(() => { if (shown) return; shown = true; @@ -34,10 +59,26 @@ export function useAutoSizeWindow(width: number) { }) .catch(() => {}); }; + // Double rAF: first frame lands after React commits the new + // translated strings, second frame lands after the browser has + // recomputed layout, so apply() sees the final box. + const scheduleApply = () => { + cancelAnimationFrame(raf1); + cancelAnimationFrame(raf2); + raf1 = requestAnimationFrame(() => { + raf2 = requestAnimationFrame(apply); + }); + }; apply(); const ro = new ResizeObserver(apply); ro.observe(el); - return () => ro.disconnect(); + i18next.on("languageChanged", scheduleApply); + return () => { + ro.disconnect(); + cancelAnimationFrame(raf1); + cancelAnimationFrame(raf2); + i18next.off("languageChanged", scheduleApply); + }; }, [width]); return ref; } diff --git a/client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx b/client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx index 3047e0a5a..1b3be91f2 100644 --- a/client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx +++ b/client/ui/frontend/src/modules/auto-update/UpdateInProgressDialog.tsx @@ -86,10 +86,10 @@ export default function UpdateInProgressDialog() { {isError ? ( ) : ( - + )}
diff --git a/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx b/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx index a4693a7d1..30dfaa8ba 100644 --- a/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx +++ b/client/ui/frontend/src/modules/login/LoginWaitingForBrowserDialog.tsx @@ -58,7 +58,7 @@ export default function LoginWaitingForBrowserDialog() {
diff --git a/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx b/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx index 63ba4d2eb..74c7b9b79 100644 --- a/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx +++ b/client/ui/frontend/src/modules/session/SessionAboutToExpireDialog.tsx @@ -121,7 +121,7 @@ export default function SessionAboutToExpireDialog() { return ( - +
diff --git a/client/ui/frontend/src/modules/session/SessionExpiredDialog.tsx b/client/ui/frontend/src/modules/session/SessionExpiredDialog.tsx index 2a37590a8..a4a44285a 100644 --- a/client/ui/frontend/src/modules/session/SessionExpiredDialog.tsx +++ b/client/ui/frontend/src/modules/session/SessionExpiredDialog.tsx @@ -29,7 +29,7 @@ export default function SessionExpiredDialog() { return ( - +
{t("sessionExpired.title")}