From 26459716d4271849f3b2ccee47b90dbb3b61ac36 Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:09:52 +0500 Subject: [PATCH 1/9] fix(linux): dynamic X11 HUD sizing and click-through prevention - Dynamically resize HUD window on Linux X11 when popovers/menus open and close, preventing full-screen/center click blocking while collapsed. - Keep horizontal width reserve for vertical collapsed tray to avoid tooltip clipping. - Temporarily toggle window resizability around setBounds on X11 so window managers honor bounds changes with fixed WM_NORMAL_HINTS. --- electron/windows.ts | 35 +++++++++++++++++----- src/components/launch/LaunchWindow.tsx | 41 ++++++++++++++++++-------- 2 files changed, 55 insertions(+), 21 deletions(-) diff --git a/electron/windows.ts b/electron/windows.ts index 8db7d9cd4a..f895214526 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -165,6 +165,21 @@ function hudBounds(win: BrowserWindow): Electron.Rectangle { return { x, y, ...hudWindowSize }; } +/** + * Applies `setBounds` to the HUD window. On Linux/X11, window managers such as + * Mutter ignore programmatic size changes when `resizable: false` locks + * `WM_NORMAL_HINTS` min/max dimensions, so `resizable` is toggled around the call. + */ +function setHudWindowBounds(win: BrowserWindow, next: Electron.Rectangle) { + if (process.platform === "linux") { + win.setResizable(true); + win.setBounds(next, false); + win.setResizable(false); + } else { + win.setBounds(next, false); + } +} + // Origin the current drag gesture started from. The renderer sends the pointer's // *total* travel since pointerdown rather than per-frame deltas, so every move is // an absolute `origin + delta` — no rounding to accumulate, and a dropped message @@ -196,9 +211,11 @@ function hudWorkAreaFor(bounds: Electron.Rectangle): Electron.Rectangle { return screen.getDisplayMatching(reference).workArea; } -// Re-clamps a HUD nobody is dragging: a drag owns the position frame by frame, and the -// renderer re-measures on release. Skipped while minimized, where moving the window is -// platform-defined; "restore" runs it instead. +/** + * Re-clamps a HUD nobody is dragging: a drag owns the position frame by frame, and the + * renderer re-measures on release. Skipped while minimized, where moving the window is + * platform-defined; "restore" runs it instead. + */ function reclampHud() { const win = hudOverlayWindow; if (!win || win.isDestroyed() || win.isMinimized() || hudDragOrigin) return; @@ -217,7 +234,7 @@ function reclampHud() { }) : clampHudBoundsToWorkArea(bounds, hudContentRect, workArea); if (!sameRect(next, bounds)) { - win.setBounds(next, false); + setHudWindowBounds(win, next); } } @@ -435,7 +452,7 @@ ipcMain.on("hud-overlay-set-size", (_event, width: number, height: number, conte hudWindowSize = { width: next.width, height: next.height }; if (!sameRect(next, bounds)) { - hudOverlayWindow.setBounds(next, false); + setHudWindowBounds(hudOverlayWindow, next); } reassertAlwaysOnTop(hudOverlayWindow); }); @@ -460,9 +477,11 @@ export function createHudOverlayWindow(): BrowserWindow { // Close to what the renderer asks for on its very first measurement (bar plus // the reserved space above it), so the HUD doesn't visibly resize itself the - // instant it becomes visible. See src/components/launch/hudGeometry.ts. - const windowWidth = 820; - const windowHeight = 560; + // instant it becomes visible. On Linux the HUD is not click-through, so start + // at the compact bar+tooltip size until a popover opens. + const isLinux = process.platform === "linux"; + const windowWidth = isLinux ? 440 : 820; + const windowHeight = isLinux ? 190 : 560; const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2); const y = Math.floor(workArea.y + workArea.height - windowHeight - 5); diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 50df3e6450..1881972df1 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -397,7 +397,12 @@ export function LaunchWindow() { // screen.availHeight (which a window resize can't change) and are pushed // down as CSS custom properties. // --------------------------------------------------------------------------- - const hudAllocatedSizeRef = useRef({ width: 0, height: 0, orientation: trayLayout }); + const hudAllocatedSizeRef = useRef({ + width: 0, + height: 0, + orientation: trayLayout, + popoverOpen: isPopoverOpen, + }); // Last stack rect sent to the main process, so a measurement that changed nothing // costs no IPC. const lastSentHudContentRef = useRef<{ @@ -434,32 +439,41 @@ export function LaunchWindow() { ? noticeEl.getBoundingClientRect().height || noticeEl.scrollHeight : 0; - // The two floating surfaces get their own CSS caps (a 470px-tall language - // list would look absurd), but the *window* always reserves room for the - // taller of them. Sizing the reserve to whichever happens to be open would - // mean the window grows when the panel opens — and since the stack is - // bottom-anchored, growing upward moves every bit of content down in window - // coordinates, which the renderer repaints a frame or two after the native - // resize lands. That gap is visible as exactly the position judder this - // whole architecture exists to remove. So: reserve the maximum, always. const popoverMaxHeight = computeHudPopoverMaxHeight(barHeight, availableHeight); const modalMaxHeight = computeHudModalMaxHeight(barHeight, availableHeight); const anchorEl = hudAnchorRef.current; anchorEl?.style.setProperty("--hud-popover-max-h", `${popoverMaxHeight}px`); anchorEl?.style.setProperty("--hud-modal-max-h", `${modalMaxHeight}px`); - const { required, granted } = computeHudWindowSize({ + // On Linux, the HUD window is never click-through (setHudMouseEventsEnabled), + // so reserving 500+ px of empty height when no popover is open blocks clicks + // across the center of the desktop. Reserve only tooltip clearance (86 px -> + // ~190 px total height) while collapsed, and expand when a popover opens. + const isLinux = isLinuxHud || getPlatform() === "linux"; + const collapsedLinux = isLinux && !isPopoverOpen; + const computed = computeHudWindowSize({ barWidth, barHeight, noticeHeight, availableHeight, - stackMaxHeight: Math.max(popoverMaxHeight, modalMaxHeight), + stackMaxHeight: collapsedLinux ? 86 : Math.max(popoverMaxHeight, modalMaxHeight), }); + const required = computed.required; + const granted = collapsedLinux + ? { + width: + trayLayout === "horizontal" + ? Math.max(440, required.width) + : computed.granted.width, + height: Math.max(190, required.height), + } + : computed.granted; const allocated = hudAllocatedSizeRef.current; // A different orientation is a different shape entirely (wide-short vs // narrow-tall), so the previous allocation says nothing useful. - const orientationChanged = allocated.orientation !== trayLayout; + const orientationChanged = + allocated.orientation !== trayLayout || (isLinux && allocated.popoverOpen !== isPopoverOpen); // Grow the moment the content stops fitting. Shrink only once the content // has fallen a whole reserve below what was granted — that asymmetry is the // hysteresis: the bar can grow into its reserve (recording controls, a @@ -521,11 +535,12 @@ export function LaunchWindow() { } allocated.orientation = trayLayout; + allocated.popoverOpen = isPopoverOpen; allocated.width = granted.width; allocated.height = granted.height; lastSentHudContentRef.current = grantedContent; window.electronAPI.setHudOverlaySize(granted.width, granted.height, grantedContent); - }, [trayLayout]); + }, [trayLayout, isPopoverOpen, isLinuxHud]); // One persistent observer; elements wire themselves up via callback refs as // they mount/unmount. The bar and the notice column size the window; the anchor From 73a4d81ff32770e5697cc4e37e58a41b25797fad Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:30:56 +0500 Subject: [PATCH 2/9] fix(linux): use isLinuxHud directly and retain height reservation trade-off comment --- src/components/launch/LaunchWindow.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 1881972df1..8bac79c4ca 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -445,11 +445,14 @@ export function LaunchWindow() { anchorEl?.style.setProperty("--hud-popover-max-h", `${popoverMaxHeight}px`); anchorEl?.style.setProperty("--hud-modal-max-h", `${modalMaxHeight}px`); + // On non-Linux platforms, the window always reserves the maximum popover height + // to prevent bottom-anchored position judder when panels open/close. // On Linux, the HUD window is never click-through (setHudMouseEventsEnabled), - // so reserving 500+ px of empty height when no popover is open blocks clicks - // across the center of the desktop. Reserve only tooltip clearance (86 px -> - // ~190 px total height) while collapsed, and expand when a popover opens. - const isLinux = isLinuxHud || getPlatform() === "linux"; + // so reserving 500+ px of empty height blocks desktop mouse clicks across + // the center of the screen. We make the trade-off here: accept the transient + // resize jump when opening popovers so the collapsed bar stays compact + // (reserving only tooltip clearance: 86 px stack -> ~190 px total window height). + const isLinux = isLinuxHud; const collapsedLinux = isLinux && !isPopoverOpen; const computed = computeHudWindowSize({ barWidth, From 8f891c57b7e21681110ef834a439f01f558c15c2 Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 13:53:28 +0500 Subject: [PATCH 3/9] fix(linux): account for toasts in collapsed HUD height, reduce vertical width, and fix formatting --- src/components/launch/LaunchWindow.tsx | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 8bac79c4ca..624f3b31e7 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -402,6 +402,7 @@ export function LaunchWindow() { height: 0, orientation: trayLayout, popoverOpen: isPopoverOpen, + toastsHeight: 0, }); // Last stack rect sent to the main process, so a measurement that changed nothing // costs no IPC. @@ -451,7 +452,8 @@ export function LaunchWindow() { // so reserving 500+ px of empty height blocks desktop mouse clicks across // the center of the screen. We make the trade-off here: accept the transient // resize jump when opening popovers so the collapsed bar stays compact - // (reserving only tooltip clearance: 86 px stack -> ~190 px total window height). + // (reserving tooltip clearance: 86 px stack -> ~190 px total window height, + // growing for visible toasts and shrinking when they disappear). const isLinux = isLinuxHud; const collapsedLinux = isLinux && !isPopoverOpen; const computed = computeHudWindowSize({ @@ -459,15 +461,14 @@ export function LaunchWindow() { barHeight, noticeHeight, availableHeight, - stackMaxHeight: collapsedLinux ? 86 : Math.max(popoverMaxHeight, modalMaxHeight), + stackMaxHeight: collapsedLinux + ? Math.max(86, hudToastsHeightRef.current) + : Math.max(popoverMaxHeight, modalMaxHeight), }); const required = computed.required; const granted = collapsedLinux ? { - width: - trayLayout === "horizontal" - ? Math.max(440, required.width) - : computed.granted.width, + width: trayLayout === "horizontal" ? Math.max(440, required.width) : required.width, height: Math.max(190, required.height), } : computed.granted; @@ -476,7 +477,9 @@ export function LaunchWindow() { // A different orientation is a different shape entirely (wide-short vs // narrow-tall), so the previous allocation says nothing useful. const orientationChanged = - allocated.orientation !== trayLayout || (isLinux && allocated.popoverOpen !== isPopoverOpen); + allocated.orientation !== trayLayout || + (isLinux && allocated.popoverOpen !== isPopoverOpen) || + (collapsedLinux && allocated.toastsHeight !== hudToastsHeightRef.current); // Grow the moment the content stops fitting. Shrink only once the content // has fallen a whole reserve below what was granted — that asymmetry is the // hysteresis: the bar can grow into its reserve (recording controls, a @@ -539,6 +542,7 @@ export function LaunchWindow() { allocated.orientation = trayLayout; allocated.popoverOpen = isPopoverOpen; + allocated.toastsHeight = hudToastsHeightRef.current; allocated.width = granted.width; allocated.height = granted.height; lastSentHudContentRef.current = grantedContent; From 7e4bbb150f4f14967073bbf64852056248595f14 Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:00:58 +0500 Subject: [PATCH 4/9] fix(linux): provide sufficient width (600px) for vertical-tray tooltips --- src/components/launch/LaunchWindow.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 624f3b31e7..1576250a4d 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -468,7 +468,10 @@ export function LaunchWindow() { const required = computed.required; const granted = collapsedLinux ? { - width: trayLayout === "horizontal" ? Math.max(440, required.width) : required.width, + width: + trayLayout === "horizontal" + ? Math.max(440, required.width) + : Math.max(600, required.width), height: Math.max(190, required.height), } : computed.granted; From b66c3fdee679ff77200c00a6f85937159ae706cf Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:26:22 +0500 Subject: [PATCH 5/9] style: compact tooltip bubble width (170px) tightly hugging multi-line text --- src/components/ui/tooltip.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ui/tooltip.tsx b/src/components/ui/tooltip.tsx index 6defa0101b..1a19737007 100644 --- a/src/components/ui/tooltip.tsx +++ b/src/components/ui/tooltip.tsx @@ -67,9 +67,9 @@ function TooltipContent({ sideOffset={sideOffset} className={cn( // Popper sizes its wrapper to `max-content`, so without a cap a long tooltip is one - // long line. 260px is about 40 characters a line at this size. `text-balance` evens the - // lines out, so a translation does not end on one orphan word. - "max-w-[260px] break-words text-balance px-2.5 py-1.5 text-xs leading-4 font-medium text-white/95 bg-black/85 border border-white/10 rounded-lg shadow-lg z-50", + // long line. 170px provides a compact multi-line bubble that fits beside the vertical bar. + // `w-fit` ensures the bubble tightly hugs the text width, while `text-balance` evens the lines. + "w-fit max-w-[170px] break-words text-balance px-2.5 py-1.5 text-xs leading-4 font-medium text-white/95 bg-black/85 border border-white/10 rounded-lg shadow-lg z-50", "animate-in fade-in-0 zoom-in-95", "data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95", className, From af9b8f49fa3d5871091801711cd1c7702fbe31e2 Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 15:41:34 +0500 Subject: [PATCH 6/9] fix(linux): initialize isLinuxHud synchronously from getPlatform to avoid large reserve on first frame --- src/components/launch/LaunchWindow.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 1576250a4d..e1db2d573b 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -158,7 +158,7 @@ export function LaunchWindow() { // False until the platform, and on macOS and Linux the native helper, have answered: the // cursor toggle is only shown once it is known to change something. const [supportsCursorModeToggle, setSupportsCursorModeToggle] = useState(false); - const [isLinuxHud, setIsLinuxHud] = useState(false); + const [isLinuxHud, setIsLinuxHud] = useState(() => getPlatform() === "linux"); // The running version, and whether this copy may offer an update check at all — a // Store/Flathub/Snap/Nix install is kept current by its package manager and is offered // nothing (electron/install-channel.ts). Asked once: neither answer changes while the app From d5b70412fb285a16778ad00d326c86781fc81e5b Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 17:15:23 +0500 Subject: [PATCH 7/9] fix(linux): stabilize horizontal HUD width, hug wrapped tooltips, and flip near screen edge --- electron/windows.ts | 2 +- src/components/launch/HudControls.tsx | 17 +++++++++-- src/components/launch/LaunchWindow.tsx | 4 +-- src/components/launch/hudGeometry.ts | 13 ++++++--- src/components/ui/tooltip.tsx | 39 ++++++++++++++++++++++++-- 5 files changed, 62 insertions(+), 13 deletions(-) diff --git a/electron/windows.ts b/electron/windows.ts index f895214526..5113840f5b 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -480,7 +480,7 @@ export function createHudOverlayWindow(): BrowserWindow { // instant it becomes visible. On Linux the HUD is not click-through, so start // at the compact bar+tooltip size until a popover opens. const isLinux = process.platform === "linux"; - const windowWidth = isLinux ? 440 : 820; + const windowWidth = isLinux ? 760 : 820; const windowHeight = isLinux ? 190 : 560; const x = Math.floor(workArea.x + (workArea.width - windowWidth) / 2); diff --git a/src/components/launch/HudControls.tsx b/src/components/launch/HudControls.tsx index c417b754c2..37082c5919 100644 --- a/src/components/launch/HudControls.tsx +++ b/src/components/launch/HudControls.tsx @@ -60,20 +60,31 @@ const HUD_TOOLTIP_EDGE_PADDING = 8; // right in the render that shows it, not one effect later) and added to the primitive's gap. function HudTooltip({ content, children }: { content: string; children: ReactElement }) { const vertical = useContext(HudVerticalContext); - const side = vertical ? "right" : "top"; const triggerRef = useRef(null); const [open, setOpen] = useState(false); const [clearance, setClearance] = useState(0); + const [side, setSide] = useState<"top" | "right" | "left">(vertical ? "right" : "top"); const handleOpenChange = (next: boolean) => { const trigger = triggerRef.current; const bar = trigger?.closest("[data-tray-layout]"); if (next && trigger && bar) { + let currentSide: "top" | "right" | "left" = "top"; + if (vertical) { + const barRect = bar.getBoundingClientRect(); + const screenLeft = (window.screenX || window.screenLeft || 0) + barRect.right; + const screen = window.screen as Screen & { availLeft?: number }; + const screenRight = (screen.availLeft ?? 0) + (screen.availWidth || screen.width || 1920); + const requiredRoom = 260 + TOOLTIP_GAP_PX + HUD_TOOLTIP_EDGE_PADDING; + // When HUD is near the right edge of its display, flip tooltip to the left of the bar + currentSide = screenLeft + requiredRoom > screenRight ? "left" : "right"; + } + setSide(currentSide); setClearance( computeHudTooltipClearance( trigger.getBoundingClientRect(), bar.getBoundingClientRect(), - side, + currentSide, ), ); } @@ -84,7 +95,7 @@ function HudTooltip({ content, children }: { content: string; children: ReactEle