fix(foundry): stabilize map window compositor and drag

This commit is contained in:
Codex
2026-08-09 17:29:29 +03:00
parent 3e9ae2ecac
commit 6c1265b8b6
6 changed files with 106 additions and 10 deletions
+57 -5
View File
@@ -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}