From 8a3539abefdb6e9fad6fe5be7e4b5522650a2d42 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Wed, 6 May 2026 21:50:07 +0200 Subject: [PATCH] add: LaunchWindow source selector without that much of a jitter. it's still weirdly behaving but it's better --- electron/ipc/register/sources.ts | 14 - src/components/launch/LaunchWindow.module.css | 17 ++ src/components/launch/LaunchWindow.tsx | 280 +++++++++++------- src/components/launch/SourceSelector.tsx | 89 +++--- 4 files changed, 242 insertions(+), 158 deletions(-) diff --git a/electron/ipc/register/sources.ts b/electron/ipc/register/sources.ts index d023ede1..618b4a2a 100644 --- a/electron/ipc/register/sources.ts +++ b/electron/ipc/register/sources.ts @@ -20,20 +20,6 @@ function normalizeDesktopSourceName(value: string) { return value.trim().replace(/\s+/g, " ").toLowerCase(); } -function hasUsableSourceThumbnail( - thumbnail: - | { - isEmpty: () => boolean; - getSize: () => { width: number; height: number }; - } - | null - | undefined, -) { - if (!thumbnail || thumbnail.isEmpty()) return false; - const size = thumbnail.getSize(); - return size.width > 1 && size.height > 1; -} - function broadcastSelectedSourceChange() { for (const window of BrowserWindow.getAllWindows()) { if (!window.isDestroyed()) { diff --git a/src/components/launch/LaunchWindow.module.css b/src/components/launch/LaunchWindow.module.css index 9a60e307..da79e2dd 100644 --- a/src/components/launch/LaunchWindow.module.css +++ b/src/components/launch/LaunchWindow.module.css @@ -127,6 +127,12 @@ animation: menuCardIn 0.18s ease; } +.menuCardTop { + margin-top: 8px; + margin-bottom: auto; + animation: menuCardInTop 0.18s ease; +} + @keyframes menuCardIn { from { opacity: 0; @@ -138,6 +144,17 @@ } } +@keyframes menuCardInTop { + from { + opacity: 0; + transform: translateY(-12px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + .menuCard::-webkit-scrollbar { width: 4px; } diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 864e8de6..4b8f8b8b 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -31,7 +31,6 @@ import { useMicrophoneDevices } from "../../hooks/useMicrophoneDevices"; import { useScreenRecorder } from "../../hooks/useScreenRecorder"; import { useVideoDevices } from "../../hooks/useVideoDevices"; import { AudioLevelMeter } from "../ui/audio-level-meter"; -import { ContentClamp } from "../ui/content-clamp"; import ProjectBrowserDialog, { type ProjectLibraryEntry, } from "../video-editor/ProjectBrowserDialog"; @@ -45,7 +44,7 @@ import { } from "./hudMousePassthrough"; import styles from "./LaunchWindow.module.css"; import { RecordingControls } from "./RecordingControls"; -import { SourceSelector } from "./SourceSelector"; +import { SourceSelectorContent } from "./SourceSelector"; interface DesktopSource { id: string; @@ -250,6 +249,20 @@ export function LaunchWindow() { const isHudDraggingRef = useRef(false); const isWebcamPreviewDraggingRef = useRef(false); + const [isAtTop, setIsAtTop] = useState(false); + + useEffect(() => { + const checkPosition = () => { + const screenHeight = window.screen.availHeight; + const windowY = window.screenY; + setIsAtTop(windowY < screenHeight / 2); + }; + checkPosition(); + if (activeDropdown !== "none" || projectBrowserOpen) { + checkPosition(); + } + }, [activeDropdown, projectBrowserOpen]); + const micDropdownOpen = activeDropdown === "mic"; const webcamDropdownOpen = activeDropdown === "webcam"; const showWebcamControls = webcamEnabled && !recording; @@ -868,8 +881,12 @@ export function LaunchWindow() { const toggleDropdown = (which: "sources" | "more" | "mic" | "countdown" | "webcam") => { setProjectBrowserOpen(false); - setActiveDropdown(activeDropdown === which ? "none" : which); - if (activeDropdown !== which && which === "sources") fetchSources(); + const nextActive = activeDropdown === which ? "none" : which; + setActiveDropdown(nextActive); + if (nextActive !== "none") { + window.electronAPI?.hudOverlaySetIgnoreMouse?.(false); + } + if (nextActive !== "none" && which === "sources") fetchSources(); }; const handleSourceSelect = async (source: DesktopSource) => { @@ -988,16 +1005,24 @@ export function LaunchWindow() { <> {platform !== "linux" && ( <> - setActiveDropdown(open ? "sources" : "none")} - /> + @@ -1105,12 +1130,16 @@ export function LaunchWindow() { return (
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false)} onMouseLeave={() => { if ( @@ -1122,12 +1151,117 @@ export function LaunchWindow() { } }} > +
+
+ +
+ +
+ +
+ + + {finalizing + ? finalizingControls + : recording + ? recordingControls + : idleControls} + + +
+
+
+ {showRecordingWebcamPreview && ( +
+
+ )} +
+ {/* Only the visible HUD content should become interactive. */}
window.electronAPI?.hudOverlaySetIgnoreMouse?.(false)} + onMouseLeave={() => { + if ( + !isHudDraggingRef.current && + !isWebcamPreviewDraggingRef.current && + !webcamPreviewDragStartRef.current && + activeDropdown === "none" && + !projectBrowserOpen + ) { + window.electronAPI?.hudOverlaySetIgnoreMouse?.(true); + } }} > {projectBrowserOpen ? ( @@ -1143,10 +1277,26 @@ export function LaunchWindow() { />
) : null} - {activeDropdown !== "none" && activeDropdown !== "sources" && ( -
+ {activeDropdown !== "none" && ( +
+ {activeDropdown === "sources" && ( + <> +
{t("recording.sources")}
+ + + )} - {activeDropdown === "mic" && ( + {activeDropdown === "mic" && ( <>
{t("recording.microphone")} @@ -1418,98 +1568,6 @@ export function LaunchWindow() {
)}
- -
-
- -
- -
- -
- - - {finalizing - ? finalizingControls - : recording - ? recordingControls - : idleControls} - - -
-
-
- {showRecordingWebcamPreview && ( -
-
- )} -
); diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 1266733d..971ea5dd 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -117,29 +117,17 @@ interface SourceSelectorProps { } /** - * SourceSelector - A rich source selection component with thumbnails - * Uses Radix UI Popover for positioning and accessibility + * SourceSelectorContent - The actual list of sources */ -export const SourceSelector = React.memo(function SourceSelector({ +export const SourceSelectorContent = ({ screenSources = [], windowSources = [], selectedSource = "Screen", loading = false, onSourceSelect = () => {}, - onFetchSources = async () => {}, - open = false, - onOpenChange = () => {}, - children, -}: SourceSelectorProps) { +}: Pick) => { const t = useScopedT("launch"); - // Fetch sources when popover opens - useEffect(() => { - if (open) { - void onFetchSources(); - } - }, [open, onFetchSources]); - // Memoized source item to prevent unnecessary re-renders const SourceItem = useCallback( ({ source, isSelected }: { source: DesktopSource; isSelected: boolean }) => ( @@ -246,6 +234,52 @@ export const SourceSelector = React.memo(function SourceSelector({ ); }, [windowSources, selectedSource, SourceItem, t]); + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {screenSection || windowSection ? ( + <> + {screenSection} + {windowSection} + + ) : ( +
+ {t("recording.noSourcesFound")} +
+ )} +
+ ); +}; + +/** + * SourceSelector - A rich source selection component with thumbnails + * Uses Radix UI Popover for positioning and accessibility + */ +export const SourceSelector = React.memo(function SourceSelector({ + screenSources = [], + windowSources = [], + selectedSource = "Screen", + loading = false, + onSourceSelect = () => {}, + onFetchSources = async () => {}, + open = false, + onOpenChange = () => {}, + children, +}: SourceSelectorProps) { + // Fetch sources when popover opens + useEffect(() => { + if (open) { + void onFetchSources(); + } + }, [open, onFetchSources]); + return ( @@ -287,24 +321,13 @@ export const SourceSelector = React.memo(function SourceSelector({ collisionPadding={10} usePortal={false} > - {loading ? ( -
-
-
- ) : ( -
- {screenSection || windowSection ? ( - <> - {screenSection} - {windowSection} - - ) : ( -
- {t("recording.noSourcesFound")} -
- )} -
- )} + );