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() {
>
(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;