From 5a2cfee6b4a5ec2bd8bdcbe5d2ea36487824e004 Mon Sep 17 00:00:00 2001 From: Alan Trebugeais Date: Thu, 7 May 2026 00:17:05 +0200 Subject: [PATCH] add: better performance through RAF of the Camera preview overlay dragging (avoiding re-renders) add: SourceSelector marquee (scrolling text when too big) --- src/components/launch/LaunchWindow.tsx | 3 +- src/components/launch/SourceSelector.css | 60 ++++++++++++-- src/components/launch/SourceSelector.tsx | 48 +++++++++-- .../launch/hooks/useWebcamPreviewOverlay.ts | 79 ++++++++++++++----- 4 files changed, 161 insertions(+), 29 deletions(-) diff --git a/src/components/launch/LaunchWindow.tsx b/src/components/launch/LaunchWindow.tsx index 91be0e5b..4a2a0198 100644 --- a/src/components/launch/LaunchWindow.tsx +++ b/src/components/launch/LaunchWindow.tsx @@ -33,6 +33,7 @@ import { MicPopover } from "./popovers/MicPopover"; import { MorePopover } from "./popovers/MorePopover"; import { SourcePopover } from "./popovers/SourcePopover"; import { WebcamPopover } from "./popovers/WebcamPopover"; +import { MarqueeText } from "./SourceSelector"; import styles from "./LaunchWindow.module.css"; import { Separator } from "@/components/ui/separator"; @@ -235,7 +236,7 @@ function LaunchWindowContent() { >
-
{selectedSource}
+
(null); + const [overflowing, setOverflowing] = useState(false); + + useLayoutEffect(() => { + const node = staticRef.current; + if (!node) return; + const checkOverflow = () => { + setOverflowing(node.scrollWidth > node.clientWidth + 1); + }; + checkOverflow(); + const observer = new ResizeObserver(checkOverflow); + observer.observe(node); + return () => observer.disconnect(); + }, [text]); + + return ( +
+ + {text} + + + + {text} + + {text} + + + +
+ ); +} + /** * SourceSelectorContent - The actual list of sources */ @@ -84,9 +120,9 @@ export const SourceSelectorContent = ({ )} -
-
- {source.windowTitle || source.name} +
+
+
{source.sourceType === "screen" ? t("recording.screen") : t("recording.window")} @@ -196,7 +232,9 @@ export const SourceSelector = React.memo(function SourceSelector({ title={selectedSource} > -
{selectedSource}
+
+ +
(null); const recordingWebcamPreviewRef = useRef(null); const recordingWebcamPreviewContainerRef = useRef(null); const previewStreamRef = useRef(null); + const previewDragMoveRafRef = useRef(null); + const previewDragPendingPointerRef = useRef<{ clientX: number; clientY: number } | null>(null); const webcamPreviewDragStartRef = useRef<{ pointerId: number; startX: number; @@ -47,7 +50,11 @@ export function useWebcamPreviewOverlay({ useEffect(() => { if (!webcamEnabled) { + webcamPreviewOffsetRef.current = DEFAULT_WEBCAM_PREVIEW_OFFSET; setWebcamPreviewOffset(DEFAULT_WEBCAM_PREVIEW_OFFSET); + if (recordingWebcamPreviewContainerRef.current) { + recordingWebcamPreviewContainerRef.current.style.transform = "translate(0px, 0px)"; + } webcamPreviewDragStartRef.current = null; isWebcamPreviewDraggingRef.current = false; setShowFloatingWebcamPreview(true); @@ -68,8 +75,8 @@ export function useWebcamPreviewOverlay({ pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, - originX: webcamPreviewOffset.x, - originY: webcamPreviewOffset.y, + originX: webcamPreviewOffsetRef.current.x, + originY: webcamPreviewOffsetRef.current.y, initialLeft: previewRect.left, initialTop: previewRect.top, previewWidth: previewRect.width, @@ -78,7 +85,7 @@ export function useWebcamPreviewOverlay({ }; event.currentTarget.setPointerCapture(event.pointerId); }, - [webcamPreviewOffset.x, webcamPreviewOffset.y], + [], ); const handleWebcamPreviewPointerMove = useCallback((event: PointerEvent) => { @@ -99,22 +106,42 @@ export function useWebcamPreviewOverlay({ isWebcamPreviewDraggingRef.current = true; } - const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); - const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); - const unclampedLeft = dragState.initialLeft + deltaX; - const unclampedTop = dragState.initialTop + deltaY; - const clampedLeft = Math.min( - Math.max(0, unclampedLeft), - Math.max(0, viewportWidth - dragState.previewWidth), - ); - const clampedTop = Math.min( - Math.max(0, unclampedTop), - Math.max(0, viewportHeight - dragState.previewHeight), - ); + previewDragPendingPointerRef.current = { clientX: event.clientX, clientY: event.clientY }; + if (previewDragMoveRafRef.current !== null) { + return; + } - setWebcamPreviewOffset({ - x: dragState.originX + (clampedLeft - dragState.initialLeft), - y: dragState.originY + (clampedTop - dragState.initialTop), + previewDragMoveRafRef.current = requestAnimationFrame(() => { + previewDragMoveRafRef.current = null; + const latestDragState = webcamPreviewDragStartRef.current; + const pointer = previewDragPendingPointerRef.current; + if (!latestDragState || !pointer) { + return; + } + + const latestDeltaX = pointer.clientX - latestDragState.startX; + const latestDeltaY = pointer.clientY - latestDragState.startY; + const viewportWidth = Math.max(window.innerWidth, window.screen?.width ?? 0); + const viewportHeight = Math.max(window.innerHeight, window.screen?.height ?? 0); + const unclampedLeft = latestDragState.initialLeft + latestDeltaX; + const unclampedTop = latestDragState.initialTop + latestDeltaY; + const clampedLeft = Math.min( + Math.max(0, unclampedLeft), + Math.max(0, viewportWidth - latestDragState.previewWidth), + ); + const clampedTop = Math.min( + Math.max(0, unclampedTop), + Math.max(0, viewportHeight - latestDragState.previewHeight), + ); + + const nextOffset = { + x: latestDragState.originX + (clampedLeft - latestDragState.initialLeft), + y: latestDragState.originY + (clampedTop - latestDragState.initialTop), + }; + webcamPreviewOffsetRef.current = nextOffset; + if (recordingWebcamPreviewContainerRef.current) { + recordingWebcamPreviewContainerRef.current.style.transform = `translate(${nextOffset.x}px, ${nextOffset.y}px)`; + } }); }, []); @@ -123,10 +150,16 @@ export function useWebcamPreviewOverlay({ if (!dragState || dragState.pointerId !== event.pointerId) { return; } + if (previewDragMoveRafRef.current !== null) { + cancelAnimationFrame(previewDragMoveRafRef.current); + previewDragMoveRafRef.current = null; + } + previewDragPendingPointerRef.current = null; const wasDragging = dragState.dragging; webcamPreviewDragStartRef.current = null; isWebcamPreviewDraggingRef.current = false; + setWebcamPreviewOffset({ ...webcamPreviewOffsetRef.current }); if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId); } @@ -166,6 +199,16 @@ export function useWebcamPreviewOverlay({ [attachPreviewStreamToNode], ); + useEffect(() => { + return () => { + if (previewDragMoveRafRef.current !== null) { + cancelAnimationFrame(previewDragMoveRafRef.current); + } + previewDragMoveRafRef.current = null; + previewDragPendingPointerRef.current = null; + }; + }, []); + useEffect(() => { let mounted = true;