Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 32 additions & 2 deletions packages/web/src/theme/ProgramExample/TraceDrawer.css
Original file line number Diff line number Diff line change
Expand Up @@ -337,15 +337,45 @@
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;
border-top: 1px solid var(--ifm-color-emphasis-200);
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;
Expand Down Expand Up @@ -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;
}

Expand Down
110 changes: 104 additions & 6 deletions packages/web/src/theme/ProgramExample/TraceDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
<BrowserOnly fallback={null}>{() => <TraceDrawerContent />}</BrowserOnly>
Expand Down Expand Up @@ -57,6 +64,72 @@ function TraceDrawerContent(): JSX.Element {
const [traceError, setTraceError] = useState<string | null>(null);
const [storage, setStorage] = useState<Record<string, string>>({});
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<HTMLDivElement>(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<HTMLDivElement>) => {
e.preventDefault();
objectDragRef.current = { startY: e.clientY, startHeight: objectHeight };
e.currentTarget.setPointerCapture(e.pointerId);
setIsResizingObject(true);
},
[objectHeight],
);

const handleObjectResizeMove = useCallback(
(e: React.PointerEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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(() => {
Expand Down Expand Up @@ -462,7 +535,7 @@ function TraceDrawerContent(): JSX.Element {
)}

{hasTrace && (
<div className="trace-viewer">
<div className="trace-viewer" ref={viewerRef}>
<div className="trace-controls">
<button
onClick={jumpToStart}
Expand Down Expand Up @@ -589,23 +662,48 @@ function TraceDrawerContent(): JSX.Element {
</div>
</div>

<div className="instruction-object-panel">
<div
className={`instruction-object-panel${
isResizingObject ? " resizing-object" : ""
}`}
>
{showInstructionObject && (
<div
className="instruction-object-resize-handle"
onPointerDown={handleObjectResizeStart}
onPointerMove={handleObjectResizeMove}
onPointerUp={handleObjectResizeEnd}
onPointerCancel={handleObjectResizeEnd}
role="separator"
aria-orientation="horizontal"
aria-label="Resize instruction object panel"
>
<div className="instruction-object-resize-bar" />
</div>
)}
<div className="panel-header instruction-object-header">
<label className="instruction-object-toggle">
<input
type="checkbox"
checked={showInstructionObject}
onChange={(e) =>
setShowInstructionObject(e.target.checked)
}
onChange={(e) => {
const on = e.target.checked;
setShowInstructionObject(on);
if (on) {
setObjectHeight((h) => clampObjectHeight(h));
}
}}
/>
<span>
<code>ethdebug/format/instruction</code> object
</span>
</label>
</div>
{showInstructionObject && (
<div className="instruction-object-body">
<div
className="instruction-object-body"
style={{ height: objectHeight }}
>
{currentFormatInstruction ? (
<pre className="instruction-object-json">
{JSON.stringify(currentFormatInstruction, null, 2)}
Expand Down
Loading