fix(foundry): stabilize map window compositor and drag
This commit is contained in:
@@ -53,6 +53,8 @@ type WorkspaceInteraction = {
|
||||
startClientX: number;
|
||||
startClientY: number;
|
||||
startRect: WorkspaceWindowRect;
|
||||
previewRect: WorkspaceWindowRect;
|
||||
startInlineTransform: string;
|
||||
};
|
||||
|
||||
const rectEquals = (left: WorkspaceWindowRect, right: WorkspaceWindowRect) => (
|
||||
@@ -138,7 +140,10 @@ export function WorkspaceWindow({
|
||||
}: WorkspaceWindowProps) {
|
||||
const titleId = useId();
|
||||
const descriptionId = useId();
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const interactionRef = useRef<WorkspaceInteraction | null>(null);
|
||||
const interactionFrameRef = useRef(0);
|
||||
const controlledRectRef = useRef(rect);
|
||||
const rectRef = useRef(rect);
|
||||
const onRectChangeRef = useRef(onRectChange);
|
||||
const headRef = useRef<HTMLElement>(null);
|
||||
@@ -147,7 +152,8 @@ export function WorkspaceWindow({
|
||||
const footerRef = useRef<HTMLElement>(null);
|
||||
const [interactionKind, setInteractionKind] = useState<WorkspaceInteraction["kind"] | null>(null);
|
||||
|
||||
rectRef.current = rect;
|
||||
controlledRectRef.current = rect;
|
||||
if (!interactionRef.current) rectRef.current = rect;
|
||||
onRectChangeRef.current = onRectChange;
|
||||
|
||||
const readBounds = (): WorkspaceBounds | null => {
|
||||
@@ -167,6 +173,7 @@ export function WorkspaceWindow({
|
||||
if (!bounds || maximized) return;
|
||||
|
||||
const clampToBounds = () => {
|
||||
if (interactionRef.current) return;
|
||||
const nextBounds = readBounds();
|
||||
if (nextBounds) emitRect(normalizeRect(rectRef.current, nextBounds, minWidth, minHeight));
|
||||
};
|
||||
@@ -192,6 +199,7 @@ export function WorkspaceWindow({
|
||||
|
||||
const fitContent = () => {
|
||||
frame = 0;
|
||||
if (interactionRef.current) return;
|
||||
const nextBounds = readBounds();
|
||||
if (!nextBounds) return;
|
||||
const normalized = normalizeRect(rectRef.current, nextBounds, minWidth, minHeight);
|
||||
@@ -228,6 +236,27 @@ export function WorkspaceWindow({
|
||||
}, [autoHeight, boundsRef, maximized, minHeight, minWidth, rect.width]);
|
||||
|
||||
useEffect(() => {
|
||||
const paintInteraction = () => {
|
||||
interactionFrameRef.current = 0;
|
||||
const interaction = interactionRef.current;
|
||||
const root = rootRef.current;
|
||||
if (!interaction || !root) return;
|
||||
if (interaction.kind === "move") {
|
||||
const deltaX = interaction.previewRect.x - interaction.startRect.x;
|
||||
const deltaY = interaction.previewRect.y - interaction.startRect.y;
|
||||
const prefix = interaction.startInlineTransform ? `${interaction.startInlineTransform} ` : "";
|
||||
root.style.transform = `${prefix}translate3d(${deltaX}px, ${deltaY}px, 0)`;
|
||||
return;
|
||||
}
|
||||
root.style.width = `${interaction.previewRect.width}px`;
|
||||
root.style.height = `${interaction.previewRect.height}px`;
|
||||
};
|
||||
|
||||
const scheduleInteractionPaint = () => {
|
||||
if (interactionFrameRef.current) return;
|
||||
interactionFrameRef.current = window.requestAnimationFrame(paintInteraction);
|
||||
};
|
||||
|
||||
const handlePointerMove = (event: globalThis.PointerEvent) => {
|
||||
const interaction = interactionRef.current;
|
||||
if (!interaction || event.pointerId !== interaction.pointerId) return;
|
||||
@@ -243,20 +272,40 @@ export function WorkspaceWindow({
|
||||
y: interaction.startRect.y + deltaY,
|
||||
}, bounds, minWidth, minHeight)
|
||||
: resizeRect(interaction.startRect, deltaX, deltaY, bounds, minWidth, minHeight);
|
||||
emitRect(nextRect);
|
||||
interaction.previewRect = nextRect;
|
||||
rectRef.current = nextRect;
|
||||
scheduleInteractionPaint();
|
||||
event.preventDefault();
|
||||
};
|
||||
|
||||
const endInteraction = (event: globalThis.PointerEvent) => {
|
||||
if (interactionRef.current?.pointerId !== event.pointerId) return;
|
||||
const interaction = interactionRef.current;
|
||||
if (!interaction || interaction.pointerId !== event.pointerId) return;
|
||||
if (interactionFrameRef.current) {
|
||||
window.cancelAnimationFrame(interactionFrameRef.current);
|
||||
interactionFrameRef.current = 0;
|
||||
}
|
||||
paintInteraction();
|
||||
const nextRect = interaction.previewRect;
|
||||
const root = rootRef.current;
|
||||
if (root) {
|
||||
root.style.left = `${nextRect.x}px`;
|
||||
root.style.top = `${nextRect.y}px`;
|
||||
root.style.width = `${nextRect.width}px`;
|
||||
root.style.height = `${nextRect.height}px`;
|
||||
root.style.transform = interaction.startInlineTransform;
|
||||
}
|
||||
interactionRef.current = null;
|
||||
setInteractionKind(null);
|
||||
if (!rectEquals(controlledRectRef.current, nextRect)) onRectChangeRef.current(nextRect);
|
||||
};
|
||||
|
||||
window.addEventListener("pointermove", handlePointerMove, { passive: false });
|
||||
window.addEventListener("pointerup", endInteraction);
|
||||
window.addEventListener("pointercancel", endInteraction);
|
||||
return () => {
|
||||
if (interactionFrameRef.current) window.cancelAnimationFrame(interactionFrameRef.current);
|
||||
interactionFrameRef.current = 0;
|
||||
window.removeEventListener("pointermove", handlePointerMove);
|
||||
window.removeEventListener("pointerup", endInteraction);
|
||||
window.removeEventListener("pointercancel", endInteraction);
|
||||
@@ -271,19 +320,21 @@ export function WorkspaceWindow({
|
||||
}, [maximized, resizable]);
|
||||
|
||||
const beginInteraction = (kind: WorkspaceInteraction["kind"], event: PointerEvent<HTMLElement>) => {
|
||||
if (maximized || event.button !== 0 || (kind === "resize" && !resizable)) return;
|
||||
if (interactionRef.current || maximized || event.button !== 0 || (kind === "resize" && !resizable)) return;
|
||||
if (kind === "move" && (event.target as HTMLElement).closest("button, input, select, textarea, a")) return;
|
||||
const bounds = readBounds();
|
||||
if (!bounds) return;
|
||||
const startRect = normalizeRect(rectRef.current, bounds, minWidth, minHeight);
|
||||
emitRect(startRect);
|
||||
interactionRef.current = {
|
||||
kind,
|
||||
pointerId: event.pointerId,
|
||||
startClientX: event.clientX,
|
||||
startClientY: event.clientY,
|
||||
startRect,
|
||||
previewRect: startRect,
|
||||
startInlineTransform: rootRef.current?.style.transform ?? "",
|
||||
};
|
||||
rectRef.current = startRect;
|
||||
setInteractionKind(kind);
|
||||
event.currentTarget.focus();
|
||||
event.preventDefault();
|
||||
@@ -320,6 +371,7 @@ export function WorkspaceWindow({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={rootRef}
|
||||
{...props}
|
||||
className={cn("nodedc-workspace-window nodedc-glass-material nodedc-ui-root", className)}
|
||||
data-active={active ? "true" : undefined}
|
||||
|
||||
Reference in New Issue
Block a user