From 6a22a3ecbdff2d0e6a4c24c8db40bb86bc01b692 Mon Sep 17 00:00:00 2001 From: Maze Winther Date: Sat, 25 Apr 2026 03:56:22 +0200 Subject: refactor: centralize editor interaction state Move timeline and preview gestures into dedicated controllers so hooks stay thin around editor state and DOM events. This also routes drag and playback synchronization through manager APIs and reuses persistent wasm surfaces so scrubbing, seeking, and preview rendering stay in sync. Made-with: Cursor --- apps/web/src/timeline/hooks/use-timeline-seek.ts | 195 ++++------------------- 1 file changed, 31 insertions(+), 164 deletions(-) (limited to 'apps/web/src/timeline/hooks/use-timeline-seek.ts') diff --git a/apps/web/src/timeline/hooks/use-timeline-seek.ts b/apps/web/src/timeline/hooks/use-timeline-seek.ts index 889177c9..56f68ed6 100644 --- a/apps/web/src/timeline/hooks/use-timeline-seek.ts +++ b/apps/web/src/timeline/hooks/use-timeline-seek.ts @@ -1,9 +1,9 @@ -import { useCallback, useRef } from "react"; -import type { MutableRefObject, RefObject } from "react"; -import { BASE_TIMELINE_PIXELS_PER_SECOND } from "@/timeline/scale"; -import { snappedSeekTime } from "opencut-wasm"; -import { TICKS_PER_SECOND } from "@/wasm"; +import { useEffect, useRef, type RefObject } from "react"; import { useEditor } from "@/editor/use-editor"; +import { + SeekController, + type SeekConfig, +} from "@/timeline/controllers/seek-controller"; interface UseTimelineSeekProps { playheadRef: RefObject; @@ -17,44 +17,6 @@ interface UseTimelineSeekProps { seek: (time: number) => void; } -function resetMouseTracking({ - mouseTrackingRef, -}: { - mouseTrackingRef: MutableRefObject<{ - isMouseDown: boolean; - downX: number; - downY: number; - downTime: number; - }>; -}) { - mouseTrackingRef.current = { - isMouseDown: false, - downX: 0, - downY: 0, - downTime: 0, - }; -} - -function setMouseTracking({ - mouseTrackingRef, - event, -}: { - mouseTrackingRef: MutableRefObject<{ - isMouseDown: boolean; - downX: number; - downY: number; - downTime: number; - }>; - event: React.MouseEvent; -}) { - mouseTrackingRef.current = { - isMouseDown: true, - downX: event.clientX, - downY: event.clientY, - downTime: event.timeStamp, - }; -} - export function useTimelineSeek({ playheadRef, trackLabelsRef, @@ -67,131 +29,36 @@ export function useTimelineSeek({ seek, }: UseTimelineSeekProps) { const editor = useEditor(); - const activeProject = editor.project.getActive(); - - const mouseTrackingRef = useRef({ - isMouseDown: false, - downX: 0, - downY: 0, - downTime: 0, - }); - - const handleTracksMouseDown = useCallback((event: React.MouseEvent) => { - if (event.button !== 0) return; - setMouseTracking({ mouseTrackingRef, event }); - }, []); - - const handleRulerMouseDown = useCallback((event: React.MouseEvent) => { - if (event.button !== 0) return; - setMouseTracking({ mouseTrackingRef, event }); - }, []); - - const shouldProcessTimelineClick = useCallback( - ({ event }: { event: React.MouseEvent }) => { - const target = event.target as HTMLElement; - const { isMouseDown, downX, downY, downTime } = mouseTrackingRef.current; - const deltaX = Math.abs(event.clientX - downX); - const deltaY = Math.abs(event.clientY - downY); - const deltaTime = event.timeStamp - downTime; - const isPlayhead = !!playheadRef.current?.contains(target); - const isTrackLabels = !!trackLabelsRef.current?.contains(target); - const shouldBlockForDrag = deltaX > 5 || deltaY > 5 || deltaTime > 500; - - if (!isMouseDown) return false; - if (shouldBlockForDrag) return false; - if (isSelecting) return false; - if (isPlayhead) return false; - if (isTrackLabels) { - clearSelectedElements(); - return false; - } - - return true; - }, - [isSelecting, clearSelectedElements, playheadRef, trackLabelsRef], - ); - - const handleTimelineSeek = useCallback( - ({ - event, - source, - }: { - event: React.MouseEvent; - source: "ruler" | "tracks"; - }) => { - const scrollContainer = - source === "ruler" ? rulerScrollRef.current : tracksScrollRef.current; - - if (!scrollContainer) return; - - const rect = scrollContainer.getBoundingClientRect(); - const mouseX = event.clientX - rect.left; - const scrollLeft = scrollContainer.scrollLeft; - - const rawTimeSeconds = Math.max( - 0, - Math.min( - duration / TICKS_PER_SECOND, - (mouseX + scrollLeft) / (BASE_TIMELINE_PIXELS_PER_SECOND * zoomLevel), - ), - ); - const rawTime = Math.round(rawTimeSeconds * TICKS_PER_SECOND); - - const rate = activeProject?.settings.fps; - const time = rate - ? (snappedSeekTime({ time: rawTime, duration, rate }) ?? rawTime) - : rawTime; - seek(time); - editor.project.setTimelineViewState({ - viewState: { - zoomLevel, - scrollLeft: scrollContainer.scrollLeft, - playheadTime: time, - }, - }); - }, - [ - duration, - zoomLevel, - rulerScrollRef, - tracksScrollRef, - seek, - editor, - activeProject?.settings.fps.numerator, - activeProject?.settings.fps.denominator, - ], - ); - - const handleTracksClick = useCallback( - (event: React.MouseEvent) => { - const shouldProcess = shouldProcessTimelineClick({ event }); - resetMouseTracking({ mouseTrackingRef }); + const config: SeekConfig = { + zoomLevel, + duration, + isSelecting, + getPlayheadEl: () => playheadRef.current, + getTrackLabelsEl: () => trackLabelsRef.current, + getRulerScrollEl: () => rulerScrollRef.current, + getTracksScrollEl: () => tracksScrollRef.current, + getActiveProjectFps: () => editor.project.getActive()?.settings.fps ?? null, + clearSelectedElements, + seek, + setTimelineViewState: (viewState) => + editor.project.setTimelineViewState({ viewState }), + }; - if (shouldProcess) { - clearSelectedElements(); - handleTimelineSeek({ event, source: "tracks" }); - } - }, - [shouldProcessTimelineClick, handleTimelineSeek, clearSelectedElements], - ); + const configRef = useRef(config); + configRef.current = config; - const handleRulerClick = useCallback( - (event: React.MouseEvent) => { - const shouldProcess = shouldProcessTimelineClick({ event }); - resetMouseTracking({ mouseTrackingRef }); + const controllerRef = useRef(null); + if (!controllerRef.current) { + controllerRef.current = new SeekController({ configRef }); + } + const controller = controllerRef.current; - if (shouldProcess) { - clearSelectedElements(); - handleTimelineSeek({ event, source: "ruler" }); - } - }, - [shouldProcessTimelineClick, handleTimelineSeek, clearSelectedElements], - ); + useEffect(() => () => controller.destroy(), [controller]); return { - handleTracksMouseDown, - handleTracksClick, - handleRulerMouseDown, - handleRulerClick, + handleTracksMouseDown: controller.onTracksMouseDown, + handleTracksClick: controller.onTracksClick, + handleRulerMouseDown: controller.onRulerMouseDown, + handleRulerClick: controller.onRulerClick, }; } -- cgit v1.3.1