返回 DeepSeek-Reasonix
resizeDrag.ts
根目录 / desktop / frontend / src / lib / resizeDrag.ts
1 interface StyleTarget {
2 style: {
3 setProperty(name: string, value: string): void;
4 };
5 }
6
7 interface AriaTarget {
8 setAttribute(name: string, value: string): void;
9 }
10
11 interface RafResizeUpdaterOptions {
12 target: StyleTarget;
13 separator?: AriaTarget | null;
14 cssVar: string;
15 onApply?: (value: number) => void;
16 }
17
18 export interface RafResizeUpdater {
19 schedule(value: number): void;
20 flush(): void;
21 cancel(): void;
22 }
23
24 export interface PointerResizeLifecycle {
25 /** Finish once, remove every listener, and release pointer capture. */
26 finish(): void;
27 }
28
29 /**
30 * Own one pointer-resize gesture across capture loss, cancellation, blur, and
31 * component cleanup. Every terminal path calls onFinish exactly once.
32 */
33 export function createPointerResizeLifecycle({
34 separator,
35 pointerId,
36 onMove,
37 onFinish,
38 }: {
39 separator: HTMLElement;
40 pointerId: number;
41 onMove: (event: PointerEvent) => void;
42 onFinish: () => void;
43 }): PointerResizeLifecycle {
44 let active = true;
45
46 function removeListeners() {
47 window.removeEventListener("pointermove", handleMove);
48 window.removeEventListener("pointerup", handlePointerDone);
49 window.removeEventListener("pointercancel", handlePointerDone);
50 window.removeEventListener("blur", finish);
51 separator.removeEventListener("lostpointercapture", handlePointerDone);
52 }
53
54 function finish() {
55 if (!active) return;
56 active = false;
57 removeListeners();
58 try {
59 if (separator.hasPointerCapture(pointerId)) separator.releasePointerCapture(pointerId);
60 } catch {
61 // WebView2 can release capture before the terminal pointer event.
62 }
63 onFinish();
64 }
65
66 function handleMove(event: PointerEvent) {
67 if (event.pointerId === pointerId) onMove(event);
68 }
69
70 function handlePointerDone(event: PointerEvent) {
71 if (event.pointerId === pointerId) finish();
72 }
73
74 try {
75 separator.setPointerCapture(pointerId);
76 } catch {
77 // Pointer capture is optional in older embedded browser runtimes.
78 }
79 window.addEventListener("pointermove", handleMove);
80 window.addEventListener("pointerup", handlePointerDone);
81 window.addEventListener("pointercancel", handlePointerDone);
82 window.addEventListener("blur", finish);
83 separator.addEventListener("lostpointercapture", handlePointerDone);
84
85 return { finish };
86 }
87
88 function roundedPixel(value: number): number {
89 return Math.round(value);
90 }
91
92 export function createRafResizeUpdater({ target, separator, cssVar, onApply }: RafResizeUpdaterOptions): RafResizeUpdater {
93 let frame: number | null = null;
94 let latest: number | null = null;
95
96 const apply = () => {
97 frame = null;
98 if (latest === null) return;
99 const rounded = roundedPixel(latest);
100 target.style.setProperty(cssVar, `${rounded}px`);
101 separator?.setAttribute("aria-valuenow", String(rounded));
102 onApply?.(rounded);
103 };
104
105 return {
106 schedule(value: number) {
107 latest = value;
108 if (frame !== null) return;
109 frame = requestAnimationFrame(apply);
110 },
111 flush() {
112 if (frame !== null) {
113 cancelAnimationFrame(frame);
114 frame = null;
115 }
116 apply();
117 },
118 cancel() {
119 if (frame === null) return;
120 cancelAnimationFrame(frame);
121 frame = null;
122 },
123 };
124 }
125
125 lines TYPESCRIPT