Skip to content

Commit 5e02427

Browse files
committed
refactor(studio): isolate clip drag lifecycle
1 parent bad5a54 commit 5e02427

2 files changed

Lines changed: 287 additions & 216 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
import type { Dispatch, RefObject, SetStateAction } from "react";
2+
import { resolveTimelineDragEscape } from "./timelineEditing";
3+
import { commitDraggedClipMove } from "./timelineClipDragCommit";
4+
import type {
5+
BlockedClipState,
6+
DraggedClipState,
7+
ResizingClipState,
8+
} from "./timelineClipDragTypes";
9+
import type { TimelineGroupResizeSession } from "./timelineGroupEditing";
10+
import { commitTimelineGroupResize } from "./timelineGroupResizeCommit";
11+
import {
12+
beginTimelineOptimisticGesture,
13+
rollbackLatestTimelineOptimisticGesture,
14+
} from "./timelineOptimisticRevision";
15+
import type { TimelineEditCallbacks } from "./timelineCallbacks";
16+
import type { StackingPatch } from "./timelineStackingSync";
17+
import { usePlayerStore, type TimelineElement } from "../store/playerStore";
18+
19+
type UpdateElement = ReturnType<typeof usePlayerStore.getState>["updateElement"];
20+
21+
interface TimelineClipDragGestureLifecycleInput {
22+
draggedClipRef: RefObject<DraggedClipState | null>;
23+
resizingClipRef: RefObject<ResizingClipState | null>;
24+
blockedClipRef: RefObject<BlockedClipState | null>;
25+
groupResizeRef: RefObject<TimelineGroupResizeSession | null>;
26+
suppressClickRef: RefObject<boolean>;
27+
elementsRef: RefObject<TimelineElement[]>;
28+
trackOrderRef: RefObject<number[]>;
29+
setDraggedClip: Dispatch<SetStateAction<DraggedClipState | null>>;
30+
setResizingClip: Dispatch<SetStateAction<ResizingClipState | null>>;
31+
setShowPopover: (show: boolean) => void;
32+
setRangeSelectionRef: RefObject<((selection: null) => void) | null>;
33+
applyResizePointerRef: RefObject<(resize: ResizingClipState, clientX: number) => void>;
34+
syncClipDragAutoScrollRef: RefObject<(clientX: number, clientY: number) => void>;
35+
stopClipDragAutoScrollRef: RefObject<() => void>;
36+
updateDraggedClipPreviewRef: RefObject<
37+
(drag: DraggedClipState, clientX: number, clientY: number) => DraggedClipState
38+
>;
39+
restoreGroupResizeMembers: (session: TimelineGroupResizeSession, all?: boolean) => void;
40+
updateElement: UpdateElement;
41+
onMoveElementRef: RefObject<TimelineEditCallbacks["onMoveElement"]>;
42+
onMoveElementsRef: RefObject<TimelineEditCallbacks["onMoveElements"]>;
43+
onResizeElementRef: RefObject<TimelineEditCallbacks["onResizeElement"]>;
44+
onResizeElementsRef: RefObject<TimelineEditCallbacks["onResizeElements"]>;
45+
onBlockedEditAttemptRef: RefObject<
46+
((element: TimelineElement, intent: BlockedClipState["intent"]) => void) | undefined
47+
>;
48+
readZIndexRef: RefObject<((element: TimelineElement) => number) | undefined>;
49+
onStackingPatchesRef: RefObject<
50+
((patches: StackingPatch[]) => Promise<unknown> | void) | undefined
51+
>;
52+
refreshAfterLaneMoveRef: RefObject<(() => void) | undefined>;
53+
}
54+
55+
export function mountTimelineClipDragGestureLifecycle({
56+
draggedClipRef,
57+
resizingClipRef,
58+
blockedClipRef,
59+
groupResizeRef,
60+
suppressClickRef,
61+
elementsRef,
62+
trackOrderRef,
63+
setDraggedClip,
64+
setResizingClip,
65+
setShowPopover,
66+
setRangeSelectionRef,
67+
applyResizePointerRef,
68+
syncClipDragAutoScrollRef,
69+
stopClipDragAutoScrollRef,
70+
updateDraggedClipPreviewRef,
71+
restoreGroupResizeMembers,
72+
updateElement,
73+
onMoveElementRef,
74+
onMoveElementsRef,
75+
onResizeElementRef,
76+
onResizeElementsRef,
77+
onBlockedEditAttemptRef,
78+
readZIndexRef,
79+
onStackingPatchesRef,
80+
refreshAfterLaneMoveRef,
81+
}: TimelineClipDragGestureLifecycleInput): () => void {
82+
const clearSuppressedClick = () => {
83+
requestAnimationFrame(() => {
84+
suppressClickRef.current = false;
85+
});
86+
};
87+
88+
const handleResizePointerMove = (event: PointerEvent, resize: ResizingClipState) => {
89+
const distance = Math.abs(event.clientX - resize.originClientX);
90+
if (!resize.started && distance < 2) return;
91+
setShowPopover(false);
92+
setRangeSelectionRef.current?.(null);
93+
applyResizePointerRef.current(resize, event.clientX);
94+
syncClipDragAutoScrollRef.current(event.clientX, event.clientY);
95+
};
96+
97+
const handleBlockedPointerMove = (event: PointerEvent, blocked: BlockedClipState) => {
98+
const distance = Math.hypot(
99+
event.clientX - blocked.originClientX,
100+
event.clientY - blocked.originClientY,
101+
);
102+
const threshold = blocked.intent === "move" ? 4 : 2;
103+
if (!blocked.started && distance < threshold) return;
104+
if (!blocked.started) {
105+
blocked.started = true;
106+
blockedClipRef.current = blocked;
107+
suppressClickRef.current = true;
108+
setShowPopover(false);
109+
setRangeSelectionRef.current?.(null);
110+
onBlockedEditAttemptRef.current?.(blocked.element, blocked.intent);
111+
}
112+
};
113+
114+
const handleDragPointerMove = (event: PointerEvent, drag: DraggedClipState) => {
115+
const distance = Math.hypot(
116+
event.clientX - drag.originClientX,
117+
event.clientY - drag.originClientY,
118+
);
119+
if (!drag.started && distance < 4) return;
120+
setShowPopover(false);
121+
setRangeSelectionRef.current?.(null);
122+
setDraggedClip((previous) =>
123+
previous
124+
? updateDraggedClipPreviewRef.current(previous, event.clientX, event.clientY)
125+
: previous,
126+
);
127+
syncClipDragAutoScrollRef.current(event.clientX, event.clientY);
128+
};
129+
130+
const handleWindowPointerMove = (event: PointerEvent) => {
131+
const resize = resizingClipRef.current;
132+
if (resize) return handleResizePointerMove(event, resize);
133+
const blocked = blockedClipRef.current;
134+
if (blocked) return handleBlockedPointerMove(event, blocked);
135+
const drag = draggedClipRef.current;
136+
if (drag) handleDragPointerMove(event, drag);
137+
};
138+
139+
const commitResizePointerUp = (resize: ResizingClipState) => {
140+
resizingClipRef.current = null;
141+
setResizingClip(null);
142+
const groupSession = groupResizeRef.current;
143+
groupResizeRef.current = null;
144+
if (!resize.started) {
145+
if (groupSession) restoreGroupResizeMembers(groupSession);
146+
return;
147+
}
148+
suppressClickRef.current = true;
149+
clearSuppressedClick();
150+
if (groupSession) {
151+
commitTimelineGroupResize(groupSession, updateElement, onResizeElementsRef.current);
152+
return;
153+
}
154+
const hasChanged =
155+
resize.previewStart !== resize.element.start ||
156+
resize.previewDuration !== resize.element.duration ||
157+
resize.previewPlaybackStart !== resize.element.playbackStart;
158+
if (!hasChanged) return;
159+
160+
const resizeKey = resize.element.key ?? resize.element.id;
161+
const revision = beginTimelineOptimisticGesture(updateElement, [resizeKey]);
162+
updateElement(resizeKey, {
163+
start: resize.previewStart,
164+
duration: resize.previewDuration,
165+
playbackStart: resize.previewPlaybackStart,
166+
});
167+
Promise.resolve(
168+
onResizeElementRef.current?.(resize.element, {
169+
start: resize.previewStart,
170+
duration: resize.previewDuration,
171+
playbackStart: resize.previewPlaybackStart,
172+
}),
173+
).catch((error) => {
174+
rollbackLatestTimelineOptimisticGesture(updateElement, revision, [
175+
{
176+
key: resizeKey,
177+
updates: {
178+
start: resize.element.start,
179+
duration: resize.element.duration,
180+
playbackStart: resize.element.playbackStart,
181+
},
182+
},
183+
]);
184+
console.error("[Timeline] Failed to persist clip resize", error);
185+
});
186+
};
187+
188+
const finishBlockedPointerUp = (blocked: BlockedClipState) => {
189+
blockedClipRef.current = null;
190+
if (blocked.started) clearSuppressedClick();
191+
};
192+
193+
const commitDragPointerUp = (drag: DraggedClipState) => {
194+
draggedClipRef.current = null;
195+
setDraggedClip(null);
196+
if (!drag.started) return;
197+
suppressClickRef.current = true;
198+
clearSuppressedClick();
199+
commitDraggedClipMove(drag, {
200+
elements: elementsRef.current,
201+
trackOrder: trackOrderRef.current,
202+
updateElement,
203+
onMoveElement: onMoveElementRef.current,
204+
onMoveElements: onMoveElementsRef.current,
205+
selectedKeys: usePlayerStore.getState().selectedElementIds,
206+
readZIndex: readZIndexRef.current,
207+
onStackingPatches: onStackingPatchesRef.current,
208+
refreshAfterLaneMove: refreshAfterLaneMoveRef.current,
209+
});
210+
};
211+
212+
const handleWindowPointerUp = () => {
213+
stopClipDragAutoScrollRef.current();
214+
const resize = resizingClipRef.current;
215+
if (resize) return commitResizePointerUp(resize);
216+
const blocked = blockedClipRef.current;
217+
if (blocked) return finishBlockedPointerUp(blocked);
218+
const drag = draggedClipRef.current;
219+
if (!drag) {
220+
if (suppressClickRef.current) clearSuppressedClick();
221+
return;
222+
}
223+
commitDragPointerUp(drag);
224+
};
225+
226+
const handleWindowKeyDown = (event: KeyboardEvent) => {
227+
const decision = resolveTimelineDragEscape({
228+
key: event.key,
229+
drag: draggedClipRef.current,
230+
resize: resizingClipRef.current,
231+
blocked: blockedClipRef.current,
232+
});
233+
if (!decision.cancel) return;
234+
event.preventDefault();
235+
event.stopPropagation();
236+
stopClipDragAutoScrollRef.current();
237+
draggedClipRef.current = null;
238+
setDraggedClip(null);
239+
resizingClipRef.current = null;
240+
setResizingClip(null);
241+
const groupSession = groupResizeRef.current;
242+
groupResizeRef.current = null;
243+
if (groupSession) restoreGroupResizeMembers(groupSession);
244+
blockedClipRef.current = null;
245+
if (decision.suppressClick) suppressClickRef.current = true;
246+
};
247+
248+
window.addEventListener("pointermove", handleWindowPointerMove);
249+
window.addEventListener("pointerup", handleWindowPointerUp);
250+
window.addEventListener("pointercancel", handleWindowPointerUp);
251+
window.addEventListener("keydown", handleWindowKeyDown, true);
252+
return () => {
253+
stopClipDragAutoScrollRef.current();
254+
window.removeEventListener("pointermove", handleWindowPointerMove);
255+
window.removeEventListener("pointerup", handleWindowPointerUp);
256+
window.removeEventListener("pointercancel", handleWindowPointerUp);
257+
window.removeEventListener("keydown", handleWindowKeyDown, true);
258+
};
259+
}

0 commit comments

Comments
 (0)