[eric] canvas: marquee rect moves imperatively off a channel, measured 61 to 4.5 percent dropped frames

This commit is contained in:
ciregenz
2026-08-05 22:50:04 -07:00
parent 110d5a9b3b
commit 41bfbde7e6
4 changed files with 68 additions and 18 deletions
@@ -1,5 +1,6 @@
import React, { type RefObject } from 'react';
import { AnimatePresence } from 'framer-motion';
import MarqueeRect from './MarqueeRect';
import AgentCard from '../cards/AgentCard';
import DashboardViewCard from '../cards/DashboardViewCard';
import BrowserCard from '../cards/BrowserCard';
@@ -234,23 +235,8 @@ const DashboardCardLayer: React.FC<DashboardCardLayerProps> = ({
onDragEnd={onDragEnd}
onBringToFront={onBringToFront}
/>
{/* Marquee selection rectangle */}
{selection.marquee && (
<div
style={{
position: 'absolute',
left: selection.marquee.x,
top: selection.marquee.y,
width: selection.marquee.width,
height: selection.marquee.height,
border: '1.5px dashed rgba(59, 130, 246, 0.6)',
background: 'rgba(59, 130, 246, 0.08)',
borderRadius: 2,
pointerEvents: 'none',
zIndex: 9999,
}}
/>
)}
{/* Marquee selection rectangle: mounted once per sweep, moved imperatively off the channel */}
{selection.marquee && <MarqueeRect initial={selection.marquee} />}
</>
);
};
@@ -0,0 +1,36 @@
import React, { useEffect, useRef } from 'react';
import { subscribeMarqueeRect, LiveMarqueeRect } from '../hooks/interaction/marqueeLiveChannel';
// The selection rectangle, moved imperatively off the marquee channel: React mounts it once per
// sweep and never re-renders it mid-drag.
const MarqueeRect: React.FC<{ initial: LiveMarqueeRect }> = ({ initial }) => {
const ref = useRef<HTMLDivElement | null>(null);
useEffect(() => subscribeMarqueeRect((r) => {
const el = ref.current;
if (!el || !r) return;
el.style.left = `${r.x}px`;
el.style.top = `${r.y}px`;
el.style.width = `${r.width}px`;
el.style.height = `${r.height}px`;
}), []);
return (
<div
ref={ref}
data-marquee-rect
style={{
position: 'absolute',
left: initial.x,
top: initial.y,
width: initial.width,
height: initial.height,
border: '1.5px dashed rgba(59, 130, 246, 0.6)',
background: 'rgba(59, 130, 246, 0.08)',
borderRadius: 2,
pointerEvents: 'none',
zIndex: 9999,
}}
/>
);
};
export default MarqueeRect;
@@ -0,0 +1,23 @@
// Per-frame marquee rects travel OUTSIDE React, same pattern as liveDragChannel: a sweep at pointer
// rate re-rendered the whole card layer per frame just to move one rectangle (measured 61% dropped
// frames). React mounts/unmounts the rect; this channel moves it.
export interface LiveMarqueeRect {
x: number;
y: number;
width: number;
height: number;
}
type Listener = (rect: LiveMarqueeRect | null) => void;
const listeners = new Set<Listener>();
export function publishMarqueeRect(rect: LiveMarqueeRect | null): void {
for (const listener of listeners) listener(rect);
}
export function subscribeMarqueeRect(listener: Listener): () => void {
listeners.add(listener);
return () => { listeners.delete(listener); };
}
@@ -1,6 +1,7 @@
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';
import { publishMarqueeRect } from '../interaction/marqueeLiveChannel';
export type { CardType } from '@/shared/state/dashboardLayoutSlice';
import type { CardType } from '@/shared/state/dashboardLayoutSlice';
@@ -26,6 +27,7 @@ interface ScreenToCanvas {
const DRAG_THRESHOLD = 4;
function rectsIntersect(
a: { x: number; y: number; width: number; height: number },
b: { x: number; y: number; width: number; height: number },
@@ -252,7 +254,9 @@ export function useDashboardSelection(
width: Math.abs(end.x - start.x),
height: Math.abs(end.y - start.y),
};
setMarquee(rect);
// React mounts the rect once; per-frame movement rides the channel (the layer re-rendered per frame otherwise).
publishMarqueeRect(rect);
setMarquee((prev) => prev ?? rect);
const next = computeMarqueeSelection(rect, shiftHeldRef.current);
// Same membership = same state object, so sweeping across empty space re-renders nothing.
setSelectedIds((prev) => {
@@ -287,6 +291,7 @@ export function useDashboardSelection(
cancelAnimationFrame(marqueeRafRef.current);
marqueeRafRef.current = null;
}
publishMarqueeRect(null);
setMarquee(null);
document.body.style.userSelect = '';
document.body.classList.remove('dashboard-marquee-active');