From 23afa4c41817bef4a5d20e9b93eab56ddf3e94d6 Mon Sep 17 00:00:00 2001 From: Rover <110387638+rover-001@users.noreply.github.com> Date: Fri, 4 Sep 2026 11:36:31 +0530 Subject: [PATCH 1/3] Fix Linux Wayland HUD oscillation on hover and popover menu clipping - Expand HUD window only while popover menus are active, preventing hover oscillation loops under Wayland compositors (e.g. Hyprland) that re-center resizing floating windows - Calculate dynamic bottom padding when resize anchor is centered so the HUD bar remains stationary on screen during window expansion - Increase NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP from 540 to 680 to prevent tall popover dropdowns (More, Mic, Cam) from clipping at the top - Update unit tests for 680px bounds and getHudOverlayResizeAnchor - Document Hyprland/Omarchy window rules in README.md --- README.md | 31 +++++++++++++++ electron/electron-env.d.ts | 2 + electron/hudOverlayBounds.test.ts | 39 +++++++++++++++---- electron/hudOverlayBounds.ts | 17 +++++++- electron/preload.ts | 3 ++ electron/windows.ts | 10 +++++ src/components/launch/LaunchWindow.tsx | 9 ++++- .../hooks/useLaunchHudInteractionState.ts | 1 + .../hooks/useLaunchWindowSystemState.ts | 5 +++ 9 files changed, 106 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 121ef9343..946419fca 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,37 @@ Target-specific build commands are also available: --- +## Linux: Hyprland / Omarchy + +Recordly's recording HUD, countdown, and source picker are transparent floating windows. +Hyprland decorates them like any other window, so the compositor's blur, shadow, dim, and +opacity rules show up as a grey box around the HUD. Wayland also ignores `alwaysOnTop`, so +the HUD can end up behind other windows or stuck on one workspace. + +Add these rules to `~/.config/hypr/hyprland.lua` (Omarchy) and run `hyprctl reload`: + +```lua +o.window("^[Rr]ecordly$", { tag = "-default-opacity", opacity = "1 1" }) +o.window({ class = "^[Rr]ecordly$", float = true }, { + pin = true, + no_blur = true, + no_shadow = true, + border_size = 0, + no_dim = true, +}) +``` + +Plain `hyprland.conf` equivalent: + +```ini +windowrule = opacity 1 1, class:^[Rr]ecordly$ +windowrule = pin, class:^[Rr]ecordly$, floating:1 +windowrule = noblur, class:^[Rr]ecordly$, floating:1 +windowrule = noshadow, class:^[Rr]ecordly$, floating:1 +windowrule = nodim, class:^[Rr]ecordly$, floating:1 +windowrule = bordersize 0, class:^[Rr]ecordly$, floating:1 +``` + ## macOS: "App cannot be opened" Locally built apps may be quarantined by macOS. diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index c00ef3226..87e708bda 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -198,6 +198,7 @@ interface RendererNativeExportCapabilities { interface Window { electronAPI: { hudOverlaySetIgnoreMouse: (ignore: boolean) => void; + hudOverlaySetMenuOpen: (open: boolean) => void; hudOverlaySetSourceSelectionActive: (active: boolean) => void; hudOverlayDrag: (phase: "start" | "move" | "end", screenX: number, screenY: number) => void; hudOverlayHide: () => void; @@ -208,6 +209,7 @@ interface Window { getHudOverlayMousePassthroughSupported: () => Promise<{ success: boolean; supported: boolean; + resizeAnchor?: "bottom" | "center"; }>; setHudOverlayCaptureProtection: ( enabled: boolean, diff --git a/electron/hudOverlayBounds.test.ts b/electron/hudOverlayBounds.test.ts index db21e92bd..d08b59b0a 100644 --- a/electron/hudOverlayBounds.test.ts +++ b/electron/hudOverlayBounds.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "vitest"; import { + getHudOverlayResizeAnchor, getHudOverlayWindowBounds, resizeHudOverlayFallbackBounds, shouldExpandHudOverlayFallback, @@ -30,9 +31,9 @@ describe("getHudOverlayWindowBounds", () => { it("expands the non-passthrough fallback for HUD menus and hover interaction", () => { expect(getHudOverlayWindowBounds(workArea, false, true)).toEqual({ x: 650, - y: 540, + y: 400, width: 860, - height: 540, + height: 680, }); }); @@ -98,9 +99,9 @@ describe("resizeHudOverlayFallbackBounds", () => { ), ).toEqual({ x: 420, - y: 320, + y: 180, width: 860, - height: 540, + height: 680, }); }); @@ -110,9 +111,9 @@ describe("resizeHudOverlayFallbackBounds", () => { workArea, { x: 420, - y: 320, + y: 180, width: 860, - height: 540, + height: 680, }, false, ), @@ -138,13 +139,35 @@ describe("resizeHudOverlayFallbackBounds", () => { ), ).toEqual({ x: 1060, - y: 520, + y: 380, width: 860, - height: 540, + height: 680, }); }); }); +describe("getHudOverlayResizeAnchor", () => { + it("returns bottom on non-linux platforms", () => { + expect(getHudOverlayResizeAnchor("darwin", {})).toBe("bottom"); + expect(getHudOverlayResizeAnchor("win32", {})).toBe("bottom"); + }); + + it("returns center on linux under wayland", () => { + expect( + getHudOverlayResizeAnchor("linux", { XDG_SESSION_TYPE: "wayland" }), + ).toBe("center"); + expect( + getHudOverlayResizeAnchor("linux", { WAYLAND_DISPLAY: "wayland-1" }), + ).toBe("center"); + }); + + it("returns bottom on linux under x11", () => { + expect( + getHudOverlayResizeAnchor("linux", { XDG_SESSION_TYPE: "x11" }), + ).toBe("bottom"); + }); +}); + describe("shouldExpandHudOverlayFallback", () => { it("expands while recording only when the floating webcam preview is visible", () => { expect( diff --git a/electron/hudOverlayBounds.ts b/electron/hudOverlayBounds.ts index 8c51b88c7..0f24ca8ea 100644 --- a/electron/hudOverlayBounds.ts +++ b/electron/hudOverlayBounds.ts @@ -7,12 +7,27 @@ export interface HudOverlayWorkArea { const NON_PASSTHROUGH_HUD_WIDTH_DIP = 860; const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 160; -const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 540; +const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 680; function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } +export type HudOverlayResizeAnchor = "bottom" | "center"; + +// Wayland refuses client-side window placement, so the compositor decides where +// a resized HUD lands. Hyprland keeps floating windows centered while X11 honors +// the bottom-anchored bounds this module computes. +export function getHudOverlayResizeAnchor( + platform: NodeJS.Platform, + env: NodeJS.ProcessEnv, +): HudOverlayResizeAnchor { + if (platform !== "linux") { + return "bottom"; + } + return env.XDG_SESSION_TYPE === "wayland" || env.WAYLAND_DISPLAY ? "center" : "bottom"; +} + export function getHudOverlayWindowBounds( workArea: HudOverlayWorkArea, mousePassthroughSupported: boolean, diff --git a/electron/preload.ts b/electron/preload.ts index a30372314..fced9567d 100644 --- a/electron/preload.ts +++ b/electron/preload.ts @@ -167,6 +167,9 @@ contextBridge.exposeInMainWorld("electronAPI", { hudOverlaySetIgnoreMouse: (ignore: boolean) => { ipcRenderer.send("hud-overlay-set-ignore-mouse", ignore); }, + hudOverlaySetMenuOpen: (open: boolean) => { + ipcRenderer.send("hud-overlay-set-menu-open", open); + }, hudOverlaySetSourceSelectionActive: (active: boolean) => { ipcRenderer.send("hud-overlay-set-source-selection-active", active); }, diff --git a/electron/windows.ts b/electron/windows.ts index 47e90b8fc..0083275f5 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -6,6 +6,7 @@ import { app, BrowserWindow, ipcMain } from "electron"; import { supportsHudCaptureProtection } from "../src/lib/hudCaptureProtection"; import { USER_DATA_PATH } from "./appPaths"; import { + getHudOverlayResizeAnchor, getHudOverlayWindowBounds, resizeHudOverlayFallbackBounds, shouldExpandHudOverlayFallback, @@ -331,6 +332,14 @@ ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => { setHudOverlayMousePassthrough(Boolean(ignore)); }); +// Linux has no hover-driven passthrough, so the compact HUD window only grows +// while a menu is open; growing on hover moves the bar out from under the pointer. +ipcMain.on("hud-overlay-set-menu-open", (_event, open: boolean) => { + if (process.platform === "linux") { + setHudOverlayFallbackExpanded(Boolean(open)); + } +}); + ipcMain.on("hud-overlay-set-source-selection-active", (_event, active: boolean) => { hudOverlaySourceSelectionActive = Boolean(active); if (hudOverlaySourceSelectionActive) { @@ -418,6 +427,7 @@ ipcMain.handle("get-hud-overlay-mouse-passthrough-supported", () => { return { success: true, supported: isHudOverlayMousePassthroughSupported(), + resizeAnchor: getHudOverlayResizeAnchor(process.platform, process.env), }; }); diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f7583b28e..add1cddd2 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -109,6 +109,7 @@ function LaunchWindowContent() { const { hudOverlayMousePassthroughSupported, + hudOverlayResizeAnchor, platform, appVersion, hideHudFromCapture, @@ -446,8 +447,12 @@ function LaunchWindowContent() { value={{ onMouseEnter: handleHudMouseEnter, onMouseLeave: handleHudMouseLeave }} >