import { useEffect, useMemo, useRef, useState } from "react"; import { Platform, type PointerEvent as RNPointerEvent, type ViewProps } from "react-native"; import { getIsDesktopMac, DESKTOP_TRAFFIC_LIGHT_WIDTH, DESKTOP_TRAFFIC_LIGHT_HEIGHT, } from "@/constants/layout"; import { getDesktopWindow } from "@/desktop/electron/window"; import { isDesktop } from "@/desktop/host"; export async function toggleMaximize() { const win = getDesktopWindow(); if (win && typeof win.toggleMaximize === "function") { try { await win.toggleMaximize(); } catch (error) { console.warn("[DesktopWindow] toggleMaximize failed", error); } } } // --------------------------------------------------------------------------- // Manual window dragging via pointer events. // Mirrors the Tauri implementation: single pointerdown handler with // double-click-to-maximize via timing, closest() for interactive check, // and pointer capture for move tracking. // --------------------------------------------------------------------------- const INTERACTIVE_SELECTOR = "button, a, input, textarea, select, " + "[role='button'], [role='link'], [role='textbox'], [role='combobox'], " + "[role='tab'], [role='switch'], [role='checkbox'], [role='slider'], " + "[role='menuitem'], [contenteditable='true']"; const DOUBLE_CLICK_MS = 300; type DesktopDragViewProps = Pick< ViewProps, "onPointerDown" | "onPointerMove" | "onPointerUp" | "onPointerCancel" >; export function useDesktopDragHandlers(): DesktopDragViewProps { const isDragging = useRef(false); const lastPointerDownAt = useRef(0); const isActive = Platform.OS === "web" && isDesktop(); useEffect(() => { if (!isActive) return; function handleBlur() { if (!isDragging.current) return; isDragging.current = false; getDesktopWindow()?.endMove?.(); } window.addEventListener("blur", handleBlur); return () => window.removeEventListener("blur", handleBlur); }, [isActive]); return useMemo((): DesktopDragViewProps => { if (!isActive) return {}; function stopDrag(e: RNPointerEvent) { if (!isDragging.current) return; isDragging.current = false; // On web, currentTarget is a DOM Element (typed as HostInstance in RN) const el = e.currentTarget as unknown as Element | null; if (el && "releasePointerCapture" in el) { el.releasePointerCapture(e.nativeEvent.pointerId); } getDesktopWindow()?.endMove?.(); } return { onPointerDown: (e: RNPointerEvent) => { if (e.nativeEvent.button !== 0) return; // On web, e.target is a DOM Element (typed as HostInstance in RN) const target = e.target as unknown as Element; if (target.closest?.(INTERACTIVE_SELECTOR)) return; e.preventDefault(); const now = Date.now(); if (now - lastPointerDownAt.current < DOUBLE_CLICK_MS) { lastPointerDownAt.current = 0; void toggleMaximize(); return; } lastPointerDownAt.current = now; const win = getDesktopWindow(); if (!win?.startMove) return; isDragging.current = true; const el = e.currentTarget as unknown as Element; el.setPointerCapture(e.nativeEvent.pointerId); win.startMove(e.nativeEvent.screenX, e.nativeEvent.screenY); }, onPointerMove: (e: RNPointerEvent) => { if (!isDragging.current) return; getDesktopWindow()?.moving?.(e.nativeEvent.screenX, e.nativeEvent.screenY); }, onPointerUp: stopDrag, onPointerCancel: stopDrag, }; }, [isActive]); } export function useTrafficLightPadding(): { left: number; top: number } { const [isFullscreen, setIsFullscreen] = useState(false); useEffect(() => { if (Platform.OS !== "web" || !getIsDesktopMac()) return; let disposed = false; let cleanup: (() => void) | undefined; let didCleanup = false; function runCleanup() { if (!cleanup || didCleanup) return; didCleanup = true; try { void Promise.resolve(cleanup()).catch((error) => { console.warn("[DesktopWindow] Failed to remove resize listener", error); }); } catch (error) { console.warn("[DesktopWindow] Failed to remove resize listener", error); } } async function setup() { const win = getDesktopWindow(); if (!win) return; const fullscreen = typeof win.isFullscreen === "function" ? await win.isFullscreen() : false; if (disposed) return; setIsFullscreen(fullscreen); if (typeof win.onResized !== "function") { return; } const unlisten = await win.onResized(async () => { if (disposed) return; const fs = typeof win.isFullscreen === "function" ? await win.isFullscreen() : false; if (disposed) return; setIsFullscreen(fs); }); cleanup = unlisten; if (disposed) { runCleanup(); } } void setup(); return () => { disposed = true; runCleanup(); }; }, []); if (!getIsDesktopMac() || isFullscreen) { return { left: 0, top: 0 }; } return { left: DESKTOP_TRAFFIC_LIGHT_WIDTH, top: DESKTOP_TRAFFIC_LIGHT_HEIGHT, }; }