From 6b1e56a65afa37c94ae72cd74a4c3586676c9b87 Mon Sep 17 00:00:00 2001 From: Etienne Lescot Date: Thu, 1 Oct 2026 02:02:19 +0200 Subject: [PATCH] fix(hud): report the stack rect from the allocation, not a measurement Switching the bar to vertical and back left it about 130 DIP higher each round trip (Windows 11, 125 %). Right after hud-overlay-set-size the ResizeObserver fires again while the old viewport is still laid out, so the hud-overlay-content that follows carries a rect measured in the window the main process has just replaced. The main process stores it as the anchor, and the next flip re-anchors the window on it: the window keeps its top-left instead of the bar's bottom-centre. The stack is always centred and pinned HUD_BAR_BOTTOM above the bottom edge, which is how grantedContent is already computed. The reported rect now uses the same formula on the allocated size, and only its size comes from the measurement. Fixes #951 --- src/components/launch/LaunchWindow.test.tsx | 14 ++++++++++++-- src/components/launch/LaunchWindow.tsx | 20 ++++++++++---------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/components/launch/LaunchWindow.test.tsx b/src/components/launch/LaunchWindow.test.tsx index 06c83db94..b65e8518b 100644 --- a/src/components/launch/LaunchWindow.test.tsx +++ b/src/components/launch/LaunchWindow.test.tsx @@ -1358,9 +1358,19 @@ describe("LaunchWindow overlay sizing", () => { stubBox(bar.parentElement as HTMLElement, 400, stackHeight); await flushResizeObservers(); + // The stub sits at (0, 0), which is not where the stack is in the allocated + // window. The reported position must come from the allocation, not the + // measurement: a measured one can belong to a viewport the main process has + // already resized, and anchoring the next layout flip on it walks the bar (#951). + const sizeCalls = ( + window.electronAPI.setHudOverlaySize as unknown as { + mock: { calls: [number, number, unknown][] }; + } + ).mock.calls; + const [allocatedWidth, allocatedHeight] = sizeCalls[sizeCalls.length - 1]; expect(window.electronAPI.setHudOverlayContent).toHaveBeenLastCalledWith({ - x: 0, - y: 0, + x: (allocatedWidth - 400) / 2, + y: allocatedHeight - HUD_BAR_BOTTOM - stackHeight, width: 400, height: stackHeight, }); diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index d92ebe7f4..ca9dd076a 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -459,18 +459,18 @@ export function LaunchWindow() { const stackRect = anchorRect?.width && anchorRect.height ? anchorRect : barRect; const stackWidth = stackRect.width || barWidth; const stackHeight = stackRect.height || barHeight; - const currentContent = { - x: stackRect.x, - y: stackRect.y, + const contentFor = (size: { width: number; height: number }) => ({ + x: (size.width - stackWidth) / 2, + y: size.height - HUD_BAR_BOTTOM - stackHeight, width: stackWidth, height: stackHeight, - }; - const grantedContent = { - x: (granted.width - stackWidth) / 2, - y: granted.height - HUD_BAR_BOTTOM - stackHeight, - width: stackWidth, - height: stackHeight, - }; + }); + // Computed from the allocation too, never read off stackRect: right after a + // resize the observer fires while the old viewport is still laid out, and a + // measured position would hand the main process a rect from a window it has + // already replaced — the next flip then anchors on it and walks the bar (#951). + const currentContent = contentFor(allocated); + const grantedContent = contentFor(granted); if (!needsResize) { const last = lastSentHudContentRef.current;