diff --git a/electron/electron-env.d.ts b/electron/electron-env.d.ts index c98cad95a..7fd4fd69d 100644 --- a/electron/electron-env.d.ts +++ b/electron/electron-env.d.ts @@ -231,6 +231,7 @@ interface Window { getHudOverlayMousePassthroughSupported: () => Promise<{ success: boolean; supported: boolean; + nativeDrag?: boolean; }>; setHudOverlayCaptureProtection: ( enabled: boolean, diff --git a/electron/gpuSwitches.test.ts b/electron/gpuSwitches.test.ts index 4cbb1cdf4..694338555 100644 --- a/electron/gpuSwitches.test.ts +++ b/electron/gpuSwitches.test.ts @@ -1,66 +1,111 @@ import { describe, expect, it } from "vitest"; -import { getGpuSwitches, shouldForceLinuxEgl } from "./gpuSwitches"; +import { getGpuSwitches, isLinuxX11Session } from "./gpuSwitches"; -describe("shouldForceLinuxEgl", () => { - it("does not force EGL in a Wayland session", () => { +describe("isLinuxX11Session", () => { + it("is false in a Wayland session", () => { expect( - shouldForceLinuxEgl({ + isLinuxX11Session({ XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0", }), ).toBe(false); }); - it("does not force EGL when Wayland is explicitly requested via Ozone", () => { + it("is false when Wayland is explicitly requested via Ozone", () => { expect( - shouldForceLinuxEgl({ + isLinuxX11Session({ OZONE_PLATFORM: "wayland", XDG_SESSION_TYPE: "x11", }), ).toBe(false); }); - it("falls back to Electron's ozone hint when OZONE_PLATFORM is invalid", () => { + it("is true when launched with --ozone-platform=x11 from a Wayland session", () => { expect( - shouldForceLinuxEgl({ - OZONE_PLATFORM: "auto", - ELECTRON_OZONE_PLATFORM_HINT: "wayland", - XDG_SESSION_TYPE: "x11", - }), + isLinuxX11Session({ XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0" }, [ + "/opt/Recordly/recordly", + "--ozone-platform=x11", + ]), + ).toBe(true); + }); + + it("is false when launched with --ozone-platform=wayland from an X11 session", () => { + expect( + isLinuxX11Session({ XDG_SESSION_TYPE: "x11" }, [ + "recordly", + "--ozone-platform=wayland", + ]), ).toBe(false); }); - it("forces EGL in an X11 session", () => { - expect(shouldForceLinuxEgl({ XDG_SESSION_TYPE: "x11" })).toBe(true); + it("uses the last --ozone-platform switch when repeated", () => { + expect( + isLinuxX11Session({ XDG_SESSION_TYPE: "wayland" }, [ + "recordly", + "--ozone-platform=wayland", + "--ozone-platform=x11", + ]), + ).toBe(true); + }); + + it("prefers --ozone-platform over --ozone-platform-hint", () => { + expect( + isLinuxX11Session({ XDG_SESSION_TYPE: "x11" }, [ + "recordly", + "--ozone-platform-hint=x11", + "--ozone-platform=wayland", + ]), + ).toBe(false); }); - it("forces EGL when x11 is explicitly requested via Electron's ozone hint", () => { + it("honors --ozone-platform-hint when --ozone-platform is absent", () => { expect( - shouldForceLinuxEgl({ + isLinuxX11Session({ WAYLAND_DISPLAY: "wayland-0" }, [ + "recordly", + "--ozone-platform-hint=x11", + ]), + ).toBe(true); + }); + + it("falls back to the session when the ozone switch is auto", () => { + expect( + isLinuxX11Session({ XDG_SESSION_TYPE: "wayland" }, [ + "recordly", + "--ozone-platform-hint=auto", + ]), + ).toBe(false); + }); + + it("is true in an X11 session", () => { + expect(isLinuxX11Session({ XDG_SESSION_TYPE: "x11" })).toBe(true); + }); + + it("ignores ELECTRON_OZONE_PLATFORM_HINT, which Electron 43 no longer honors", () => { + expect( + isLinuxX11Session({ ELECTRON_OZONE_PLATFORM_HINT: "x11", + XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0", }), - ).toBe(true); + ).toBe(false); }); }); describe("getGpuSwitches", () => { - it("returns the Linux VAAPI workaround without forcing EGL on Wayland", () => { + it("returns the Linux VAAPI workaround on Wayland", () => { expect( getGpuSwitches("linux", { XDG_SESSION_TYPE: "wayland", WAYLAND_DISPLAY: "wayland-0", }), ).toEqual({ - useGl: undefined, disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], }); }); - it("returns the X11 EGL workaround on Linux X11", () => { + it("does not force EGL on Linux X11", () => { expect(getGpuSwitches("linux", { XDG_SESSION_TYPE: "x11" })).toEqual({ - useGl: "egl", disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], }); }); diff --git a/electron/gpuSwitches.ts b/electron/gpuSwitches.ts index 7b7c81ee8..5dc03ba2e 100644 --- a/electron/gpuSwitches.ts +++ b/electron/gpuSwitches.ts @@ -13,15 +13,33 @@ function normalizeLinuxWindowSystem(value: string | undefined): "wayland" | "x11 return null; } -function getForcedLinuxWindowSystem(env: NodeJS.ProcessEnv): "wayland" | "x11" | null { +function getOzoneSwitchValue(argv: readonly string[], name: string): string | undefined { + const prefix = `--${name}=`; + // Chromium uses the last occurrence of a repeated switch. + for (let i = argv.length - 1; i >= 0; i--) { + if (argv[i].startsWith(prefix)) { + return argv[i].slice(prefix.length); + } + } + return undefined; +} + +function getForcedLinuxWindowSystem( + env: NodeJS.ProcessEnv, + argv: readonly string[], +): "wayland" | "x11" | null { + // The --ozone-platform switch picks the backend Chromium actually uses, e.g. + // XWayland when launched with --ozone-platform=x11 from a Wayland session. + // ELECTRON_OZONE_PLATFORM_HINT is ignored by Electron 43, so it is not read. return ( - normalizeLinuxWindowSystem(env.OZONE_PLATFORM) ?? - normalizeLinuxWindowSystem(env.ELECTRON_OZONE_PLATFORM_HINT) + normalizeLinuxWindowSystem(getOzoneSwitchValue(argv, "ozone-platform")) ?? + normalizeLinuxWindowSystem(getOzoneSwitchValue(argv, "ozone-platform-hint")) ?? + normalizeLinuxWindowSystem(env.OZONE_PLATFORM) ); } -export function shouldForceLinuxEgl(env: NodeJS.ProcessEnv): boolean { - const forcedWindowSystem = getForcedLinuxWindowSystem(env); +export function isLinuxX11Session(env: NodeJS.ProcessEnv, argv: readonly string[] = []): boolean { + const forcedWindowSystem = getForcedLinuxWindowSystem(env, argv); if (forcedWindowSystem === "wayland") { return false; } @@ -42,7 +60,7 @@ export function shouldForceLinuxEgl(env: NodeJS.ProcessEnv): boolean { export function getGpuSwitches( platform: NodeJS.Platform, - env: NodeJS.ProcessEnv = process.env, + _env: NodeJS.ProcessEnv = process.env, ): GpuSwitches { if (platform === "darwin") { return { @@ -56,8 +74,9 @@ export function getGpuSwitches( } if (platform === "linux") { + // Do not force --use-gl=egl on X11: Electron 43 only allows ANGLE, so the + // GPU process exits and WebGL/WebGPU become unavailable to the editor. return { - useGl: shouldForceLinuxEgl(env) ? "egl" : undefined, disableFeatures: ["VaapiVideoDecoder", "VaapiVideoEncoder"], }; } diff --git a/electron/windows.ts b/electron/windows.ts index 745d6caa4..01ec6eda4 100644 --- a/electron/windows.ts +++ b/electron/windows.ts @@ -11,6 +11,7 @@ import { shouldProtectHudCapture, } from "../src/lib/hudCaptureProtection"; import { USER_DATA_PATH } from "./appPaths"; +import { isLinuxX11Session } from "./gpuSwitches"; import { getHudOverlayWindowBounds, resizeHudOverlayFallbackBounds, @@ -142,6 +143,14 @@ export function isHudOverlayMousePassthroughSupported(): boolean { return process.platform !== "linux"; } +// Wayland forbids client-side window placement, so the HUD must be dragged by +// the compositor via -webkit-app-region. On X11 the HUD is non-focusable, which +// makes it override-redirect: the window manager ignores app-region drags, but +// setBounds() works, so the drag IPC moves the window instead. +export function isHudOverlayNativeDragRequired(): boolean { + return process.platform === "linux" && !isLinuxX11Session(process.env, process.argv); +} + function loadHudOverlayCaptureProtectionSetting(): boolean { if (hudOverlayCaptureProtectionLoaded) { return hudOverlayHiddenFromCapture; @@ -253,7 +262,21 @@ function applyHudOverlayBounds() { if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) { return; } - hudOverlayWindow.setBounds(getHudOverlayBounds(), false); + // Keep a user-dragged fallback HUD where it was placed instead of snapping + // it back to the bottom-center of the display. + const nextBounds = + hudUserPosition && !isHudOverlayMousePassthroughSupported() + ? resizeHudOverlayFallbackBounds( + getHudOverlayDisplay().workArea, + hudOverlayWindow.getBounds(), + shouldExpandHudOverlayFallback({ + fallbackExpanded: hudOverlayFallbackExpanded, + recordingActive: hudOverlayRecordingActive, + webcamPreviewVisible: hudOverlayWebcamPreviewVisible, + }), + ) + : getHudOverlayBounds(); + hudOverlayWindow.setBounds(nextBounds, false); positionUpdateToastWindow(); if (!hudOverlayWindow.isVisible()) { @@ -386,13 +409,12 @@ let hudDragFixedSize: { width: number; height: number } | null = null; ipcMain.on("hud-overlay-drag", (_event, phase: string, screenX: number, screenY: number) => { if (!hudOverlayWindow || hudOverlayWindow.isDestroyed()) return; - // On Linux the compositor (especially Wayland) refuses programmatic window - // placement, so BrowserWindow.setBounds() with x/y is silently ignored and - // the HUD appears "stuck". The renderer marks the drag handle as - // -webkit-app-region: drag on Linux, letting the OS move the window for us. - // The resulting position is captured by the win.on("moved", ...) listener - // below so `hudUserPosition` stays in sync. - if (process.platform === "linux") { + // On Wayland the compositor refuses programmatic window placement, so + // BrowserWindow.setBounds() with x/y is silently ignored. The renderer marks + // the drag handle as -webkit-app-region: drag there, letting the compositor + // move the window. The resulting position is captured by the + // win.on("moved", ...) listener below so `hudUserPosition` stays in sync. + if (isHudOverlayNativeDragRequired()) { return; } @@ -453,6 +475,7 @@ ipcMain.handle("get-hud-overlay-mouse-passthrough-supported", () => { return { success: true, supported: isHudOverlayMousePassthroughSupported(), + nativeDrag: isHudOverlayNativeDragRequired(), }; }); @@ -642,8 +665,8 @@ export function createHudOverlayWindow(): BrowserWindow { hudOverlayWindow = win; - // On Linux the HUD is dragged by the OS via -webkit-app-region (Wayland - // forbids client-side positioning). Mirror moved bounds into drag state. + // On Wayland the HUD is dragged by the compositor via -webkit-app-region + // (client-side positioning is forbidden). Mirror moved bounds into drag state. if (process.platform === "linux") { win.on("moved", () => { if (win.isDestroyed()) return; diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index f41c6d07c..920535522 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -96,7 +96,7 @@ function LaunchWindowContent() { setSelectedDeviceId: setSelectedVideoDeviceId, } = useVideoDevices(webcamEnabled || openId === "webcam"); - const { hudOverlayMousePassthroughSupported, platform } = + const { hudOverlayMousePassthroughSupported, hudOverlayNativeDragRequired } = useLaunchWindowSystemState(preparePermissions); useEffect(() => { @@ -160,6 +160,9 @@ function LaunchWindowContent() { hudContentRef, hudBarRef, recordingWebcamPreviewContainerRef, + // A fixed-size fallback HUD window has no room to translate the bar in, + // so move the window itself via the main process. + moveHudWindow: hudOverlayMousePassthroughSupported === false, }); const { handleHudMouseEnter, handleHudMouseLeave, beginInteractiveHudAction } = @@ -441,8 +444,7 @@ function LaunchWindowContent() { ); const hudMode = finalizing ? "finalizing" : recording ? "recording" : "idle"; - const useNativeHudBarDrag = - platform === "linux" || hudOverlayMousePassthroughSupported === false; + const useNativeHudBarDrag = hudOverlayNativeDragRequired; const shouldAnimateHudLayout = !recording && !showRecordingWebcamPreview && !isHudDragging; return ( @@ -473,9 +475,8 @@ function LaunchWindowContent() { onMouseLeave={handleHudMouseLeave} >