From b47cfbee7c4fd24e5d73e5753b4755db62a92a63 Mon Sep 17 00:00:00 2001 From: Kit Langton Date: Tue, 28 Jul 2026 22:43:46 -0400 Subject: [PATCH] fix(tui): reduce tab pulse allocations (#39433) --- packages/tui/src/component/tab-pulse.tsx | 46 ++++++++++++++----- packages/tui/test/component/tab-pulse.test.ts | 23 +++++++++- 2 files changed, 57 insertions(+), 12 deletions(-) diff --git a/packages/tui/src/component/tab-pulse.tsx b/packages/tui/src/component/tab-pulse.tsx index e76ff05f2a0..b06a4f189d2 100644 --- a/packages/tui/src/component/tab-pulse.tsx +++ b/packages/tui/src/component/tab-pulse.tsx @@ -1,6 +1,5 @@ import { OptimizedBuffer, Renderable, RGBA, type RenderableOptions, type RenderContext } from "@opentui/core" import { extend } from "@opentui/solid" -import { tint } from "../theme/color" type TabPulseOptions = RenderableOptions & { enabled?: boolean @@ -23,6 +22,7 @@ const COMPLETION_DURATION = 900 const COMPLETION_ATTACK = 0.16 const GLOW_TAIL = 12 const GLOW_OPACITY = 0.16 +const DEFAULT_FOREGROUND = RGBA.defaultForeground() const intensityAt = (index: number, front: number, head: number, tail: number) => { const distance = front - index return distance < 0 ? smootherstep(clamp(1 + distance / head)) : smootherstep(clamp(1 - distance / tail)) @@ -41,6 +41,26 @@ export const unreadGlowIntensity = (index: number, width: number) => { const tail = Math.min(GLOW_TAIL, Math.max(1, width - 2)) return smootherstep(clamp(1 - Math.max(0, index - 1) / tail)) } +export function blendTabPulseColor( + output: RGBA, + background: RGBA, + glowColor: RGBA, + runningColor: RGBA, + completionColor: RGBA, + glow: number, + running: number, + completion: number, +) { + output.r = background.r + (glowColor.r - background.r) * glow + output.g = background.g + (glowColor.g - background.g) * glow + output.b = background.b + (glowColor.b - background.b) * glow + output.r += (runningColor.r - output.r) * running + output.g += (runningColor.g - output.g) * running + output.b += (runningColor.b - output.b) * running + output.r += (completionColor.r - output.r) * completion + output.g += (completionColor.g - output.g) * completion + output.b += (completionColor.b - output.b) * completion +} class TabPulseRenderable extends Renderable { private _enabled: boolean private _active: boolean @@ -54,6 +74,7 @@ class TabPulseRenderable extends Renderable { private fadeClock: number | undefined private completionClock: number | undefined private completionPending = false + private renderColor = RGBA.fromInts(0, 0, 0) constructor(ctx: RenderContext, options: TabPulseOptions = {}) { const enabled = options.enabled ?? true @@ -191,17 +212,20 @@ class TabPulseRenderable extends Renderable { intensityAt(index, front, RUN_HEAD, RUN_TAIL), intensityAt(index, secondFront, RUN_HEAD, RUN_TAIL), ) - const background = this._glow - ? tint(this._backgroundColor, this._glowColor, unreadGlowIntensity(index, this.width) * GLOW_OPACITY) - : this._backgroundColor - const running = tint(background, this._color, intensity * 0.14 * runningOpacity) - buffer.setCell( - this.screenX + index, - this.screenY, - " ", - RGBA.defaultForeground(), - tint(running, this._completionColor, completionOpacity * 0.18), + const glow = this._glow ? unreadGlowIntensity(index, this.width) * GLOW_OPACITY : 0 + const running = intensity * 0.14 * runningOpacity + const completion = completionOpacity * 0.18 + blendTabPulseColor( + this.renderColor, + this._backgroundColor, + this._glowColor, + this._color, + this._completionColor, + glow, + running, + completion, ) + buffer.setCell(this.screenX + index, this.screenY, " ", DEFAULT_FOREGROUND, this.renderColor) } } } diff --git a/packages/tui/test/component/tab-pulse.test.ts b/packages/tui/test/component/tab-pulse.test.ts index d06cd382b6f..21561b6e298 100644 --- a/packages/tui/test/component/tab-pulse.test.ts +++ b/packages/tui/test/component/tab-pulse.test.ts @@ -1,5 +1,7 @@ import { expect, test } from "bun:test" -import { completionPulseOpacity, unreadGlowIntensity } from "../../src/component/tab-pulse" +import { RGBA } from "@opentui/core" +import { blendTabPulseColor, completionPulseOpacity, unreadGlowIntensity } from "../../src/component/tab-pulse" +import { tint } from "../../src/theme/color" test("completion pulse rises quickly and fades over the remaining duration", () => { expect(completionPulseOpacity(0)).toBe(0) @@ -24,3 +26,22 @@ test("unread glow reaches the normal background on compact tabs", () => { expect(unreadGlowIntensity(0, 8)).toBe(1) expect(unreadGlowIntensity(7, 8)).toBe(0) }) + +test("reuses a color while preserving the original glow and pulse blend stages", () => { + const output = RGBA.fromInts(0, 0, 0) + const background = RGBA.fromHex("#1a1b26") + const glowColor = RGBA.fromHex("#82aaff") + const runningColor = RGBA.fromHex("#c8d3f5") + const completionColor = RGBA.fromHex("#ff9e64") + + for (const glow of [0, 0.08, 0.16]) { + for (const running of [0, 0.01, 0.07, 0.14]) { + for (const completion of [0, 0.03, 0.09, 0.18]) { + blendTabPulseColor(output, background, glowColor, runningColor, completionColor, glow, running, completion) + expect(output.buffer).toEqual( + tint(tint(tint(background, glowColor, glow), runningColor, running), completionColor, completion).buffer, + ) + } + } + } +})