fix(runtime): unmount written <Static> items to match Ink (G01) (#34)
* fix(runtime): unmount written <Static> items to match Ink (G01) Ink's <Static> renders `items.slice(index)` and advances `index` to `items.length` in a post-commit `useLayoutEffect`, so once an item has been painted it is removed from the tree and its component unmounts. vue-tui kept every Static item mounted forever: the component always mapped the full `props.items`, and write-once was enforced only at flush time via a positional `writtenCount` slice — the item components never tore down. Now the <Static> component owns a `cursor` (Ink's `index`) and renders only `items.slice(cursor)`. The renderer advances the cursor AFTER a commit has painted the fresh items, via an `onWritten` callback registered on the host static node — the vue-tui analogue of Ink's post-commit layout effect. This ordering guarantees items are written before they are sliced out and unmounted, so no item is ever lost or re-painted. Write-once bookkeeping moved from a positional `writtenCount` to a `writtenNodes` Set keyed by host-node identity. A single logical item expands to several host nodes (the <Text>/<Box> plus empty text-leaf fragment anchors Vue inserts), so a positional count mis-sliced once the cursor advanced; identity tracking is anchor-agnostic. The shared `paintStaticNode` helper paints children not yet in the set, records them, prunes unmounted entries, then fires `onWritten`; render.ts, render-to-string.ts and flushStatic all use it. Make the cursor mirror Ink fully so it can DECREASE, not just increase. `onWritten` now SETS the cursor to items.length (was max-with-current), and a length watch lowers it on shrink — needed because a shrink that leaves the already-sliced children empty produces no host mutation, hence no commit/ onWritten to re-sync. Without this, [A,B] (cursor→2) → [A] → [A,C] sliced(2)=[] and silently dropped C. paintStaticNode now always prunes and calls onWritten (even on empty commits), painting only when there are fresh children. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * chore(parity): ledger — G01 pr-open, reconcile G12 merged Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -21,8 +21,8 @@ import { attachYoga, detachYoga } from "./host/yoga.ts";
|
||||
import { buildNodeOps } from "./host/node-ops.ts";
|
||||
import { createCommitScheduler } from "./scheduler.ts";
|
||||
import { createAnimationScheduler } from "./animation-scheduler.ts";
|
||||
import { paint, paintIsolated } from "./paint/paint.ts";
|
||||
import { findStatics } from "./paint/static-channel.ts";
|
||||
import { paint } from "./paint/paint.ts";
|
||||
import { findStatics, paintStaticNode } from "./paint/static-channel.ts";
|
||||
import { createFrameWriter } from "./io/frame-writer.ts";
|
||||
import { bsu, esu, shouldSynchronize } from "./io/write-synchronized.ts";
|
||||
import {
|
||||
@@ -550,13 +550,10 @@ export function createApp(root: Component, rootProps?: RootProps | null): TuiApp
|
||||
const w = resolveSize(stdout).columns;
|
||||
let staticOutput = "";
|
||||
for (const stat of findStatics(tuiRoot)) {
|
||||
const fresh = stat.children.slice(stat.writtenCount);
|
||||
if (fresh.length === 0) continue;
|
||||
const staticFrame = paintIsolated(fresh, w, stat);
|
||||
const staticFrame = paintStaticNode(stat, w);
|
||||
if (staticFrame.length > 0) {
|
||||
staticOutput += staticFrame + "\n";
|
||||
}
|
||||
stat.writtenCount = stat.children.length;
|
||||
}
|
||||
const hasStaticOutput = staticOutput !== "" && staticOutput !== "\n";
|
||||
if (hasStaticOutput) {
|
||||
|
||||
Reference in New Issue
Block a user