import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, type JSX, type MouseEvent as ReactMouseEvent, type PointerEvent as ReactPointerEvent } from 'react' import type { ScopePopoutSnapshot } from '../../types/popout' import { SCOPE_LABELS, type ScopeKind } from '../../types/scope' import { DEFAULT_SCOPE_SETTINGS, type ScopeSettings } from '../../types/settings' import { applyResolvedThemeToDocument, createDefaultTheme, resolveTheme } from '../../shared/themeState' import ScopeModule from '../components/ScopeModule' import ScopeSettingsSection from '../components/ScopeSettingsSection' import WindowResizeOverlay from '../components/WindowResizeOverlay' import { usePerformanceStore } from '../stores/performanceStore' import { useUiStore } from '../stores/uiStore' import { useWindowBackgroundStore } from '../stores/windowBackgroundStore' import { getRendererWindowCapabilities } from '../windowCapabilities' import { ScopePopoutDataSource } from './ScopePopoutDataSource' import { FrameScheduler } from '../visualizers/frameScheduler' function PopInIcon(): JSX.Element { return ( ) } function SettingsIcon(): JSX.Element { return ( ) } function PinIcon(): JSX.Element { return ( ) } function GripIcon(): JSX.Element { return ( ) } interface ScopePopoutWindowProps { scopeKind: ScopeKind } const POPOUT_SETTINGS_EXPAND_HEIGHT = 260 const defaultTheme = resolveTheme(createDefaultTheme()) export default function ScopePopoutWindow({ scopeKind }: ScopePopoutWindowProps): JSX.Element { const [snapshot, setSnapshot] = useState | null>(null) const [isAlwaysOnTop, setIsAlwaysOnTop] = useState(false) const [cursorInsideWindow, setCursorInsideWindow] = useState(false) const [measurementActive, setMeasurementActive] = useState(false) const prevMiniSettingsOpenRef = useRef(false) const frameTarget = usePerformanceStore((s) => s.frameTarget) const miniSettingsOpen = useUiStore((s) => s.settingsOpen) const setMiniSettingsOpen = useUiStore((s) => s.setSettingsOpen) const frameScheduler = useMemo(() => new FrameScheduler({ frameTarget }), []) const dataSource = useMemo(() => new ScopePopoutDataSource(scopeKind), [scopeKind]) const useWindowManagerDragRegions = getRendererWindowCapabilities().useNativeDragRegions const initializeWindowBackground = useWindowBackgroundStore((s) => s.initialize) const windowBackgroundMode = useWindowBackgroundStore((s) => s.effective.mode) useEffect(() => { void window.electronAPI.isAlwaysOnTop().then(setIsAlwaysOnTop) const unsubscribe = window.electronAPI.onAlwaysOnTopChanged(setIsAlwaysOnTop) return unsubscribe }, []) useEffect(() => { void initializeWindowBackground() }, [initializeWindowBackground]) useEffect(() => { frameScheduler.setFrameTarget(frameTarget) }, [frameScheduler, frameTarget]) useEffect(() => { let isDisposed = false const syncCursorInsideWindow = (): void => { void window.electronAPI.isCursorInsideWindow() .then((isInside) => { if (!isDisposed) { setCursorInsideWindow(isInside) } }) .catch(() => { // Renderer pointer events still update the chrome when cursor polling is unavailable. }) } syncCursorInsideWindow() const interval = setInterval(syncCursorInsideWindow, 120) return () => { isDisposed = true clearInterval(interval) } }, []) useEffect(() => { const unsubscribeSnapshot = window.electronAPI.onScopePopoutSnapshot((nextSnapshot) => { if (nextSnapshot.kind !== scopeKind) return setSnapshot(nextSnapshot) applyResolvedThemeToDocument({ interface: nextSnapshot.interfaceTheme }, document.documentElement.style) }) const unsubscribeAudio = window.electronAPI.onScopePopoutAudio((kind, batch) => { if (kind !== scopeKind) return dataSource.pushAudioBatch(batch) }) const unsubscribeSession = window.electronAPI.onScopePopoutSession((kind, sessionState) => { if (kind !== scopeKind) return dataSource.setSessionState(sessionState) }) window.electronAPI.notifyScopePopoutReady(scopeKind) return () => { unsubscribeSnapshot() unsubscribeAudio() unsubscribeSession() } }, [dataSource, scopeKind]) const effectiveSettings = (snapshot?.settings ?? DEFAULT_SCOPE_SETTINGS[scopeKind]) as ScopeSettings[ScopeKind] const effectiveScopeTheme = snapshot?.scopeTheme ?? defaultTheme[scopeKind] const settingsHeight = miniSettingsOpen ? POPOUT_SETTINGS_EXPAND_HEIGHT : 0 const handleUpdateScopeSettings = (kind: K, partial: Partial): void => { if (kind !== scopeKind) return setSnapshot((prev) => { if (!prev) return prev return { ...prev, settings: { ...prev.settings, ...partial, } as ScopeSettings[K], } }) window.electronAPI.sendScopePopoutSettingsUpdate(kind, partial) } const handleDragStart = useCallback((event: ReactPointerEvent): void => { if (useWindowManagerDragRegions || event.button !== 0) return event.preventDefault() event.currentTarget.setPointerCapture(event.pointerId) window.electronAPI.startWindowMove() }, [useWindowManagerDragRegions]) const handleDragEnd = useCallback((event: ReactPointerEvent): void => { if (useWindowManagerDragRegions) { return } if (event.currentTarget.hasPointerCapture(event.pointerId)) { event.currentTarget.releasePointerCapture(event.pointerId) } window.electronAPI.stopWindowMove() }, [useWindowManagerDragRegions]) const handleAltDragStart = useCallback((event: ReactMouseEvent): void => { if (useWindowManagerDragRegions || !event.altKey || event.button !== 0) return const target = event.target if (target instanceof Element && target.closest('.scope-popout__drag-handle')) { return } event.preventDefault() window.electronAPI.startWindowMove() }, [useWindowManagerDragRegions]) const handleAltDragEnd = useCallback((): void => { if (useWindowManagerDragRegions) { return } window.electronAPI.stopWindowMove() }, [useWindowManagerDragRegions]) useLayoutEffect(() => { if (miniSettingsOpen && !prevMiniSettingsOpenRef.current) { window.electronAPI.expandSettings(POPOUT_SETTINGS_EXPAND_HEIGHT) } else if (!miniSettingsOpen && prevMiniSettingsOpenRef.current) { window.electronAPI.collapseSettings(POPOUT_SETTINGS_EXPAND_HEIGHT) } prevMiniSettingsOpenRef.current = miniSettingsOpen }, [miniSettingsOpen]) useEffect(() => { return () => { if (prevMiniSettingsOpenRef.current) { window.electronAPI.setSettingsHeight(0) } window.electronAPI.stopWindowMove() } }, []) return (
setCursorInsideWindow(true)} onMouseMove={() => setCursorInsideWindow(true)} onMouseLeave={() => setCursorInsideWindow(false)} onMouseDown={useWindowManagerDragRegions ? undefined : handleAltDragStart} onMouseUp={useWindowManagerDragRegions ? undefined : handleAltDragEnd} >
{snapshot?.label ?? SCOPE_LABELS[scopeKind]} Detached Scope
{miniSettingsOpen && (
)} {windowBackgroundMode !== 'solid' && }
) }