// Region selection view - fullscreen transparent overlay for drawing a capture region import { useState, useCallback, useEffect, useRef } from "react"; import { emit, listen } from "@tauri-apps/api/event"; import { getCurrentWindow } from "@tauri-apps/api/window"; import { hideRegionSelector, setRegionSelectorPassthrough } from "../lib/api"; interface RegionBounds { x: number; y: number; width: number; height: number; // Optional debug/UX fields (used by the overlay to show logical vs physical size). logicalWidth?: number; logicalHeight?: number; scaleFactor?: number; } interface DragState { startX: number; startY: number; currentX: number; currentY: number; } function RegionSelectorView() { const [dragState, setDragState] = useState(null); const [selectedRegion, setSelectedRegion] = useState(null); const [locked, setLocked] = useState(false); const containerRef = useRef(null); // Use physical screen pixels end-to-end. // - CGEventTap locations are in global display pixels // - CGDisplay bounds/pixels are in global display pixels // - screencapture -R/-D behaves consistently when fed display pixels // // Mouse events are in CSS pixels (logical). Convert to physical using scaleFactor. // Mouse events are relative to the webview content area (inner rect), not the outer window. const [windowOriginPx, setWindowOriginPx] = useState<{ x: number; y: number }>({ x: 0, y: 0, }); const [scaleFactor, setScaleFactor] = useState(1); useEffect(() => { let cancelled = false; (async () => { try { const w = getCurrentWindow(); const [pos, scale] = await Promise.all([w.innerPosition(), w.scaleFactor()]); if (cancelled) return; setWindowOriginPx({ x: pos.x, y: pos.y }); setScaleFactor(scale || 1); } catch { // Best-effort: fall back to assuming origin (0,0). } })(); return () => { cancelled = true; }; }, []); // When the backend reopens this window, reset back to interactive mode. useEffect(() => { let unlistenFn: (() => void) | undefined; const setup = async () => { unlistenFn = await listen("region-selector-reset", async () => { setLocked(false); setSelectedRegion(null); setDragState(null); // The backend may reposition this window per-monitor when reopening it. // Refresh the origin/scale so our logical->physical conversion stays correct. try { const w = getCurrentWindow(); const [pos, scale] = await Promise.all([w.innerPosition(), w.scaleFactor()]); setWindowOriginPx({ x: pos.x, y: pos.y }); setScaleFactor(scale || 1); } catch { // ignore } try { await setRegionSelectorPassthrough(false); } catch { // ignore } }); }; setup(); return () => { unlistenFn?.(); }; }, []); // This window is configured as `transparent: true` in Tauri, but our global CSS theme // sets an opaque body background. Override it here so the desktop is visible behind // the semi-transparent overlay while selecting a region. useEffect(() => { const prevHtmlBg = document.documentElement.style.background; const prevBodyBg = document.body.style.background; const prevBodyBgColor = document.body.style.backgroundColor; document.documentElement.style.background = "transparent"; document.body.style.background = "transparent"; document.body.style.backgroundColor = "transparent"; return () => { document.documentElement.style.background = prevHtmlBg; document.body.style.background = prevBodyBg; document.body.style.backgroundColor = prevBodyBgColor; }; }, []); // Calculate the actual region bounds from drag state const getRegionFromDrag = useCallback((drag: DragState): RegionBounds => { const x = Math.min(drag.startX, drag.currentX); const y = Math.min(drag.startY, drag.currentY); const width = Math.abs(drag.currentX - drag.startX); const height = Math.abs(drag.currentY - drag.startY); return { x, y, width, height }; }, []); const handleMouseDown = useCallback( (e: React.MouseEvent) => { if (locked) return; // Use CSS pixels which correspond to "points" for our window. const x = e.clientX; const y = e.clientY; setDragState({ startX: x, startY: y, currentX: x, currentY: y, }); setSelectedRegion(null); }, [locked] ); const handleMouseMove = useCallback( (e: React.MouseEvent) => { if (!dragState) return; if (locked) return; // Use CSS pixels which correspond to "points" for our window. const x = e.clientX; const y = e.clientY; setDragState((prev) => prev ? { ...prev, currentX: x, currentY: y, } : null ); }, [dragState, locked] ); const handleMouseUp = useCallback(() => { if (!dragState) return; if (locked) return; const region = getRegionFromDrag(dragState); // Only accept regions with meaningful size (at least 50x50 pixels) if (region.width >= 50 && region.height >= 50) { setSelectedRegion(region); } setDragState(null); }, [dragState, getRegionFromDrag, locked]); const handleConfirm = useCallback(async () => { if (!selectedRegion) return; // Convert local CSS pixels (logical) to global physical pixels. const s = scaleFactor || 1; const globalRegion: RegionBounds = { x: Math.round(windowOriginPx.x + selectedRegion.x * s), y: Math.round(windowOriginPx.y + selectedRegion.y * s), width: Math.round(selectedRegion.width * s), height: Math.round(selectedRegion.height * s), logicalWidth: Math.round(selectedRegion.width), logicalHeight: Math.round(selectedRegion.height), scaleFactor: s, }; // Emit the selected region back to the overlay window await emit("region-selected", globalRegion); // Keep the frame visible, but make the window click-through so the user can // keep working and hit Record in the recording overlay. setLocked(true); await setRegionSelectorPassthrough(true); }, [selectedRegion, scaleFactor, windowOriginPx.x, windowOriginPx.y]); const handleCancel = useCallback(async () => { await emit("region-cancelled", {}); setLocked(false); try { await setRegionSelectorPassthrough(false); } catch { // ignore } await hideRegionSelector(); }, []); // Handle escape key to cancel useEffect(() => { const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Escape") { handleCancel(); } else if (e.key === "Enter" && selectedRegion) { handleConfirm(); } }; window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [handleCancel, handleConfirm, selectedRegion]); // Current drag region for display (in CSS pixels) const displayRegion = dragState ? getRegionFromDrag(dragState) : selectedRegion; const cssRegion = displayRegion ? { ...displayRegion } : null; return (
{/* Instructions overlay */} {!dragState && !selectedRegion && (
Select Recording Region
Click and drag to select the area you want to record
Press ESC to cancel
)} {/* Selection rectangle */} {cssRegion && cssRegion.width > 0 && cssRegion.height > 0 && ( <> {/* Frame + outside mask */}
{/* Dimension label */}
{Math.round(displayRegion!.width)} x {Math.round(displayRegion!.height)} {locked ? " (locked)" : ""}
{/* Corner handles */} {!locked && [ { x: cssRegion.x, y: cssRegion.y }, { x: cssRegion.x + cssRegion.width, y: cssRegion.y }, { x: cssRegion.x, y: cssRegion.y + cssRegion.height }, { x: cssRegion.x + cssRegion.width, y: cssRegion.y + cssRegion.height }, ].map((corner, i) => (
))} )} {/* Confirmation buttons */} {selectedRegion && !dragState && !locked && (
e.stopPropagation()} >
)}
); } export default RegionSelectorView;