|
|
@@ -1,6 +1,7 @@
|
|
|
import { RGBA, ScrollBoxRenderable, TextAttributes, type MouseEvent } from "@opentui/core"
|
|
|
import {
|
|
|
For,
|
|
|
+ Index,
|
|
|
Match,
|
|
|
Show,
|
|
|
Switch,
|
|
|
@@ -34,7 +35,7 @@ import { TabPulse, unreadGlowIntensity } from "./tab-pulse"
|
|
|
import { tint } from "../theme/color"
|
|
|
import { SESSION_SIDEBAR_WIDTH } from "../ui/layout"
|
|
|
import { projectName } from "../util/project"
|
|
|
-import { marqueeCycleWidth, marqueeOverflows, marqueeText } from "../util/marquee"
|
|
|
+import { marqueeCycleWidth, marqueeOverflows, marqueeTextParts } from "../util/marquee"
|
|
|
import { useDialog } from "../ui/dialog"
|
|
|
import { DialogSessionRename } from "./dialog-session-rename"
|
|
|
|
|
|
@@ -43,7 +44,7 @@ const FADE_WIDTH = 4
|
|
|
// The add button renders as " + " at the end of the strip, so the tab layout leaves it room.
|
|
|
const ADD_TAB_WIDTH = 3
|
|
|
const MARQUEE_DELAY = 600
|
|
|
-const MARQUEE_INTERVAL = 100
|
|
|
+const MARQUEE_INTERVAL = 80
|
|
|
const CONTEXT_MENU_WIDTH = 16
|
|
|
const RIGHT_MOUSE_BUTTON = 2
|
|
|
|
|
|
@@ -394,12 +395,19 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
|
|
const titleWidth = () => (hovered() === tab.sessionID ? hoveredTitleWidth() : restingTitleWidth())
|
|
|
const title = () => tab.title ?? "Untitled session"
|
|
|
const scrolling = () => marquee.active() === tab.sessionID
|
|
|
- const visibleTitle = createMemo(() =>
|
|
|
+ const visibleTitleParts = createMemo(() =>
|
|
|
scrolling()
|
|
|
- ? marqueeText(title(), titleWidth(), marquee.offset())
|
|
|
- : Locale.takeWidth(title(), titleWidth()),
|
|
|
+ ? marqueeTextParts(title(), titleWidth(), marquee.offset())
|
|
|
+ : Locale.graphemes(Locale.takeWidth(title(), titleWidth())).map((value) => ({
|
|
|
+ value,
|
|
|
+ separator: false,
|
|
|
+ })),
|
|
|
+ )
|
|
|
+ const visibleTitle = createMemo(() =>
|
|
|
+ visibleTitleParts()
|
|
|
+ .map((part) => part.value)
|
|
|
+ .join(""),
|
|
|
)
|
|
|
- const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
|
|
const titleFades = createMemo(() => marqueeOverflows(title(), titleWidth()) && titleWidth() > FADE_WIDTH)
|
|
|
const detail = createMemo(() => {
|
|
|
const fixture = tabs.detail?.(tab.sessionID)
|
|
|
@@ -497,13 +505,13 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
|
|
}
|
|
|
const separatorUpperColor = createMemo(() => tint(theme.background.default, previousGlowHue(), 0.1))
|
|
|
const separatorLowerColor = createMemo(() => tint(theme.background.default, glowHue(), 0.12))
|
|
|
- const titleColor = (index: number) => {
|
|
|
+ const titleColor = (index: number, separator: boolean) => {
|
|
|
const level = titleGlow.value().level
|
|
|
const color =
|
|
|
level === 0
|
|
|
? foreground()
|
|
|
: glowTextColor(foreground(), glowColor(), 1 + numberWidth() + index, width(), level)
|
|
|
- return titleFades()
|
|
|
+ const faded = titleFades()
|
|
|
? fadeTitleColor(
|
|
|
color,
|
|
|
pulseBackground(),
|
|
|
@@ -512,6 +520,7 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
|
|
scrolling() ? marquee.leading() : 0,
|
|
|
)
|
|
|
: color
|
|
|
+ return separator ? tint(faded, pulseBackground(), 0.55) : faded
|
|
|
}
|
|
|
const release = () => {
|
|
|
setDragging(undefined)
|
|
|
@@ -648,10 +657,15 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
|
|
selectable={false}
|
|
|
attributes={selected() ? TextAttributes.BOLD : undefined}
|
|
|
>
|
|
|
- <Show when={titleGlow.value().level > 0 || titleFades()} fallback={visibleTitle()}>
|
|
|
- <For each={visibleTitleParts()}>
|
|
|
- {(character, index) => <span style={{ fg: titleColor(index()) }}>{character}</span>}
|
|
|
- </For>
|
|
|
+ <Show
|
|
|
+ when={scrolling() || titleGlow.value().level > 0 || titleFades()}
|
|
|
+ fallback={visibleTitle()}
|
|
|
+ >
|
|
|
+ <Index each={visibleTitleParts()}>
|
|
|
+ {(part, index) => (
|
|
|
+ <span style={{ fg: titleColor(index, part().separator) }}>{part().value}</span>
|
|
|
+ )}
|
|
|
+ </Index>
|
|
|
</Show>
|
|
|
</text>
|
|
|
<text
|
|
|
@@ -984,12 +998,19 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|
|
const hoveredTitleWidth = () => Math.max(1, restingTitleWidth() - 2)
|
|
|
const availableTitleWidth = () => (hovered() === tab.sessionID ? hoveredTitleWidth() : restingTitleWidth())
|
|
|
const scrolling = () => marquee.active() === tab.sessionID
|
|
|
- const visibleTitle = createMemo(() =>
|
|
|
+ const visibleTitleParts = createMemo(() =>
|
|
|
scrolling()
|
|
|
- ? marqueeText(title(), availableTitleWidth(), marquee.offset())
|
|
|
- : Locale.takeWidth(title(), availableTitleWidth()),
|
|
|
+ ? marqueeTextParts(title(), availableTitleWidth(), marquee.offset())
|
|
|
+ : Locale.graphemes(Locale.takeWidth(title(), availableTitleWidth())).map((value) => ({
|
|
|
+ value,
|
|
|
+ separator: false,
|
|
|
+ })),
|
|
|
+ )
|
|
|
+ const visibleTitle = createMemo(() =>
|
|
|
+ visibleTitleParts()
|
|
|
+ .map((part) => part.value)
|
|
|
+ .join(""),
|
|
|
)
|
|
|
- const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
|
|
const titleFades = createMemo(
|
|
|
() => marqueeOverflows(title(), availableTitleWidth()) && availableTitleWidth() > FADE_WIDTH,
|
|
|
)
|
|
|
@@ -1001,10 +1022,10 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|
|
}
|
|
|
// Title characters sitting over the glow tinge toward its color, following the same
|
|
|
// spatial falloff as the glow itself; characters beyond the tail stay neutral.
|
|
|
- const characterColor = (index: number) => {
|
|
|
+ const characterColor = (index: number, separator: boolean) => {
|
|
|
const base = foreground()
|
|
|
const color = glows() ? glowTextColor(base, glowColor(), 1 + numberWidth() + index, width()) : base
|
|
|
- return titleFades()
|
|
|
+ const faded = titleFades()
|
|
|
? fadeTitleColor(
|
|
|
color,
|
|
|
background(),
|
|
|
@@ -1013,6 +1034,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|
|
scrolling() ? marquee.leading() : 0,
|
|
|
)
|
|
|
: color
|
|
|
+ return separator ? tint(faded, background(), 0.55) : faded
|
|
|
}
|
|
|
// The running sweep's level under the number cell, reported by the pulse renderable.
|
|
|
const [sweepLevel, setSweepLevel] = createSignal(0)
|
|
|
@@ -1101,10 +1123,12 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|
|
selectable={false}
|
|
|
attributes={bold()}
|
|
|
>
|
|
|
- <Show when={glows() || titleFades()} fallback={visibleTitle()}>
|
|
|
- <For each={visibleTitleParts()}>
|
|
|
- {(character, index) => <span style={{ fg: characterColor(index()) }}>{character}</span>}
|
|
|
- </For>
|
|
|
+ <Show when={scrolling() || glows() || titleFades()} fallback={visibleTitle()}>
|
|
|
+ <Index each={visibleTitleParts()}>
|
|
|
+ {(part, index) => (
|
|
|
+ <span style={{ fg: characterColor(index, part().separator) }}>{part().value}</span>
|
|
|
+ )}
|
|
|
+ </Index>
|
|
|
</Show>
|
|
|
</text>
|
|
|
<text
|