터치로 전략컨트롤 동작

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,6 +1,7 @@
import React from 'react';
import {
handlePalettePointerDown,
handlePaletteTouchStart,
needsPointerPaletteDrag,
shouldSuppressPaletteClick,
writePaletteHtmlDragData,
@@ -55,6 +56,10 @@ export default function PaletteChip({
handlePalettePointerDown(e, buildPayload(), composite ? `${label} + ${label}` : label);
};
const onTouchStart = (e: React.TouchEvent) => {
handlePaletteTouchStart(e, buildPayload(), composite ? `${label} + ${label}` : label);
};
const handleClick = () => {
if (shouldSuppressPaletteClick()) return;
if (onSelect) onSelect();
@@ -83,6 +88,7 @@ export default function PaletteChip({
className={`se-palette-card ${color ? `se-palette-card--${color}` : 'se-palette-card--ind'}${composite ? ' se-palette-card--composite' : ''}${selected ? ' se-palette-card--selected' : ''}`}
onDragStart={onDragStart}
onPointerDown={onPointerDown}
onTouchStart={onTouchStart}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
role="button"
@@ -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}
@@ -3,6 +3,7 @@ import { createPortal } from 'react-dom';
import type { DefType } from '../../utils/strategyEditorShared';
import {
handlePalettePointerDown,
handlePaletteTouchStart,
needsPointerPaletteDrag,
writePaletteHtmlDragData,
type PaletteDragPayload,
@@ -94,6 +95,16 @@ function SidewaysFilterChip({
handlePalettePointerDown(e, payload, item.label);
};
const onTouchStart = (e: React.TouchEvent) => {
hideHint();
const payload: PaletteDragPayload = {
type: 'sidewaysFilter',
value: item.id,
label: item.label,
};
handlePaletteTouchStart(e, payload, item.label);
};
const handleClick = () => onSelect();
const handleDoubleClick = (e: React.MouseEvent) => {
e.preventDefault();
@@ -113,6 +124,7 @@ function SidewaysFilterChip({
className={`se-palette-card se-palette-card--range se-palette-card--range-${item.mode}${selected ? ' se-palette-card--selected' : ''}`}
onDragStart={onDragStart}
onPointerDown={onPointerDown}
onTouchStart={onTouchStart}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onMouseEnter={e => showHint(e.currentTarget)}