터치로 전략컨트롤 동작

This commit is contained in:
Macbook
2026-06-15 00:19:31 +09:00
parent d8529ed582
commit f35f17df36
9 changed files with 345 additions and 103 deletions
@@ -1,4 +1,4 @@
import React, { useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import {
bindPaletteDragOverlayElement,
@@ -6,53 +6,67 @@ import {
notifyPaletteDragEnd,
notifyPaletteDragMove,
registerPaletteDragOverlay,
subscribePaletteDrag,
type PaletteDragOverlaySession,
} from '../../utils/paletteDragSession';
import { clientXYFromReact } from '../../utils/pointerDrag';
/**
* Tauri WebView — 드래그 overlay (라벨 위치는 ref 갱신, move 마다 React re-render 없음)
* pointer/touch 드래그 overlay (sessionRef 로 stale closure 방지, window touch 백업과 연동)
*/
export default function PaletteDragOverlay() {
const [session, setSession] = useState<PaletteDragOverlaySession | null>(null);
const overlayRef = useRef<HTMLDivElement | null>(null);
const sessionRef = useRef<PaletteDragOverlaySession | null>(null);
const labelRef = useRef<HTMLDivElement | null>(null);
sessionRef.current = session;
useEffect(() => {
registerPaletteDragOverlay({
mount: next => {
sessionRef.current = next;
setSession(next);
requestAnimationFrame(() => {
const label = labelRef.current;
if (label) {
label.style.left = `${next.x}px`;
label.style.top = `${next.y}px`;
}
});
},
unmount: () => setSession(null),
bindElement: el => {
overlayRef.current = el as HTMLDivElement | null;
unmount: () => {
sessionRef.current = null;
setSession(null);
bindPaletteDragOverlayElement(null);
},
bindElement: () => { /* ref callback 에서 처리 */ },
});
return () => registerPaletteDragOverlay(null);
}, []);
useLayoutEffect(() => {
bindPaletteDragOverlayElement(overlayRef.current);
return () => bindPaletteDragOverlayElement(null);
}, [session]);
useEffect(() => {
return subscribePaletteDrag(ev => {
if (ev.phase !== 'move' && ev.phase !== 'start') return;
const label = labelRef.current;
if (!label) return;
label.style.left = `${ev.x}px`;
label.style.top = `${ev.y}px`;
});
}, []);
useLayoutEffect(() => {
if (!session || !overlayRef.current) return;
try {
overlayRef.current.setPointerCapture(session.pointerId);
} catch {
/* ignore */
const overlayRefCallback = useCallback((el: HTMLDivElement | null) => {
bindPaletteDragOverlayElement(el);
const s = sessionRef.current;
if (el && s) {
try {
el.setPointerCapture(s.pointerId);
} catch {
/* window backup in paletteDragSession */
}
const label = labelRef.current;
if (label) {
label.style.left = `${s.x}px`;
label.style.top = `${s.y}px`;
}
}
}, [session]);
}, []);
const onPointerMove = useCallback((e: React.PointerEvent) => {
if (!session || e.pointerId !== session.pointerId) return;
const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault();
e.stopPropagation();
const label = labelRef.current;
@@ -61,31 +75,60 @@ export default function PaletteDragOverlay() {
label.style.top = `${e.clientY}px`;
}
notifyPaletteDragMove(e.clientX, e.clientY);
}, [session]);
}, []);
const onPointerUp = useCallback((e: React.PointerEvent) => {
if (!session || e.pointerId !== session.pointerId) return;
const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault();
e.stopPropagation();
notifyPaletteDragEnd(e.clientX, e.clientY);
}, [session]);
}, []);
const onPointerCancel = useCallback((e: React.PointerEvent) => {
if (!session || e.pointerId !== session.pointerId) return;
const s = sessionRef.current;
if (!s || e.pointerId !== s.pointerId) return;
e.preventDefault();
e.stopPropagation();
notifyPaletteDragCancel(e.clientX, e.clientY);
}, [session]);
}, []);
const onTouchMove = useCallback((e: React.TouchEvent) => {
const s = sessionRef.current;
if (!s || e.touches.length !== 1) return;
e.preventDefault();
const { x, y } = clientXYFromReact(e);
notifyPaletteDragMove(x, y);
}, []);
const onTouchEnd = useCallback((e: React.TouchEvent) => {
const s = sessionRef.current;
if (!s) return;
const { x, y } = clientXYFromReact(e);
e.preventDefault();
notifyPaletteDragEnd(x, y);
}, []);
const onTouchCancel = useCallback((e: React.TouchEvent) => {
const s = sessionRef.current;
if (!s) return;
const { x, y } = clientXYFromReact(e);
e.preventDefault();
notifyPaletteDragCancel(x, y);
}, []);
if (!session) return null;
return createPortal(
<div
ref={overlayRef}
ref={overlayRefCallback}
className="se-palette-drag-overlay"
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerCancel}
onTouchMove={onTouchMove}
onTouchEnd={onTouchEnd}
onTouchCancel={onTouchCancel}
>
<div
ref={labelRef}