mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-02 16:26:14 -04:00
Compare commits
1 Commits
v2
...
tab-pulse-layers
| Author | SHA1 | Date | |
|---|---|---|---|
| ced6747737 |
@@ -1,5 +1,5 @@
|
||||
import { RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core"
|
||||
import { For, Show, createComputed, createEffect, createMemo, createSignal, untrack } from "solid-js"
|
||||
import { For, Show, createComputed, createEffect, createMemo, createSignal, onCleanup, untrack } from "solid-js"
|
||||
import { useTerminalDimensions } from "@opentui/solid"
|
||||
import { useConfig } from "../config"
|
||||
import { useSessionTabs } from "../context/session-tabs"
|
||||
@@ -18,7 +18,7 @@ import {
|
||||
import { createAnimatable, spring, tween } from "../ui/animation"
|
||||
import { Locale } from "../util/locale"
|
||||
import { stringWidth } from "../util/string-width"
|
||||
import { TabPulse, unreadGlowIntensity } from "./tab-pulse"
|
||||
import { TabPulse, TabPulseTimeline, unreadGlowIntensity } from "./tab-pulse"
|
||||
import { tint } from "../theme/color"
|
||||
import { SESSION_SIDEBAR_WIDTH } from "../ui/layout"
|
||||
import { projectName } from "../util/project"
|
||||
@@ -97,6 +97,14 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
),
|
||||
)
|
||||
const itemStatus = (tab: SessionTab) => statuses().get(tab.sessionID)!
|
||||
const pulseTimelines = new Map<string, TabPulseTimeline>()
|
||||
const pulseTimeline = (sessionID: string) => {
|
||||
const existing = pulseTimelines.get(sessionID)
|
||||
if (existing) return existing
|
||||
const created = new TabPulseTimeline()
|
||||
pulseTimelines.set(sessionID, created)
|
||||
return created
|
||||
}
|
||||
let rail: { screenY: number } | undefined
|
||||
let scroll: ScrollBoxRenderable | undefined
|
||||
|
||||
@@ -132,6 +140,8 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
<box flexShrink={0} flexDirection="column" gap={1}>
|
||||
<For each={items()}>
|
||||
{(tab, index) => {
|
||||
const timeline = pulseTimeline(tab.sessionID)
|
||||
onCleanup(() => pulseTimelines.delete(tab.sessionID))
|
||||
const selected = () => activeID() === tab.sessionID
|
||||
const status = createMemo(() => itemStatus(tab))
|
||||
const [sweepLevel, setSweepLevel] = createSignal(0)
|
||||
@@ -185,6 +195,10 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
const detailColor = createMemo(() => tint(theme.text.subdued, pulseBackground(), 0.35))
|
||||
const glows = () => status().glows
|
||||
const previous = createMemo(() => items()[index() - 1])
|
||||
const previousTimeline = () => {
|
||||
const tab = previous()
|
||||
return tab ? pulseTimeline(tab.sessionID) : undefined
|
||||
}
|
||||
const previousStatus = createMemo(() => {
|
||||
const tab = previous()
|
||||
return tab
|
||||
@@ -244,24 +258,30 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
top={-1}
|
||||
edge="above"
|
||||
enabled={animations()}
|
||||
active={runs()}
|
||||
outerActive={previousRuns()}
|
||||
promptPulse={status().promptPulse}
|
||||
outerPromptPulse={previousStatus().promptPulse}
|
||||
complete={complete() && !status().attention}
|
||||
outerComplete={previousStatus().complete && !previousStatus().attention}
|
||||
glow={glows()}
|
||||
outerGlow={previousGlows()}
|
||||
breathe={status().attention}
|
||||
outerBreathe={previousStatus().attention}
|
||||
color={separatorLowerPulseColor()}
|
||||
outerColor={separatorUpperPulseColor()}
|
||||
glowColor={separatorLowerColor()}
|
||||
outerGlowColor={separatorUpperColor()}
|
||||
glowTail={8}
|
||||
outerGlowTail={5}
|
||||
completionColor={separatorLowerColor()}
|
||||
outerCompletionColor={separatorUpperColor()}
|
||||
layer={{
|
||||
timeline,
|
||||
active: runs(),
|
||||
promptPulse: status().promptPulse,
|
||||
complete: complete() && !status().attention,
|
||||
glow: glows(),
|
||||
breathe: status().attention,
|
||||
color: separatorLowerPulseColor(),
|
||||
glowColor: separatorLowerColor(),
|
||||
glowTail: 8,
|
||||
completionColor: separatorLowerColor(),
|
||||
}}
|
||||
edgeLayer={{
|
||||
timeline: previousTimeline(),
|
||||
active: previousRuns(),
|
||||
promptPulse: previousStatus().promptPulse,
|
||||
complete: previousStatus().complete && !previousStatus().attention,
|
||||
glow: previousGlows(),
|
||||
breathe: previousStatus().attention,
|
||||
color: separatorUpperPulseColor(),
|
||||
glowColor: separatorUpperColor(),
|
||||
glowTail: 5,
|
||||
completionColor: separatorUpperColor(),
|
||||
}}
|
||||
backgroundColor={theme.background.default}
|
||||
/>
|
||||
<Show when={index() === items().length - 1}>
|
||||
@@ -269,38 +289,41 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
top={2}
|
||||
edge="below"
|
||||
enabled={animations()}
|
||||
active={runs()}
|
||||
outerActive={false}
|
||||
promptPulse={status().promptPulse}
|
||||
outerPromptPulse={0}
|
||||
complete={complete() && !status().attention}
|
||||
outerComplete={false}
|
||||
glow={glows()}
|
||||
outerGlow={false}
|
||||
breathe={status().attention}
|
||||
outerBreathe={false}
|
||||
color={tint(theme.background.default, theme.text.default, 0.04)}
|
||||
outerColor={tint(theme.background.default, theme.text.default, 0.006)}
|
||||
glowColor={tint(theme.background.default, glowHue(), 0.1)}
|
||||
outerGlowColor={theme.background.default}
|
||||
glowTail={8}
|
||||
outerGlowTail={5}
|
||||
completionColor={tint(theme.background.default, glowHue(), 0.1)}
|
||||
outerCompletionColor={theme.background.default}
|
||||
layer={{
|
||||
timeline,
|
||||
active: runs(),
|
||||
promptPulse: status().promptPulse,
|
||||
complete: complete() && !status().attention,
|
||||
glow: glows(),
|
||||
breathe: status().attention,
|
||||
color: tint(theme.background.default, theme.text.default, 0.04),
|
||||
glowColor: tint(theme.background.default, glowHue(), 0.1),
|
||||
glowTail: 8,
|
||||
completionColor: tint(theme.background.default, glowHue(), 0.1),
|
||||
}}
|
||||
edgeLayer={{
|
||||
color: tint(theme.background.default, theme.text.default, 0.006),
|
||||
glowColor: theme.background.default,
|
||||
glowTail: 5,
|
||||
completionColor: theme.background.default,
|
||||
}}
|
||||
backgroundColor={theme.background.default}
|
||||
/>
|
||||
</Show>
|
||||
<box height={1} width="100%" flexDirection="row" position="relative">
|
||||
<TabPulse
|
||||
enabled={animations()}
|
||||
active={runs()}
|
||||
promptPulse={status().promptPulse}
|
||||
complete={complete() && !status().attention}
|
||||
glow={glows()}
|
||||
breathe={status().attention}
|
||||
color={pulseColor()}
|
||||
glowColor={glowColor()}
|
||||
completionColor={glowColor()}
|
||||
layer={{
|
||||
timeline,
|
||||
active: runs(),
|
||||
promptPulse: status().promptPulse,
|
||||
complete: complete() && !status().attention,
|
||||
glow: glows(),
|
||||
breathe: status().attention,
|
||||
color: pulseColor(),
|
||||
glowColor: glowColor(),
|
||||
completionColor: glowColor(),
|
||||
}}
|
||||
backgroundColor={pulseBackground()}
|
||||
onLevel={setSweepLevel}
|
||||
/>
|
||||
@@ -346,15 +369,18 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
||||
<box height={1} width="100%" position="relative" flexDirection="row">
|
||||
<TabPulse
|
||||
enabled={animations()}
|
||||
active={runs()}
|
||||
promptPulse={status().promptPulse}
|
||||
complete={complete() && !status().attention}
|
||||
glow={glows()}
|
||||
breathe={status().attention}
|
||||
color={detailPulseColor()}
|
||||
glowColor={detailGlowColor()}
|
||||
glowTail={10}
|
||||
completionColor={detailGlowColor()}
|
||||
layer={{
|
||||
timeline,
|
||||
active: runs(),
|
||||
promptPulse: status().promptPulse,
|
||||
complete: complete() && !status().attention,
|
||||
glow: glows(),
|
||||
breathe: status().attention,
|
||||
color: detailPulseColor(),
|
||||
glowColor: detailGlowColor(),
|
||||
glowTail: 10,
|
||||
completionColor: detailGlowColor(),
|
||||
}}
|
||||
backgroundColor={pulseBackground()}
|
||||
/>
|
||||
<box zIndex={1} width="100%" flexDirection="row" paddingLeft={numberWidth() + 1} paddingRight={2}>
|
||||
@@ -651,15 +677,17 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
||||
>
|
||||
<TabPulse
|
||||
enabled={animations()}
|
||||
active={status().busy && !status().attention}
|
||||
promptPulse={status().promptPulse}
|
||||
complete={status().complete && !status().attention}
|
||||
glow={glows()}
|
||||
breathe={status().attention}
|
||||
color={pulseColor()}
|
||||
glowColor={glowColor()}
|
||||
flashColor={flashColor()}
|
||||
completionColor={accent()}
|
||||
layer={{
|
||||
active: status().busy && !status().attention,
|
||||
promptPulse: status().promptPulse,
|
||||
complete: status().complete && !status().attention,
|
||||
glow: glows(),
|
||||
breathe: status().attention,
|
||||
color: pulseColor(),
|
||||
glowColor: glowColor(),
|
||||
flashColor: flashColor(),
|
||||
completionColor: accent(),
|
||||
}}
|
||||
backgroundColor={background()}
|
||||
onLevel={setSweepLevel}
|
||||
/>
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import { OptimizedBuffer, Renderable, RGBA, type RenderableOptions, type RenderContext } from "@opentui/core"
|
||||
import { extend } from "@opentui/solid"
|
||||
|
||||
export type TabPulseState = {
|
||||
active?: boolean
|
||||
promptPulse?: number
|
||||
complete?: boolean
|
||||
glow?: boolean
|
||||
breathe?: boolean
|
||||
}
|
||||
|
||||
export type TabPulseLayer = TabPulseState & {
|
||||
timeline?: TabPulseTimeline
|
||||
color: RGBA
|
||||
glowColor?: RGBA
|
||||
glowTail?: number
|
||||
flashColor?: RGBA
|
||||
completionColor?: RGBA
|
||||
}
|
||||
|
||||
type TabPulseOptions = RenderableOptions<TabPulseRenderable> & {
|
||||
edge?: "above" | "below"
|
||||
enabled?: boolean
|
||||
active?: boolean
|
||||
outerActive?: boolean
|
||||
promptPulse?: number
|
||||
outerPromptPulse?: number
|
||||
complete?: boolean
|
||||
outerComplete?: boolean
|
||||
glow?: boolean
|
||||
outerGlow?: boolean
|
||||
breathe?: boolean
|
||||
outerBreathe?: boolean
|
||||
color?: RGBA
|
||||
outerColor?: RGBA
|
||||
glowColor?: RGBA
|
||||
outerGlowColor?: RGBA
|
||||
glowTail?: number
|
||||
outerGlowTail?: number
|
||||
flashColor?: RGBA
|
||||
outerFlashColor?: RGBA
|
||||
completionColor?: RGBA
|
||||
outerCompletionColor?: RGBA
|
||||
layer?: TabPulseLayer
|
||||
edgeLayer?: TabPulseLayer
|
||||
backgroundColor?: RGBA
|
||||
/** Reports the running sweep's intensity at the tab number's cell, quantized; 0 when idle. */
|
||||
onLevel?: (level: number) => void
|
||||
@@ -310,71 +309,163 @@ class PulseState {
|
||||
}
|
||||
}
|
||||
|
||||
export class TabPulseTimeline {
|
||||
private state: PulseState
|
||||
private frame = -1
|
||||
|
||||
constructor(options: TabPulseState = {}, enabled = true) {
|
||||
this.state = new PulseState({
|
||||
enabled,
|
||||
active: options.active ?? false,
|
||||
promptPulse: options.promptPulse ?? 0,
|
||||
complete: options.complete ?? false,
|
||||
glow: options.glow ?? false,
|
||||
breathe: options.breathe ?? false,
|
||||
})
|
||||
}
|
||||
|
||||
set(options: TabPulseState, enabled: boolean) {
|
||||
return [
|
||||
this.state.setEnabled(enabled),
|
||||
// Stopping a run arms completion; apply complete afterward so an atomic idle update can consume it.
|
||||
this.state.setActive(options.active ?? false),
|
||||
this.state.setPromptPulse(options.promptPulse ?? 0),
|
||||
this.state.setComplete(options.complete ?? false),
|
||||
this.state.setGlow(options.glow ?? false),
|
||||
this.state.setBreathe(options.breathe ?? false),
|
||||
].some(Boolean)
|
||||
}
|
||||
|
||||
setEnabled(value: boolean) {
|
||||
return this.state.setEnabled(value)
|
||||
}
|
||||
|
||||
advance(deltaTime: number, frame: number) {
|
||||
if (frame === this.frame) return
|
||||
this.frame = frame
|
||||
this.state.advance(deltaTime)
|
||||
}
|
||||
|
||||
get live() {
|
||||
return this.state.live
|
||||
}
|
||||
|
||||
get running() {
|
||||
return this.state.running
|
||||
}
|
||||
|
||||
get completion() {
|
||||
return this.state.completion
|
||||
}
|
||||
|
||||
get flash() {
|
||||
return this.state.flash
|
||||
}
|
||||
|
||||
get glowLevel() {
|
||||
return this.state.glowLevel
|
||||
}
|
||||
|
||||
fronts(width: number) {
|
||||
return this.state.fronts(width)
|
||||
}
|
||||
}
|
||||
|
||||
class PulseLayer {
|
||||
timeline: TabPulseTimeline
|
||||
private source: TabPulseTimeline | undefined
|
||||
private enabled: boolean
|
||||
color: RGBA
|
||||
glowColor: RGBA
|
||||
glowTail: number
|
||||
flashColor: RGBA
|
||||
completionColor: RGBA
|
||||
|
||||
constructor(options: TabPulseLayer, enabled: boolean) {
|
||||
this.enabled = enabled
|
||||
this.source = options.timeline
|
||||
this.timeline = options.timeline ?? new TabPulseTimeline(options, enabled)
|
||||
if (options.timeline) options.timeline.set(options, enabled)
|
||||
this.color = options.color
|
||||
this.glowColor = options.glowColor ?? options.color
|
||||
this.glowTail = options.glowTail ?? GLOW_TAIL
|
||||
this.flashColor = options.flashColor ?? options.color
|
||||
this.completionColor = options.completionColor ?? options.color
|
||||
}
|
||||
|
||||
set(options: TabPulseLayer) {
|
||||
const sourceChanged = options.timeline !== this.source
|
||||
if (sourceChanged) {
|
||||
this.source = options.timeline
|
||||
this.timeline = options.timeline ?? new TabPulseTimeline(options, this.enabled)
|
||||
}
|
||||
const stateChanged = sourceChanged
|
||||
? (options.timeline?.set(options, this.enabled) ?? false)
|
||||
: this.timeline.set(options, this.enabled)
|
||||
const color = options.color
|
||||
const glowColor = options.glowColor ?? color
|
||||
const glowTail = options.glowTail ?? GLOW_TAIL
|
||||
const flashColor = options.flashColor ?? color
|
||||
const completionColor = options.completionColor ?? color
|
||||
const changed = [
|
||||
sourceChanged,
|
||||
stateChanged,
|
||||
!color.equals(this.color),
|
||||
!glowColor.equals(this.glowColor),
|
||||
glowTail !== this.glowTail,
|
||||
!flashColor.equals(this.flashColor),
|
||||
!completionColor.equals(this.completionColor),
|
||||
].some(Boolean)
|
||||
this.color = color
|
||||
this.glowColor = glowColor
|
||||
this.glowTail = glowTail
|
||||
this.flashColor = flashColor
|
||||
this.completionColor = completionColor
|
||||
return changed
|
||||
}
|
||||
|
||||
setEnabled(value: boolean) {
|
||||
this.enabled = value
|
||||
return this.timeline.setEnabled(value)
|
||||
}
|
||||
}
|
||||
|
||||
const DEFAULT_LAYER: TabPulseLayer = { color: RGBA.defaultForeground() }
|
||||
|
||||
class TabPulseRenderable extends Renderable {
|
||||
private _enabled: boolean
|
||||
private inner: PulseState
|
||||
private outer: PulseState
|
||||
private _color: RGBA
|
||||
private _outerColor: RGBA
|
||||
private _glowColor: RGBA
|
||||
private _outerGlowColor: RGBA
|
||||
private _glowTail: number
|
||||
private _outerGlowTail: number
|
||||
private primary: PulseLayer
|
||||
private adjacent: PulseLayer
|
||||
private primaryAssigned: boolean
|
||||
private adjacentAssigned: boolean
|
||||
private _edge: "above" | "below" | undefined
|
||||
private _flashColor: RGBA
|
||||
private _outerFlashColor: RGBA
|
||||
private _completionColor: RGBA
|
||||
private _outerCompletionColor: RGBA
|
||||
private _backgroundColor: RGBA
|
||||
private renderColor = RGBA.fromInts(0, 0, 0)
|
||||
private outerRenderColor = RGBA.fromInts(0, 0, 0)
|
||||
private primaryRenderColor = RGBA.fromInts(0, 0, 0)
|
||||
private adjacentRenderColor = RGBA.fromInts(0, 0, 0)
|
||||
private _onLevel: ((level: number) => void) | undefined
|
||||
private lastLevel = 0
|
||||
|
||||
constructor(ctx: RenderContext, options: TabPulseOptions = {}) {
|
||||
const enabled = options.enabled ?? true
|
||||
const active = options.active ?? false
|
||||
const glow = options.glow ?? false
|
||||
const breathe = options.breathe ?? false
|
||||
const primary = options.layer ?? DEFAULT_LAYER
|
||||
const adjacent = options.edgeLayer ?? DEFAULT_LAYER
|
||||
const edge = options.edge
|
||||
const outerActive = options.outerActive ?? active
|
||||
const outerGlow = options.outerGlow ?? glow
|
||||
const outerBreathe = options.outerBreathe ?? breathe
|
||||
super(ctx, {
|
||||
...options,
|
||||
height: 1,
|
||||
live:
|
||||
enabled &&
|
||||
(active || (glow && breathe) || (edge !== undefined && (outerActive || (outerGlow && outerBreathe)))),
|
||||
((primary.active ?? false) ||
|
||||
((primary.glow ?? false) && (primary.breathe ?? false)) ||
|
||||
(edge !== undefined &&
|
||||
((adjacent.active ?? false) || ((adjacent.glow ?? false) && (adjacent.breathe ?? false))))),
|
||||
})
|
||||
this._enabled = enabled
|
||||
this.inner = new PulseState({
|
||||
enabled,
|
||||
active,
|
||||
promptPulse: options.promptPulse ?? 0,
|
||||
complete: options.complete ?? false,
|
||||
glow,
|
||||
breathe,
|
||||
})
|
||||
this.outer = new PulseState({
|
||||
enabled: enabled && edge !== undefined,
|
||||
active: outerActive,
|
||||
promptPulse: options.outerPromptPulse ?? options.promptPulse ?? 0,
|
||||
complete: options.outerComplete ?? options.complete ?? false,
|
||||
glow: outerGlow,
|
||||
breathe: outerBreathe,
|
||||
})
|
||||
this._color = options.color ?? RGBA.defaultForeground()
|
||||
this._outerColor = options.outerColor ?? this._color
|
||||
this._glowColor = options.glowColor ?? this._color
|
||||
this._outerGlowColor = options.outerGlowColor ?? this._glowColor
|
||||
this._glowTail = options.glowTail ?? GLOW_TAIL
|
||||
this._outerGlowTail = options.outerGlowTail ?? this._glowTail
|
||||
this.primary = new PulseLayer(primary, enabled)
|
||||
this.adjacent = new PulseLayer(adjacent, enabled && edge !== undefined)
|
||||
this.primaryAssigned = options.layer !== undefined
|
||||
this.adjacentAssigned = options.edgeLayer !== undefined
|
||||
this._edge = edge
|
||||
this._flashColor = options.flashColor ?? this._color
|
||||
this._outerFlashColor = options.outerFlashColor ?? options.flashColor ?? this._outerColor
|
||||
this._completionColor = options.completionColor ?? this._color
|
||||
this._outerCompletionColor = options.outerCompletionColor ?? options.completionColor ?? this._outerColor
|
||||
this._backgroundColor = options.backgroundColor ?? RGBA.defaultBackground()
|
||||
this._onLevel = options.onLevel
|
||||
}
|
||||
@@ -388,129 +479,42 @@ class TabPulseRenderable extends Renderable {
|
||||
const quantized = Math.round(value * 32) / 32
|
||||
if (quantized === this.lastLevel) return
|
||||
this.lastLevel = quantized
|
||||
this._onLevel?.(quantized)
|
||||
this._onLevel(quantized)
|
||||
}
|
||||
|
||||
set enabled(value: boolean) {
|
||||
if (value === this._enabled) return
|
||||
this._enabled = value
|
||||
this.inner.setEnabled(value)
|
||||
this.outer.setEnabled(value && this._edge !== undefined)
|
||||
this.live = this.inner.live || this.outer.live
|
||||
this.requestRender()
|
||||
this.primary.setEnabled(value)
|
||||
this.adjacent.setEnabled(value && this._edge !== undefined)
|
||||
this.changed()
|
||||
}
|
||||
|
||||
set active(value: boolean) {
|
||||
if (this.inner.setActive(value)) this.changed()
|
||||
set layer(value: TabPulseLayer) {
|
||||
if (!this.primaryAssigned) {
|
||||
this.primaryAssigned = true
|
||||
this.primary = new PulseLayer(value, this._enabled)
|
||||
this.changed()
|
||||
return
|
||||
}
|
||||
if (this.primary.set(value)) this.changed()
|
||||
}
|
||||
|
||||
set outerActive(value: boolean) {
|
||||
if (this.outer.setActive(value)) this.changed()
|
||||
}
|
||||
|
||||
set promptPulse(value: number) {
|
||||
if (this.inner.setPromptPulse(value)) this.changed()
|
||||
}
|
||||
|
||||
set outerPromptPulse(value: number) {
|
||||
if (this.outer.setPromptPulse(value)) this.changed()
|
||||
}
|
||||
|
||||
set complete(value: boolean) {
|
||||
if (this.inner.setComplete(value)) this.changed()
|
||||
}
|
||||
|
||||
set outerComplete(value: boolean) {
|
||||
if (this.outer.setComplete(value)) this.changed()
|
||||
}
|
||||
|
||||
set glow(value: boolean) {
|
||||
if (this.inner.setGlow(value)) this.changed()
|
||||
}
|
||||
|
||||
set outerGlow(value: boolean) {
|
||||
if (this.outer.setGlow(value)) this.changed()
|
||||
}
|
||||
|
||||
set breathe(value: boolean) {
|
||||
if (this.inner.setBreathe(value)) this.changed()
|
||||
}
|
||||
|
||||
set outerBreathe(value: boolean) {
|
||||
if (this.outer.setBreathe(value)) this.changed()
|
||||
}
|
||||
|
||||
private changed() {
|
||||
this.live = this.inner.live || this.outer.live
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set color(value: RGBA) {
|
||||
if (value.equals(this._color)) return
|
||||
this._color = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set glowColor(value: RGBA) {
|
||||
if (value.equals(this._glowColor)) return
|
||||
this._glowColor = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set outerColor(value: RGBA) {
|
||||
if (value.equals(this._outerColor)) return
|
||||
this._outerColor = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set outerGlowColor(value: RGBA) {
|
||||
if (value.equals(this._outerGlowColor)) return
|
||||
this._outerGlowColor = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set glowTail(value: number) {
|
||||
if (value === this._glowTail) return
|
||||
this._glowTail = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set outerGlowTail(value: number) {
|
||||
if (value === this._outerGlowTail) return
|
||||
this._outerGlowTail = value
|
||||
this.requestRender()
|
||||
set edgeLayer(value: TabPulseLayer) {
|
||||
if (!this.adjacentAssigned) {
|
||||
this.adjacentAssigned = true
|
||||
this.adjacent = new PulseLayer(value, this._enabled && this._edge !== undefined)
|
||||
this.changed()
|
||||
return
|
||||
}
|
||||
if (this.adjacent.set(value)) this.changed()
|
||||
}
|
||||
|
||||
set edge(value: "above" | "below" | undefined) {
|
||||
if (value === this._edge) return
|
||||
this._edge = value
|
||||
this.outer.setEnabled(this._enabled && value !== undefined)
|
||||
this.live = this.inner.live || this.outer.live
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set flashColor(value: RGBA) {
|
||||
if (value.equals(this._flashColor)) return
|
||||
this._flashColor = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set outerFlashColor(value: RGBA) {
|
||||
if (value.equals(this._outerFlashColor)) return
|
||||
this._outerFlashColor = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set completionColor(value: RGBA) {
|
||||
if (value.equals(this._completionColor)) return
|
||||
this._completionColor = value
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
set outerCompletionColor(value: RGBA) {
|
||||
if (value.equals(this._outerCompletionColor)) return
|
||||
this._outerCompletionColor = value
|
||||
this.requestRender()
|
||||
this.adjacent.setEnabled(this._enabled && value !== undefined)
|
||||
this.changed()
|
||||
}
|
||||
|
||||
set backgroundColor(value: RGBA) {
|
||||
@@ -519,38 +523,43 @@ class TabPulseRenderable extends Renderable {
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
private changed() {
|
||||
this.live = this.primary.timeline.live || this.adjacent.timeline.live
|
||||
this.requestRender()
|
||||
}
|
||||
|
||||
protected override onUpdate(deltaTime: number): void {
|
||||
if (!this._enabled) return
|
||||
this.inner.advance(deltaTime)
|
||||
this.outer.advance(deltaTime)
|
||||
this.live = this.inner.live || this.outer.live
|
||||
this.primary.timeline.advance(deltaTime, this.ctx.frameId)
|
||||
this.adjacent.timeline.advance(deltaTime, this.ctx.frameId)
|
||||
this.live = this.primary.timeline.live || this.adjacent.timeline.live
|
||||
}
|
||||
|
||||
protected override renderSelf(buffer: OptimizedBuffer): void {
|
||||
if (!this.visible || this.isDestroyed || this.width <= 0) return
|
||||
const running = this.inner.running
|
||||
const completion = this.inner.completion
|
||||
const flash = this.inner.flash
|
||||
const glowLevel = this.inner.glowLevel
|
||||
const outerRunning = this.outer.running
|
||||
const outerCompletion = this.outer.completion
|
||||
const outerFlash = this.outer.flash
|
||||
const outerGlowLevel = this.outer.glowLevel
|
||||
const running = this.primary.timeline.running
|
||||
const completion = this.primary.timeline.completion
|
||||
const flash = this.primary.timeline.flash
|
||||
const glowLevel = this.primary.timeline.glowLevel
|
||||
const adjacentRunning = this.adjacent.timeline.running
|
||||
const adjacentCompletion = this.adjacent.timeline.completion
|
||||
const adjacentFlash = this.adjacent.timeline.flash
|
||||
const adjacentGlowLevel = this.adjacent.timeline.glowLevel
|
||||
if (
|
||||
glowLevel === 0 &&
|
||||
running === 0 &&
|
||||
completion === 0 &&
|
||||
flash === 0 &&
|
||||
outerGlowLevel === 0 &&
|
||||
outerRunning === 0 &&
|
||||
outerCompletion === 0 &&
|
||||
outerFlash === 0
|
||||
adjacentGlowLevel === 0 &&
|
||||
adjacentRunning === 0 &&
|
||||
adjacentCompletion === 0 &&
|
||||
adjacentFlash === 0
|
||||
) {
|
||||
this.emitLevel(0)
|
||||
return
|
||||
}
|
||||
const [front, secondFront] = this.inner.fronts(this.width)
|
||||
const [outerFront, outerSecondFront] = this.outer.fronts(this.width)
|
||||
const [front, secondFront] = this.primary.timeline.fronts(this.width)
|
||||
const [adjacentFront, adjacentSecondFront] = this.adjacent.timeline.fronts(this.width)
|
||||
if (this._onLevel)
|
||||
this.emitLevel(
|
||||
running === 0
|
||||
@@ -558,8 +567,8 @@ class TabPulseRenderable extends Renderable {
|
||||
: Math.max(intensityAt(1, front, RUN_HEAD, RUN_TAIL), intensityAt(1, secondFront, RUN_HEAD, RUN_TAIL)) *
|
||||
running,
|
||||
)
|
||||
const glowTail = Math.min(this._glowTail, Math.max(1, this.width - 2))
|
||||
const outerGlowTail = Math.min(this._outerGlowTail, Math.max(1, this.width - 2))
|
||||
const glowTail = Math.min(this.primary.glowTail, Math.max(1, this.width - 2))
|
||||
const adjacentGlowTail = Math.min(this.adjacent.glowTail, Math.max(1, this.width - 2))
|
||||
for (let index = 0; index < this.width; index++) {
|
||||
// Skip per-cell sweep and glow math when that stage is idle, e.g. a steady breathing glow.
|
||||
const sweep =
|
||||
@@ -571,49 +580,49 @@ class TabPulseRenderable extends Renderable {
|
||||
) *
|
||||
0.14 *
|
||||
running
|
||||
const outerSweep =
|
||||
outerRunning === 0
|
||||
const adjacentSweep =
|
||||
adjacentRunning === 0
|
||||
? 0
|
||||
: Math.max(
|
||||
intensityAt(index, outerFront, RUN_HEAD, RUN_TAIL),
|
||||
intensityAt(index, outerSecondFront, RUN_HEAD, RUN_TAIL),
|
||||
intensityAt(index, adjacentFront, RUN_HEAD, RUN_TAIL),
|
||||
intensityAt(index, adjacentSecondFront, RUN_HEAD, RUN_TAIL),
|
||||
) *
|
||||
0.14 *
|
||||
outerRunning
|
||||
adjacentRunning
|
||||
blendTabPulseColor(
|
||||
this.renderColor,
|
||||
this.primaryRenderColor,
|
||||
this._backgroundColor,
|
||||
this._glowColor,
|
||||
this._color,
|
||||
this._flashColor,
|
||||
this._completionColor,
|
||||
this.primary.glowColor,
|
||||
this.primary.color,
|
||||
this.primary.flashColor,
|
||||
this.primary.completionColor,
|
||||
glowLevel === 0 ? 0 : glowIntensityAt(index, glowTail) * GLOW_OPACITY * glowLevel,
|
||||
sweep,
|
||||
flash,
|
||||
completion,
|
||||
)
|
||||
if (!this._edge) {
|
||||
buffer.setCell(this.screenX + index, this.screenY, " ", DEFAULT_FOREGROUND, this.renderColor)
|
||||
buffer.setCell(this.screenX + index, this.screenY, " ", DEFAULT_FOREGROUND, this.primaryRenderColor)
|
||||
continue
|
||||
}
|
||||
blendTabPulseColor(
|
||||
this.outerRenderColor,
|
||||
this.adjacentRenderColor,
|
||||
this._backgroundColor,
|
||||
this._outerGlowColor,
|
||||
this._outerColor,
|
||||
this._outerFlashColor,
|
||||
this._outerCompletionColor,
|
||||
outerGlowLevel === 0 ? 0 : glowIntensityAt(index, outerGlowTail) * GLOW_OPACITY * outerGlowLevel,
|
||||
outerSweep,
|
||||
outerFlash,
|
||||
outerCompletion,
|
||||
this.adjacent.glowColor,
|
||||
this.adjacent.color,
|
||||
this.adjacent.flashColor,
|
||||
this.adjacent.completionColor,
|
||||
adjacentGlowLevel === 0 ? 0 : glowIntensityAt(index, adjacentGlowTail) * GLOW_OPACITY * adjacentGlowLevel,
|
||||
adjacentSweep,
|
||||
adjacentFlash,
|
||||
adjacentCompletion,
|
||||
)
|
||||
buffer.setCell(
|
||||
this.screenX + index,
|
||||
this.screenY,
|
||||
this._edge === "above" ? "▄" : "▀",
|
||||
this.renderColor,
|
||||
this.outerRenderColor,
|
||||
this.primaryRenderColor,
|
||||
this.adjacentRenderColor,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -632,26 +641,8 @@ export function TabPulse(props: {
|
||||
width?: number
|
||||
edge?: "above" | "below"
|
||||
enabled?: boolean
|
||||
active: boolean
|
||||
outerActive?: boolean
|
||||
promptPulse?: number
|
||||
outerPromptPulse?: number
|
||||
complete?: boolean
|
||||
outerComplete?: boolean
|
||||
glow?: boolean
|
||||
outerGlow?: boolean
|
||||
breathe?: boolean
|
||||
outerBreathe?: boolean
|
||||
color: RGBA
|
||||
outerColor?: RGBA
|
||||
glowColor?: RGBA
|
||||
outerGlowColor?: RGBA
|
||||
glowTail?: number
|
||||
outerGlowTail?: number
|
||||
flashColor?: RGBA
|
||||
outerFlashColor?: RGBA
|
||||
completionColor?: RGBA
|
||||
outerCompletionColor?: RGBA
|
||||
layer: TabPulseLayer
|
||||
edgeLayer?: TabPulseLayer
|
||||
backgroundColor: RGBA
|
||||
onLevel?: (level: number) => void
|
||||
}) {
|
||||
@@ -663,26 +654,8 @@ export function TabPulse(props: {
|
||||
zIndex={0}
|
||||
width={props.width ?? "100%"}
|
||||
enabled={props.enabled ?? true}
|
||||
active={props.active}
|
||||
outerActive={props.outerActive ?? props.active}
|
||||
promptPulse={props.promptPulse ?? 0}
|
||||
outerPromptPulse={props.outerPromptPulse ?? props.promptPulse ?? 0}
|
||||
complete={props.complete ?? false}
|
||||
outerComplete={props.outerComplete ?? props.complete ?? false}
|
||||
glow={props.glow ?? false}
|
||||
outerGlow={props.outerGlow ?? props.glow ?? false}
|
||||
breathe={props.breathe ?? false}
|
||||
outerBreathe={props.outerBreathe ?? props.breathe ?? false}
|
||||
color={props.color}
|
||||
outerColor={props.outerColor ?? props.color}
|
||||
glowColor={props.glowColor ?? props.color}
|
||||
outerGlowColor={props.outerGlowColor ?? props.glowColor ?? props.color}
|
||||
glowTail={props.glowTail ?? GLOW_TAIL}
|
||||
outerGlowTail={props.outerGlowTail ?? props.glowTail ?? GLOW_TAIL}
|
||||
flashColor={props.flashColor ?? props.color}
|
||||
outerFlashColor={props.outerFlashColor ?? props.flashColor ?? props.outerColor ?? props.color}
|
||||
completionColor={props.completionColor ?? props.color}
|
||||
outerCompletionColor={props.outerCompletionColor ?? props.completionColor ?? props.outerColor ?? props.color}
|
||||
layer={props.layer}
|
||||
edgeLayer={props.edge === undefined ? undefined : (props.edgeLayer ?? props.layer)}
|
||||
backgroundColor={props.backgroundColor}
|
||||
onLevel={props.onLevel}
|
||||
/>
|
||||
|
||||
@@ -20,10 +20,12 @@ test("a prompt pulse restarts the neutral edge flash while the tab remains busy"
|
||||
() => (
|
||||
<box width={8} height={1} backgroundColor={background}>
|
||||
<TabPulse
|
||||
active={true}
|
||||
promptPulse={promptPulse()}
|
||||
color={background}
|
||||
flashColor={flash}
|
||||
layer={{
|
||||
active: true,
|
||||
promptPulse: promptPulse(),
|
||||
color: background,
|
||||
flashColor: flash,
|
||||
}}
|
||||
backgroundColor={background}
|
||||
/>
|
||||
</box>
|
||||
|
||||
Reference in New Issue
Block a user