diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.css b/packages/web/src/theme/ProgramExample/TraceDrawer.css index b3d017cde..95cc0f769 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.css +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.css @@ -337,8 +337,9 @@ color: var(--ifm-color-content); } -/* Instruction object footer - constant height, scrolls internally */ +/* Instruction object footer - user-resizable, scrolls internally */ .instruction-object-panel { + position: relative; flex-shrink: 0; display: flex; flex-direction: column; @@ -346,6 +347,35 @@ background: var(--ifm-background-color); } +/* Drag handle straddling the divider above the footer (matches the + * drawer's own resize handle). Only present while the object is shown. */ +.instruction-object-resize-handle { + position: absolute; + top: -6px; + left: 0; + right: 0; + height: 12px; + cursor: ns-resize; + display: flex; + align-items: center; + justify-content: center; + z-index: 10; + touch-action: none; +} + +.instruction-object-resize-bar { + width: 40px; + height: 4px; + background: var(--ifm-color-emphasis-400); + border-radius: 2px; + transition: background 0.2s; +} + +.instruction-object-resize-handle:hover .instruction-object-resize-bar, +.resizing-object .instruction-object-resize-bar { + background: var(--ifm-color-primary); +} + .instruction-object-header { position: static; text-transform: none; @@ -373,7 +403,7 @@ .instruction-object-body { flex-shrink: 0; min-height: 0; - max-height: 180px; + /* Height is driven by the drag handle (inline style); scroll inside. */ overflow: auto; } diff --git a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx index 488ff6850..79befd851 100644 --- a/packages/web/src/theme/ProgramExample/TraceDrawer.tsx +++ b/packages/web/src/theme/ProgramExample/TraceDrawer.tsx @@ -30,6 +30,13 @@ import { useTracePlayground } from "./TracePlaygroundContext"; import "./TraceDrawer.css"; +// Bounds for the draggable instruction-object footer (in px). +const OBJECT_MIN_HEIGHT = 80; +const OBJECT_DEFAULT_HEIGHT = 180; +// Vertical space kept for the controls + instructions/state row so the +// footer can never grow to swallow the whole drawer. +const OBJECT_ROW_RESERVE = 160; + export function TraceDrawer(): JSX.Element { return ( {() => } @@ -57,6 +64,72 @@ function TraceDrawerContent(): JSX.Element { const [traceError, setTraceError] = useState(null); const [storage, setStorage] = useState>({}); const [showInstructionObject, setShowInstructionObject] = useState(false); + const [objectHeight, setObjectHeight] = useState(OBJECT_DEFAULT_HEIGHT); + const [isResizingObject, setIsResizingObject] = useState(false); + + // Refs for the draggable instruction-object footer + const viewerRef = useRef(null); + const objectDragRef = useRef<{ startY: number; startHeight: number } | null>( + null, + ); + + // Clamp the footer height so it keeps a minimum while leaving room for + // the controls + instructions/state row above it. + const clampObjectHeight = useCallback((height: number): number => { + const viewer = viewerRef.current; + const max = viewer + ? Math.max(OBJECT_MIN_HEIGHT, viewer.clientHeight - OBJECT_ROW_RESERVE) + : Number.POSITIVE_INFINITY; + return Math.min(max, Math.max(OBJECT_MIN_HEIGHT, height)); + }, []); + + const handleObjectResizeStart = useCallback( + (e: React.PointerEvent) => { + e.preventDefault(); + objectDragRef.current = { startY: e.clientY, startHeight: objectHeight }; + e.currentTarget.setPointerCapture(e.pointerId); + setIsResizingObject(true); + }, + [objectHeight], + ); + + const handleObjectResizeMove = useCallback( + (e: React.PointerEvent) => { + const drag = objectDragRef.current; + if (!drag) return; + // Dragging up (clientY decreases) grows the object pane. + const next = drag.startHeight + (drag.startY - e.clientY); + setObjectHeight(clampObjectHeight(next)); + }, + [clampObjectHeight], + ); + + const handleObjectResizeEnd = useCallback( + (e: React.PointerEvent) => { + if (!objectDragRef.current) return; + objectDragRef.current = null; + setIsResizingObject(false); + if (e.currentTarget.hasPointerCapture(e.pointerId)) { + e.currentTarget.releasePointerCapture(e.pointerId); + } + }, + [], + ); + + // Re-clamp the footer height when the drawer/viewer is resized, so + // shrinking the drawer shrinks the footer rather than collapsing the + // instructions/state row. Keyed on trace length so it re-attaches when + // the viewer element mounts. + useEffect(() => { + const viewer = viewerRef.current; + if (!viewer || typeof ResizeObserver === "undefined") return; + + const observer = new ResizeObserver(() => { + setObjectHeight((h) => clampObjectHeight(h)); + }); + observer.observe(viewer); + return () => observer.disconnect(); + }, [trace.length, clampObjectHeight]); // Build PC -> instruction map for source highlighting const pcToInstruction = useMemo(() => { @@ -462,7 +535,7 @@ function TraceDrawerContent(): JSX.Element { )} {hasTrace && ( - + - + + {showInstructionObject && ( + + + + )} - setShowInstructionObject(e.target.checked) - } + onChange={(e) => { + const on = e.target.checked; + setShowInstructionObject(on); + if (on) { + setObjectHeight((h) => clampObjectHeight(h)); + } + }} /> ethdebug/format/instruction object @@ -605,7 +700,10 @@ function TraceDrawerContent(): JSX.Element { {showInstructionObject && ( - + {currentFormatInstruction ? ( {JSON.stringify(currentFormatInstruction, null, 2)}
ethdebug/format/instruction
{JSON.stringify(currentFormatInstruction, null, 2)}