mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 20:27:44 +02:00
[eric] canvas: dragging a card is compositor-only per frame, and the shell's heavy children bail instead of re-rendering per move
This commit is contained in:
@@ -221,6 +221,24 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
return () => window.removeEventListener('mousemove', onMove);
|
||||
}, [fullscreenCardId]);
|
||||
|
||||
// Stable identities for the memoized shell children: an inline closure or Array.from() here would hand them a fresh prop every render, and a card drag re-renders this component per frame (liveDragInfo), which is exactly when they must bail.
|
||||
const selectedIdList = React.useMemo(() => Array.from(selection.selectedIds.keys()) as string[], [selection.selectedIds]);
|
||||
const handleRestoreCard = React.useCallback((cardId: string, rect: { x: number; y: number; width: number; height: number }) => {
|
||||
canvas.actions.fitToCards([rect], 1.15, true);
|
||||
onHighlightCard?.(cardId);
|
||||
}, [canvas.actions, onHighlightCard]);
|
||||
const handleFocusCard = React.useCallback((cardId: string, rect: { x: number; y: number; width: number; height: number }) => {
|
||||
// A parked card sits off-canvas, so flying to its stored rect would land on empty space; unpark it first.
|
||||
if (minimizedCards[cardId]) dispatch(toggleMinimizeCard({ cardId }));
|
||||
canvas.actions.fitToCards([rect], 1.15, true);
|
||||
onHighlightCard?.(cardId);
|
||||
}, [minimizedCards, dispatch, canvas.actions, onHighlightCard]);
|
||||
const handleToggleApps = React.useCallback(() => setAppsWindowOpen((v) => !v), []);
|
||||
const handleDeleteSelected = React.useCallback(() => {
|
||||
deleteSelectedCards(selection.selectedIds, dispatch);
|
||||
selection.deselectAll();
|
||||
}, [selection, dispatch]);
|
||||
|
||||
// Gestures write the transform imperatively (no React commit per frame), so a foreign render mid-gesture would paint the stale committed transform for a frame. Re-applying live after EVERY render seals that; do not remove.
|
||||
React.useLayoutEffect(() => {
|
||||
canvas.actions.syncTransform();
|
||||
@@ -278,11 +296,8 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
browserCards={browserCards}
|
||||
viewCards={viewCards}
|
||||
outputs={outputs}
|
||||
selectedIds={Array.from(selection.selectedIds.keys())}
|
||||
onRestore={(cardId, rect) => {
|
||||
canvas.actions.fitToCards([rect], 1.15, true);
|
||||
onHighlightCard?.(cardId);
|
||||
}}
|
||||
selectedIds={selectedIdList}
|
||||
onRestore={handleRestoreCard}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -294,14 +309,9 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
browserCards={browserCards}
|
||||
workflowCards={workflowCards}
|
||||
outputs={outputs}
|
||||
selectedIds={Array.from(selection.selectedIds.keys())}
|
||||
onFocusCard={(cardId, rect) => {
|
||||
// A parked card sits off-canvas, so flying to its stored rect would land on empty space; unpark it first.
|
||||
if (minimizedCards[cardId]) dispatch(toggleMinimizeCard({ cardId }));
|
||||
canvas.actions.fitToCards([rect], 1.15, true);
|
||||
onHighlightCard?.(cardId);
|
||||
}}
|
||||
onApplications={() => setAppsWindowOpen((v) => !v)}
|
||||
selectedIds={selectedIdList}
|
||||
onFocusCard={handleFocusCard}
|
||||
onApplications={handleToggleApps}
|
||||
onAddBrowser={onAddBrowser}
|
||||
/>
|
||||
)}
|
||||
@@ -466,10 +476,7 @@ const DashboardCanvas: React.FC<DashboardCanvasProps> = ({
|
||||
onNewAgentBounceEnd={onNewAgentBounceEnd}
|
||||
onFitToView={onFitToView}
|
||||
onTidy={onTidy}
|
||||
onDeleteSelected={() => {
|
||||
deleteSelectedCards(selection.selectedIds, dispatch);
|
||||
selection.deselectAll();
|
||||
}}
|
||||
onDeleteSelected={handleDeleteSelected}
|
||||
hasSelection={selection.selectedIds.size > 0}
|
||||
onSearchPaletteClose={onSearchPaletteClose}
|
||||
toolbarPrefill={toolbarPrefill}
|
||||
|
||||
@@ -255,4 +255,4 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardCardLayer;
|
||||
export default React.memo(DashboardCardLayer);
|
||||
|
||||
@@ -193,4 +193,4 @@ const DashboardOverlays: React.FC<DashboardOverlaysProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
export default DashboardOverlays;
|
||||
export default React.memo(DashboardOverlays);
|
||||
|
||||
@@ -486,7 +486,9 @@ const AgentCard: React.FC<Props> = ({
|
||||
const panDy = (cs.panY - ds.startPanY) / z;
|
||||
const dx = rawDx / z - panDx;
|
||||
const dy = rawDy / z - panDy;
|
||||
setLocalDragPos({ x: ds.origX + dx, y: ds.origY + dy });
|
||||
// Imperative compositor move, ZERO React work per frame: a setState here re-rendered the whole card (an expanded chat re-runs its full transcript) on every pointermove and every edge-pan tick, which is the drag lag. The motion.div stays parked at its start position; this transform carries the delta; React commits once, at drag end.
|
||||
const el = cardBoxRef.current as HTMLElement | null;
|
||||
if (el) el.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
||||
onDragMove?.(dx, dy, clientX, clientY);
|
||||
}, [onDragMove, getCanvasState]);
|
||||
|
||||
@@ -537,6 +539,9 @@ const AgentCard: React.FC<Props> = ({
|
||||
justDraggedRef.current = true;
|
||||
requestAnimationFrame(() => { justDraggedRef.current = false; });
|
||||
}
|
||||
// Drop the imperative drag transform in the SAME frame the committed position lands, or the card paints double-offset for a beat.
|
||||
const el = cardBoxRef.current as HTMLElement | null;
|
||||
if (el) el.style.transform = '';
|
||||
onDragEnd?.(dx, dy, didDrag.current);
|
||||
dragState.current = null;
|
||||
didDrag.current = false;
|
||||
@@ -727,7 +732,8 @@ const AgentCard: React.FC<Props> = ({
|
||||
// f7's collapsed state: a session's browser (spawned by it or docked into it) shows under the pill.
|
||||
const browserShot = useBrowserPillShot(session.id, pillMode && !pillArtifact);
|
||||
|
||||
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta);
|
||||
// justDraggedRef: the motion.div parks at the START position for the whole imperative drag, so the end-of-drag commit must snap (not spring) to the final spot or the card visibly re-glides from where the drag began.
|
||||
const noTransition = isDragging || isResizing || (isSelected && !!multiDragDelta) || justDraggedRef.current;
|
||||
|
||||
const mdDx = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dx : 0;
|
||||
const mdDy = (!isDragging && isSelected && multiDragDelta) ? multiDragDelta.dy : 0;
|
||||
|
||||
@@ -261,4 +261,4 @@ function DesktopDock({
|
||||
);
|
||||
}
|
||||
|
||||
export default DesktopDock;
|
||||
export default React.memo(DesktopDock);
|
||||
|
||||
@@ -122,4 +122,4 @@ function MinimizedStack({ browserCards, viewCards, outputs, selectedIds, onResto
|
||||
);
|
||||
}
|
||||
|
||||
export default MinimizedStack;
|
||||
export default React.memo(MinimizedStack);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useCallback, useRef, useEffect, RefObject } from 'react';
|
||||
import { useState, useCallback, useMemo, useRef, useEffect, RefObject } from 'react';
|
||||
import type { CardPosition, ViewCardPosition, BrowserCardPosition, WorkflowCardPosition, WorkflowsHubPosition } from '@/shared/state/dashboardLayoutSlice';
|
||||
import { viewCardKey } from '@/shared/state/dashboardLayoutSlice';
|
||||
|
||||
@@ -289,7 +289,8 @@ export function useDashboardSelection(
|
||||
document.head.appendChild(style);
|
||||
}, []);
|
||||
|
||||
return {
|
||||
// Stable identity: consumers (the memoized card layer, the drag hook) receive this whole object as a prop, and a fresh literal per render re-rendered them all on every controller commit, including each frame of a card drag.
|
||||
return useMemo(() => ({
|
||||
selectedIds,
|
||||
selectedArray,
|
||||
marquee,
|
||||
@@ -300,5 +301,5 @@ export function useDashboardSelection(
|
||||
handleCanvasMouseDown,
|
||||
handleCanvasMouseMove,
|
||||
handleCanvasMouseUp,
|
||||
};
|
||||
}), [selectedIds, selectedArray, marquee, isSelected, selectCard, deselectAll, selectAll, handleCanvasMouseDown, handleCanvasMouseMove, handleCanvasMouseUp]);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user