fix(ui): keep a callee row hidden until the rail has been measured (CG-44)

A row's position comes from measuring the laid-out DOM, so between Svelte
creating it and the first relayout it has no place to be. Drawing it at
top: 0 stacks the whole rail at its head for a frame; keeping the previous
symbol's coordinates is worse. It stays invisible until it has been placed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Colby McHenry
2026-08-27 00:12:07 -05:00
co-authored by Claude Opus 5
parent 5cecaabfc2
commit e9596af1cf
2 changed files with 22 additions and 3 deletions
+11 -3
View File
@@ -22,6 +22,8 @@
tops: number[]; tops: number[];
foldTop: number; foldTop: number;
noteTop: number; noteTop: number;
/** False until the view has measured the rail — see SymbolView. */
placed: boolean;
/** The focal symbol's file — a callee in it reads "same file", not a path. */ /** The focal symbol's file — a callee in it reads "same file", not a path. */
focalFile: string; focalFile: string;
/** The symbol this one was reached from, when it is a callee. */ /** The symbol this one was reached from, when it is a callee. */
@@ -31,7 +33,7 @@
onstepDown: (node: WireNodeRef) => void; onstepDown: (node: WireNodeRef) => void;
} }
let { model, tops, foldTop, noteTop, focalFile, originId, emptyReason, onstepDown }: Props = let { model, tops, foldTop, noteTop, placed, focalFile, originId, emptyReason, onstepDown }: Props =
$props(); $props();
function rowTitle(row: CalleeRow): string { function rowTitle(row: CalleeRow): string {
@@ -51,6 +53,7 @@
class:origin={node.id === originId} class:origin={node.id === originId}
class:hot={hot.is(node.id)} class:hot={hot.is(node.id)}
class:sel={railFocus.at('right', i)} class:sel={railFocus.at('right', i)}
class:unplaced={!placed}
style:top={`${tops[i] ?? 0}px`} style:top={`${tops[i] ?? 0}px`}
data-target={node.id} data-target={node.id}
role="button" role="button"
@@ -84,7 +87,7 @@
{/each} {/each}
{#if model.uncertain.length > 0} {#if model.uncertain.length > 0}
<details class="rfold" data-rail-fold style:top={`${foldTop}px`}> <details class="rfold" class:unplaced={!placed} data-rail-fold style:top={`${foldTop}px`}>
<summary> <summary>
Uncertain <span class="dim" Uncertain <span class="dim"
>· {model.uncertain.length} name-only match{model.uncertain.length === 1 ? '' : 'es'}, >· {model.uncertain.length} name-only match{model.uncertain.length === 1 ? '' : 'es'},
@@ -128,7 +131,7 @@
{#if model.rows.length === 0 && model.uncertain.length === 0} {#if model.rows.length === 0 && model.uncertain.length === 0}
<div class="rnote" style:top="60px">{emptyReason}</div> <div class="rnote" style:top="60px">{emptyReason}</div>
{:else if model.outsideCalls > 0 || model.outsideTypeRefs > 0 || model.hiddenGroups > 0} {:else if model.outsideCalls > 0 || model.outsideTypeRefs > 0 || model.hiddenGroups > 0}
<div class="rnote" style:top={`${noteTop}px`}> <div class="rnote" class:unplaced={!placed} style:top={`${noteTop}px`}>
{#if model.outsideCalls > 0} {#if model.outsideCalls > 0}
+{plural(model.outsideCalls, 'more call')} into symbols outside the index{#if model.outsideTypeRefs > 0}{' '}· +{plural(model.outsideCalls, 'more call')} into symbols outside the index{#if model.outsideTypeRefs > 0}{' '}·
{plural(model.outsideTypeRefs, 'type reference')}{/if}. {plural(model.outsideTypeRefs, 'type reference')}{/if}.
@@ -167,6 +170,11 @@
font-size: 11.5px; font-size: 11.5px;
} }
/* Positioned by measurement, so it must not paint before it is measured. */
.unplaced {
visibility: hidden;
}
.rrow { .rrow {
position: absolute; position: absolute;
right: 12px; right: 12px;
+11
View File
@@ -73,6 +73,14 @@
let stageMinHeight = $state(0); let stageMinHeight = $state(0);
let connectors = $state<Connector[]>([]); let connectors = $state<Connector[]>([]);
let overlay = $state({ width: 0, height: 0 }); let overlay = $state({ width: 0, height: 0 });
/**
* The rail has been measured at least once for the symbol on screen.
*
* Until it has, a row has no place to be: drawing it at `top: 0` would stack
* every row at the head of the rail for a frame, and drawing it at the
* PREVIOUS symbol's coordinates would be worse. It stays hidden instead.
*/
let placed = $state(false);
/* ---------------------------------------------------------------- data -- */ /* ---------------------------------------------------------------- data -- */
@@ -90,6 +98,7 @@
source = null; source = null;
railFocus.reset(); railFocus.reset();
hot.set(null); hot.set(null);
placed = false;
void project.ensure(); void project.ensure();
let node: WireSymbolPayload; let node: WireSymbolPayload;
@@ -322,6 +331,7 @@
}); });
connectors = next; connectors = next;
overlay = { width: inner.scrollWidth, height: Math.max(inner.offsetHeight, stageMinHeight) }; overlay = { width: inner.scrollWidth, height: Math.max(inner.offsetHeight, stageMinHeight) };
placed = true;
} }
let scheduled = false; let scheduled = false;
@@ -460,6 +470,7 @@
{tops} {tops}
{foldTop} {foldTop}
{noteTop} {noteTop}
{placed}
focalFile={payload.node.file} focalFile={payload.node.file}
originId={originRight} originId={originRight}
emptyReason={emptyCalleeReason} emptyReason={emptyCalleeReason}