Merge pull request #626 from webadderallorg/fix/linux-selection-window-jitter

fix(linux): stabilize portal source selection HUD
This commit is contained in:
Phạm Thị Minh Hồng
2026-05-29 23:30:02 +07:00
committed by GitHub
7 changed files with 110 additions and 8 deletions
+1
View File
@@ -204,6 +204,7 @@ interface RendererNativeExportCapabilities {
interface Window {
electronAPI: {
hudOverlaySetIgnoreMouse: (ignore: boolean) => void;
hudOverlaySetSourceSelectionActive: (active: boolean) => void;
hudOverlayDrag: (phase: "start" | "move" | "end", screenX: number, screenY: number) => void;
hudOverlayHide: () => void;
hudOverlayClose: () => void;
+4
View File
@@ -158,6 +158,10 @@ describe("shouldResizeHudOverlayFallback", () => {
it("keeps the recording HUD compact in non-passthrough mode", () => {
expect(shouldResizeHudOverlayFallback(false, true)).toBe(false);
});
it("keeps the fallback stable while source selection is active", () => {
expect(shouldResizeHudOverlayFallback(false, false, true)).toBe(false);
});
});
describe("shouldExpandHudOverlayFallback", () => {
+2 -1
View File
@@ -41,8 +41,9 @@ export function getHudOverlayWindowBounds(
export function shouldResizeHudOverlayFallback(
mousePassthroughSupported: boolean,
recordingActive: boolean,
interactionLocked = false,
): boolean {
return !mousePassthroughSupported && !recordingActive;
return !mousePassthroughSupported && !recordingActive && !interactionLocked;
}
export function shouldExpandHudOverlayFallback({
+8 -3
View File
@@ -167,6 +167,9 @@ contextBridge.exposeInMainWorld("electronAPI", {
hudOverlaySetIgnoreMouse: (ignore: boolean) => {
ipcRenderer.send("hud-overlay-set-ignore-mouse", ignore);
},
hudOverlaySetSourceSelectionActive: (active: boolean) => {
ipcRenderer.send("hud-overlay-set-source-selection-active", active);
},
hudOverlayDrag: (phase: "start" | "move" | "end", screenX: number, screenY: number) => {
ipcRenderer.send("hud-overlay-drag", phase, screenX, screenY);
},
@@ -708,8 +711,10 @@ contextBridge.exposeInMainWorld("electronAPI", {
return ipcRenderer.invoke("set-current-recording-session", session, options);
},
onRecordingSessionChanged: (callback: (session: RecordingSessionData | null) => void) => {
const listener = (_event: Electron.IpcRendererEvent, payload: RecordingSessionData | null) =>
callback(payload);
const listener = (
_event: Electron.IpcRendererEvent,
payload: RecordingSessionData | null,
) => callback(payload);
ipcRenderer.on("recording-session-changed", listener);
return () => ipcRenderer.removeListener("recording-session-changed", listener);
},
@@ -896,7 +901,7 @@ contextBridge.exposeInMainWorld("electronAPI", {
success?: boolean;
value?: unknown;
};
return result?.success ? result.value ?? null : null;
return result?.success ? (result.value ?? null) : null;
},
setAppSetting: (key: string, value: unknown) => {
const result = ipcRenderer.sendSync("app-settings:set", key, value) as {
+31 -2
View File
@@ -32,6 +32,7 @@ let hudOverlayHiddenFromCapture = true;
let hudOverlayCaptureProtectionLoaded = false;
let hudOverlayFallbackExpanded = false;
let hudOverlayIgnoringMouse = true;
let hudOverlaySourceSelectionActive = false;
let hudOverlayMouseReassertTimer: NodeJS.Timeout | null = null;
let hudOverlayRecordingActive = false;
let hudOverlayWebcamPreviewVisible = false;
@@ -287,7 +288,12 @@ function setHudOverlayFallbackExpanded(expanded: boolean) {
}
function setHudOverlayMousePassthrough(ignore: boolean) {
hudOverlayIgnoringMouse = hudOverlayRecordingActive ? false : ignore;
hudOverlayIgnoringMouse =
hudOverlaySourceSelectionActive && !hudOverlayRecordingActive
? true
: hudOverlayRecordingActive
? false
: ignore;
if (hudOverlayMouseReassertTimer) {
clearTimeout(hudOverlayMouseReassertTimer);
@@ -305,9 +311,21 @@ function setHudOverlayMousePassthrough(ignore: boolean) {
return;
}
if (hudOverlaySourceSelectionActive) {
hudOverlayFallbackExpanded = false;
hudOverlayWindow.setIgnoreMouseEvents(false);
return;
}
const mousePassthroughSupported = isHudOverlayMousePassthroughSupported();
if (!mousePassthroughSupported) {
if (shouldResizeHudOverlayFallback(mousePassthroughSupported, hudOverlayRecordingActive)) {
if (
shouldResizeHudOverlayFallback(
mousePassthroughSupported,
hudOverlayRecordingActive,
hudOverlaySourceSelectionActive,
)
) {
setHudOverlayFallbackExpanded(!ignore);
}
hudOverlayWindow.setIgnoreMouseEvents(false);
@@ -326,6 +344,17 @@ ipcMain.on("hud-overlay-set-ignore-mouse", (_event, ignore: boolean) => {
setHudOverlayMousePassthrough(Boolean(ignore));
});
ipcMain.on("hud-overlay-set-source-selection-active", (_event, active: boolean) => {
hudOverlaySourceSelectionActive = Boolean(active);
if (hudOverlaySourceSelectionActive) {
hudOverlayFallbackExpanded = false;
applyHudOverlayBounds();
return;
}
setHudOverlayMousePassthrough(hudOverlayIgnoringMouse);
});
// Keep compatibility with existing drag IPC/state.
let hudUserPosition: { x: number; y: number } | null = null;
let hudDragOffset: { x: number; y: number } | null = null;
+27
View File
@@ -7,6 +7,7 @@ import {
normalizeBrowserMicrophoneProfile,
resolveBrowserCaptureCursorPolicy,
resolveLinuxPortalCursorPresentation,
shouldLockHudDuringDisplaySelection,
shouldUseLinuxPortalCapture,
shouldUseNativeWindowsCaptureForSource,
} from "./useScreenRecorder";
@@ -246,6 +247,32 @@ describe("shouldUseLinuxPortalCapture", () => {
});
});
describe("shouldLockHudDuringDisplaySelection", () => {
it("locks HUD fallback resizing while Linux portal selection is active", () => {
expect(
shouldLockHudDuringDisplaySelection({
platform: "linux",
useLinuxPortal: true,
}),
).toBe(true);
});
it("keeps non-portal capture flows interactive", () => {
expect(
shouldLockHudDuringDisplaySelection({
platform: "linux",
useLinuxPortal: false,
}),
).toBe(false);
expect(
shouldLockHudDuringDisplaySelection({
platform: "win32",
useLinuxPortal: true,
}),
).toBe(false);
});
});
describe("getScreenCaptureCursorSetting", () => {
it("normalizes only supported screen-capture cursor settings", () => {
expect(getScreenCaptureCursorSetting({ cursor: "motion" } as MediaTrackSettings)).toBe(
+37 -2
View File
@@ -145,6 +145,16 @@ export function shouldUseLinuxPortalCapture({
);
}
export function shouldLockHudDuringDisplaySelection({
platform,
useLinuxPortal,
}: {
platform?: string;
useLinuxPortal: boolean;
}) {
return platform === "linux" && useLinuxPortal;
}
type UseScreenRecorderReturn = {
recording: boolean;
paused: boolean;
@@ -1429,6 +1439,16 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
return;
}
let hudSourceSelectionActive = false;
const setHudSourceSelectionActive = (active: boolean) => {
if (hudSourceSelectionActive === active) {
return;
}
hudSourceSelectionActive = active;
window.electronAPI?.hudOverlaySetSourceSelectionActive?.(active);
};
hasPromptedForReselect.current = false;
startInFlight.current = true;
setStarting(true);
@@ -1628,7 +1648,14 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
}
setRecording(true);
window.electronAPI?.setRecordingState(true);
try {
await window.electronAPI?.setRecordingState(true);
} catch (stateError) {
console.warn(
"Failed to notify main process that native recording started:",
stateError,
);
}
return;
}
@@ -1676,6 +1703,9 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
browserCaptureSourceId: browserCaptureSource.id,
selectedSourceId: selectedSource.id,
});
if (shouldLockHudDuringDisplaySelection({ platform, useLinuxPortal })) {
setHudSourceSelectionActive(true);
}
const browserScreenVideoConstraints = {
mandatory: {
chromeMediaSource: CHROME_MEDIA_SOURCE,
@@ -1990,7 +2020,11 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
webcamStartTime.current === null ? 0 : webcamStartTime.current - mainStartedAt;
recorder.start(RECORDER_TIMESLICE_MS);
setRecording(true);
window.electronAPI?.setRecordingState(true);
try {
await window.electronAPI?.setRecordingState(true);
} catch (stateError) {
console.warn("Failed to notify main process that recording started:", stateError);
}
} catch (error) {
console.error("Failed to start recording:", error);
alert(
@@ -2008,6 +2042,7 @@ export function useScreenRecorder(): UseScreenRecorderReturn {
await stopWebcamRecorder();
}
} finally {
setHudSourceSelectionActive(false);
startInFlight.current = false;
setStarting(false);
}