mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 23:05:49 +00:00
Merge pull request #584 from webadderallorg/fix/windows-recorder-hud-passthrough
fix(recording): compact HUD fallback on Windows 10
This commit is contained in:
@@ -0,0 +1,145 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import {
|
||||
getHudOverlayWindowBounds,
|
||||
resizeHudOverlayFallbackBounds,
|
||||
} from "./hudOverlayBounds";
|
||||
|
||||
describe("getHudOverlayWindowBounds", () => {
|
||||
const workArea = {
|
||||
x: 120,
|
||||
y: 40,
|
||||
width: 1920,
|
||||
height: 1040,
|
||||
};
|
||||
|
||||
it("uses the full work area when mouse passthrough is supported", () => {
|
||||
expect(getHudOverlayWindowBounds(workArea, true)).toEqual(workArea);
|
||||
});
|
||||
|
||||
it("uses a bottom-centered compact fallback when mouse passthrough is unavailable", () => {
|
||||
expect(getHudOverlayWindowBounds(workArea, false)).toEqual({
|
||||
x: 650,
|
||||
y: 920,
|
||||
width: 860,
|
||||
height: 160,
|
||||
});
|
||||
});
|
||||
|
||||
it("expands the non-passthrough fallback for HUD menus and hover interaction", () => {
|
||||
expect(getHudOverlayWindowBounds(workArea, false, true)).toEqual({
|
||||
x: 650,
|
||||
y: 540,
|
||||
width: 860,
|
||||
height: 540,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps the compact fallback inside small displays", () => {
|
||||
expect(
|
||||
getHudOverlayWindowBounds(
|
||||
{
|
||||
x: -100,
|
||||
y: 20,
|
||||
width: 640,
|
||||
height: 420,
|
||||
},
|
||||
false,
|
||||
),
|
||||
).toEqual({
|
||||
x: -100,
|
||||
y: 280,
|
||||
width: 640,
|
||||
height: 160,
|
||||
});
|
||||
});
|
||||
|
||||
it("fits the expanded fallback inside small displays", () => {
|
||||
expect(
|
||||
getHudOverlayWindowBounds(
|
||||
{
|
||||
x: -100,
|
||||
y: 20,
|
||||
width: 640,
|
||||
height: 420,
|
||||
},
|
||||
false,
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
x: -100,
|
||||
y: 20,
|
||||
width: 640,
|
||||
height: 420,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("resizeHudOverlayFallbackBounds", () => {
|
||||
const workArea = {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
};
|
||||
|
||||
it("preserves the dragged bottom edge when expanding", () => {
|
||||
expect(
|
||||
resizeHudOverlayFallbackBounds(
|
||||
workArea,
|
||||
{
|
||||
x: 420,
|
||||
y: 700,
|
||||
width: 860,
|
||||
height: 160,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
x: 420,
|
||||
y: 320,
|
||||
width: 860,
|
||||
height: 540,
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the dragged bottom edge when compacting", () => {
|
||||
expect(
|
||||
resizeHudOverlayFallbackBounds(
|
||||
workArea,
|
||||
{
|
||||
x: 420,
|
||||
y: 320,
|
||||
width: 860,
|
||||
height: 540,
|
||||
},
|
||||
false,
|
||||
),
|
||||
).toEqual({
|
||||
x: 420,
|
||||
y: 700,
|
||||
width: 860,
|
||||
height: 160,
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps resized fallback bounds inside the display work area", () => {
|
||||
expect(
|
||||
resizeHudOverlayFallbackBounds(
|
||||
workArea,
|
||||
{
|
||||
x: 1500,
|
||||
y: 900,
|
||||
width: 860,
|
||||
height: 160,
|
||||
},
|
||||
true,
|
||||
),
|
||||
).toEqual({
|
||||
x: 1060,
|
||||
y: 520,
|
||||
width: 860,
|
||||
height: 540,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
export interface HudOverlayWorkArea {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
const NON_PASSTHROUGH_HUD_WIDTH_DIP = 860;
|
||||
const NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP = 160;
|
||||
const NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP = 540;
|
||||
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.min(Math.max(value, min), max);
|
||||
}
|
||||
|
||||
export function getHudOverlayWindowBounds(
|
||||
workArea: HudOverlayWorkArea,
|
||||
mousePassthroughSupported: boolean,
|
||||
fallbackExpanded = false,
|
||||
): HudOverlayWorkArea {
|
||||
if (mousePassthroughSupported) {
|
||||
return { ...workArea };
|
||||
}
|
||||
|
||||
const width = Math.min(workArea.width, NON_PASSTHROUGH_HUD_WIDTH_DIP);
|
||||
const height = Math.min(
|
||||
workArea.height,
|
||||
fallbackExpanded
|
||||
? NON_PASSTHROUGH_HUD_EXPANDED_HEIGHT_DIP
|
||||
: NON_PASSTHROUGH_HUD_COMPACT_HEIGHT_DIP,
|
||||
);
|
||||
|
||||
return {
|
||||
x: Math.round(workArea.x + (workArea.width - width) / 2),
|
||||
y: Math.round(workArea.y + workArea.height - height),
|
||||
width,
|
||||
height,
|
||||
};
|
||||
}
|
||||
|
||||
export function resizeHudOverlayFallbackBounds(
|
||||
workArea: HudOverlayWorkArea,
|
||||
currentBounds: HudOverlayWorkArea,
|
||||
fallbackExpanded: boolean,
|
||||
): HudOverlayWorkArea {
|
||||
const nextBounds = getHudOverlayWindowBounds(workArea, false, fallbackExpanded);
|
||||
const maxX = workArea.x + workArea.width - nextBounds.width;
|
||||
const maxY = workArea.y + workArea.height - nextBounds.height;
|
||||
|
||||
return {
|
||||
...nextBounds,
|
||||
x: clamp(currentBounds.x, workArea.x, maxX),
|
||||
y: clamp(currentBounds.y + currentBounds.height - nextBounds.height, workArea.y, maxY),
|
||||
};
|
||||
}
|
||||
+35
-6
@@ -5,6 +5,10 @@ import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { app, BrowserWindow, ipcMain } from "electron";
|
||||
import { USER_DATA_PATH } from "./appPaths";
|
||||
import {
|
||||
getHudOverlayWindowBounds,
|
||||
resizeHudOverlayFallbackBounds,
|
||||
} from "./hudOverlayBounds";
|
||||
import { getPackagedRendererBaseUrl } from "./rendererServer";
|
||||
|
||||
const electronWindowsDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
@@ -23,6 +27,7 @@ const WINDOW_ICON_PATH = path.join(
|
||||
let hudOverlayWindow: BrowserWindow | null = null;
|
||||
let hudOverlayHiddenFromCapture = true;
|
||||
let hudOverlayCaptureProtectionLoaded = false;
|
||||
let hudOverlayFallbackExpanded = false;
|
||||
let countdownWindow: BrowserWindow | null = null;
|
||||
let updateToastWindow: BrowserWindow | null = null;
|
||||
|
||||
@@ -183,12 +188,11 @@ function getHudOverlayDisplay() {
|
||||
|
||||
function getHudOverlayBounds() {
|
||||
const { workArea } = getHudOverlayDisplay();
|
||||
return {
|
||||
x: workArea.x,
|
||||
y: workArea.y,
|
||||
width: workArea.width,
|
||||
height: workArea.height,
|
||||
};
|
||||
return getHudOverlayWindowBounds(
|
||||
workArea,
|
||||
isHudOverlayMousePassthroughSupported(),
|
||||
hudOverlayFallbackExpanded,
|
||||
);
|
||||
}
|
||||
|
||||
function applyHudOverlayBounds() {
|
||||
@@ -242,9 +246,33 @@ function positionUpdateToastWindow() {
|
||||
updateToastWindow.moveTop();
|
||||
}
|
||||
|
||||
function setHudOverlayFallbackExpanded(expanded: boolean) {
|
||||
hudOverlayFallbackExpanded = expanded;
|
||||
if (
|
||||
!hudOverlayWindow ||
|
||||
hudOverlayWindow.isDestroyed() ||
|
||||
isHudOverlayMousePassthroughSupported()
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { workArea } = getHudOverlayDisplay();
|
||||
const nextBounds = resizeHudOverlayFallbackBounds(
|
||||
workArea,
|
||||
hudOverlayWindow.getBounds(),
|
||||
expanded,
|
||||
);
|
||||
hudOverlayWindow.setBounds(nextBounds, false);
|
||||
positionUpdateToastWindow();
|
||||
if (hudOverlayWindow.isVisible()) {
|
||||
hudOverlayWindow.moveTop();
|
||||
}
|
||||
}
|
||||
|
||||
ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => {
|
||||
if (hudOverlayWindow && !hudOverlayWindow.isDestroyed()) {
|
||||
if (!isHudOverlayMousePassthroughSupported()) {
|
||||
setHudOverlayFallbackExpanded(!ignore);
|
||||
hudOverlayWindow.setIgnoreMouseEvents(false);
|
||||
return;
|
||||
}
|
||||
@@ -358,6 +386,7 @@ ipcMain.handle("set-hud-overlay-capture-protection", (_event, enabled: boolean)
|
||||
|
||||
export function createHudOverlayWindow(): BrowserWindow {
|
||||
loadHudOverlayCaptureProtectionSetting();
|
||||
hudOverlayFallbackExpanded = false;
|
||||
const initialBounds = getHudOverlayBounds();
|
||||
let hasShownHudWindow = false;
|
||||
|
||||
|
||||
@@ -1,46 +1,45 @@
|
||||
import {
|
||||
ArrowClockwiseIcon,
|
||||
CaretUpIcon,
|
||||
DotsThreeVerticalIcon,
|
||||
MicrophoneIcon,
|
||||
MicrophoneSlashIcon,
|
||||
MinusIcon,
|
||||
MonitorIcon,
|
||||
DotsThreeVerticalIcon,
|
||||
TimerIcon,
|
||||
VideoCameraIcon,
|
||||
VideoCameraSlashIcon,
|
||||
XIcon,
|
||||
ArrowClockwiseIcon,
|
||||
} from "@phosphor-icons/react";
|
||||
import { AnimatePresence, motion } from "motion/react";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { RxDragHandleDots2 } from "react-icons/rx";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { useScopedT } from "../../contexts/I18nContext";
|
||||
import { useHudBarDrag } from "./hooks/useHudBarDrag";
|
||||
import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices";
|
||||
import { useLaunchWindowSystemState } from "./hooks/useLaunchWindowSystemState";
|
||||
import { useLaunchHudInteractionState } from "./hooks/useLaunchHudInteractionState";
|
||||
import { useLaunchWindowActions } from "./hooks/useLaunchWindowActions";
|
||||
import { useRecordingTimer } from "./hooks/useRecordingTimer";
|
||||
import { useScreenRecorder } from "../../hooks/useScreenRecorder";
|
||||
import { useVideoDevices } from "../../hooks/useVideoDevices";
|
||||
import { useWebcamPreviewOverlay } from "./hooks/useWebcamPreviewOverlay";
|
||||
import { Button } from "../ui/button";
|
||||
import { HudInteractionContext } from "./contexts/HudInteractionContext";
|
||||
import {
|
||||
canToggleFloatingWebcamPreview,
|
||||
} from "./floatingWebcamPreview";
|
||||
import { LaunchPopoverCoordinatorProvider, useLaunchPopoverCoordinator } from "./popovers/LaunchPopoverCoordinator";
|
||||
import { useHudBarDrag } from "./hooks/useHudBarDrag";
|
||||
import { useLaunchHudInteractionState } from "./hooks/useLaunchHudInteractionState";
|
||||
import { useLaunchWindowActions } from "./hooks/useLaunchWindowActions";
|
||||
import { useLaunchWindowSystemState } from "./hooks/useLaunchWindowSystemState";
|
||||
import { useRecordingTimer } from "./hooks/useRecordingTimer";
|
||||
import { useWebcamPreviewOverlay } from "./hooks/useWebcamPreviewOverlay";
|
||||
import styles from "./LaunchWindow.module.css";
|
||||
import { CountdownPopover } from "./popovers/CountdownPopover";
|
||||
import { LaunchPopoverCoordinatorProvider, useLaunchPopoverCoordinator } from "./popovers/LaunchPopoverCoordinator";
|
||||
import { MicPopover } from "./popovers/MicPopover";
|
||||
import { MorePopover } from "./popovers/MorePopover";
|
||||
import { ProjectPopover } from "./popovers/ProjectPopover";
|
||||
import { SourcePopover } from "./popovers/SourcePopover";
|
||||
import { WebcamPopover } from "./popovers/WebcamPopover";
|
||||
import { HudInteractionContext } from "./contexts/HudInteractionContext";
|
||||
import { MarqueeText } from "./SourceSelector";
|
||||
import styles from "./LaunchWindow.module.css";
|
||||
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "../ui/button";
|
||||
import { RecordingControls } from "./RecordingControls";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { MarqueeText } from "./SourceSelector";
|
||||
|
||||
const SHOW_DEV_UPDATE_PREVIEW = import.meta.env.DEV;
|
||||
|
||||
@@ -427,6 +426,8 @@ function LaunchWindowContent() {
|
||||
);
|
||||
|
||||
const hudMode = finalizing ? "finalizing" : recording ? "recording" : "idle";
|
||||
const useNativeHudBarDrag =
|
||||
platform === "linux" || hudOverlayMousePassthroughSupported === false;
|
||||
|
||||
return (
|
||||
<HudInteractionContext.Provider value={{ onMouseEnter: handleHudMouseEnter, onMouseLeave: handleHudMouseLeave }}>
|
||||
@@ -456,16 +457,11 @@ function LaunchWindowContent() {
|
||||
className={`${styles.bar} launch-theme mb-2`}
|
||||
>
|
||||
<div
|
||||
// On Linux (especially Wayland) the compositor owns window
|
||||
// placement, so BrowserWindow.setBounds() is silently ignored.
|
||||
// Fall back to a native OS drag via -webkit-app-region on the
|
||||
// handle. We still need JS pointer handlers in webcam-preview
|
||||
// mode (which translates via CSS inside the window), so only
|
||||
// mark the handle as a native drag region for the IPC path.
|
||||
// Linux compositors and non-passthrough Windows fallback windows
|
||||
// need native window dragging; the JS drag path only translates
|
||||
// content inside the HUD window.
|
||||
className={`flex items-center px-0.5 cursor-grab active:cursor-grabbing ${
|
||||
platform === "linux" && !showRecordingWebcamPreview
|
||||
? styles.electronDrag
|
||||
: ""
|
||||
useNativeHudBarDrag ? styles.electronDrag : ""
|
||||
}`}
|
||||
onPointerDown={handleHudBarPointerDown}
|
||||
onPointerMove={handleHudBarPointerMove}
|
||||
|
||||
Reference in New Issue
Block a user