Просмотр исходного кода

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>
Colby McHenry 1 неделя назад
Родитель
Сommit
e9596af1cf
2 измененных файлов с 22 добавлено и 3 удалено
  1. 11 3
      ui/src/components/symbol/CalleeRail.svelte
  2. 11 0
      ui/src/views/SymbolView.svelte

+ 11 - 3
ui/src/components/symbol/CalleeRail.svelte

@@ -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 - 0
ui/src/views/SymbolView.svelte

@@ -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}