diff options
| author | user <user@clank> | 2026-07-26 09:32:49 +0200 |
|---|---|---|
| committer | user <user@clank> | 2026-08-12 19:34:35 +0200 |
| commit | 84483b6d6020eee944ada9822c5d6229dc567e04 (patch) | |
| tree | 956db77c19c50c7840be631970d7cac3bae65509 /pi-ext | |
| parent | add obnoxious-working: loud spinners + sl0p-swag status messages (diff) | |
| download | sl0ppi-84483b6d6020eee944ada9822c5d6229dc567e04.tar.gz sl0ppi-84483b6d6020eee944ada9822c5d6229dc567e04.tar.xz sl0ppi-84483b6d6020eee944ada9822c5d6229dc567e04.zip | |
slop-tool-boxes: stop full-redraw flicker on tall, top-clipped active boxes
An in-flight tool box taller than the viewport (top scrolled off-screen)
flickered violently the whole time it was active. Cause: the working spinner
(and pi's default spinner) drive ui.requestRender() at ~10-16fps, and the
active call slot recomputed a wall-clock pulse color every render, recoloring
the TOP border. tui.js falls back to a full-screen redraw when the first
changed line is above the viewport (firstChanged < prevViewportTop), so every
spinner tick = one full clear+repaint.
Fix: once the result slot is streaming (box can be tall), freeze the call
slot pulse color, caching built lines per (width, inner). The header/command
is static, so the top border is byte-identical between render ticks and never
triggers an above-viewport change. The pre-output phase (box still fully
visible) keeps the live pulse + ticking runtime; the result slot still pulses
its in-viewport bottom border. NOTES.md documents this as flicker #4.
Confirmed by repro: massive flicker with the fix reverted, calm with it in.
Diffstat (limited to 'pi-ext')
| -rw-r--r-- | pi-ext/NOTES.md | 28 | ||||
| -rw-r--r-- | pi-ext/slop-tool-boxes.ts | 28 |
2 files changed, 53 insertions, 3 deletions
diff --git a/pi-ext/NOTES.md b/pi-ext/NOTES.md index f3125cb..db62e7a 100644 --- a/pi-ext/NOTES.md +++ b/pi-ext/NOTES.md @@ -1,5 +1,33 @@ # Notes / TODO +## ✅ FIXED: tool-box flicker #4 (off-screen top border + external render ticks) + +**Symptom:** an in-flight tool box taller than the viewport, scrolled so its TOP +is clipped off-screen, flickers violently (full-screen redraw) the entire time +it is active; stops the instant it settles. + +**Cause:** two things compound. (1) `obnoxious-working.ts` (and pi's own default +spinner) animate via `ui.requestRender()` at ~10–16fps, so the whole UI +re-renders even when the tool is silent. (2) The active tool-box render did not +cache (`cw = -1`) and recomputed a wall-clock `pulsePaint()` every render, so the +**top border** (owned by the *call* slot) changed color on every tick. tui.js +only diff-renders what's on screen: `if (firstChanged < prevViewportTop) +fullRender(true)`. A recolored top border sitting *above* the viewport is +`firstChanged < viewportTop` → full clear+repaint, every spinner frame. + +(The *result* slot only recolors its **bottom** border, which stays in-viewport, +so it was never the culprit. Streaming appends new lines at the bottom too, so it +never mutates off-screen lines.) + +**Fix:** while active **and** the result slot has started streaming (box can be +tall), FREEZE the call slot's pulse color, caching the built lines per +`(width, inner)`. The command/header is static, so the top border becomes +byte-identical between render ticks → `firstChanged` never lands above the +viewport → no full redraw. The pre-output phase (box still short + fully visible) +keeps the live pulse and the ticking runtime. Net: top border no longer pulses +while output streams (usually off-screen anyway); the result slot's bottom border +still pulses in-viewport. + ## ✅ FIXED: tool-box flicker × 3 (streaming rails, startup storm, streaming timer) **Final invariant: the pulse NEVER forces a repaint.** The pulse color is a pure diff --git a/pi-ext/slop-tool-boxes.ts b/pi-ext/slop-tool-boxes.ts index 0b72dcf..2a3bd94 100644 --- a/pi-ext/slop-tool-boxes.ts +++ b/pi-ext/slop-tool-boxes.ts @@ -396,6 +396,13 @@ type FrameState = { summaryText?: string; callW: number; callLines?: string[]; + // Active-render freeze cache (anti-flicker): while streaming, hold the pulse + // color steady per (width, content) so the top border—often scrolled off the + // top of a tall box—stays byte-identical between the working-spinner's render + // ticks. Recoloring an off-screen line forces a full-screen redraw. + callActiveW?: number; + callActiveInner?: string[]; + callActiveLines?: string[]; resultRef?: any; resultExpanded?: boolean; resultW: number; @@ -524,9 +531,23 @@ function registerFramedTool(pi: ExtensionAPI, def: ToolDef) { return out; }; if (activeNow) { - cw = -1; // don't cache while animating; recompute solid once settled - // in-flight: pulsing top/bottom borders + STATIC gray dotted rails - return build(pulsePaint(), theme.fg("muted", DOTTED_RAIL)); + cw = -1; // settled cache must rebuild once the row finishes + const rail = theme.fg("muted", DOTTED_RAIL); + // Pre-output (no result slot yet): the whole box is short + visible, + // so keep the live pulse (and ticking runtime in the bottom border). + if (!st.resultActive) return build(pulsePaint(), rail); + // Result is streaming below — this slot owns the TOP border, which may + // be scrolled off-screen. Freeze the pulse color while our content + + // width are unchanged so the top border is byte-identical between the + // spinner's render ticks; otherwise every tick recolors an off-screen + // line and tui.js escalates to a full-screen redraw (violent flicker + // on tall, top-clipped tool boxes). See NOTES.md. + if (width !== st.callActiveW || inner !== st.callActiveInner) { + st.callActiveW = width; + st.callActiveInner = inner; + st.callActiveLines = build(pulsePaint(), rail); + } + return st.callActiveLines!; } if (width === cw) return cl; // settled: solid borders + rails in the status color @@ -538,6 +559,7 @@ function registerFramedTool(pi: ExtensionAPI, def: ToolDef) { invalidate() { cw = -1; st.callW = -1; + st.callActiveW = -1; // drop the frozen active lines on theme change st.callArgsRef = undefined; // rebuild native on theme change st.callInner?.invalidate?.(); }, |
