mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-23 16:53:32 +00:00
fix(tui): keep exact-fit tab titles stationary (#42073)
This commit is contained in:
parent
930b0751b1
commit
b24b1b3f16
3 changed files with 24 additions and 12 deletions
|
|
@ -23,7 +23,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, marqueeText } from "../util/marquee"
|
||||
import { marqueeCycleWidth, marqueeOverflows, marqueeText } from "../util/marquee"
|
||||
|
||||
// A long title fades out over its last cells instead of cutting hard.
|
||||
const FADE_WIDTH = 4
|
||||
|
|
@ -85,7 +85,7 @@ function createMarquee(animations: () => boolean) {
|
|||
returning = false
|
||||
return scroll()
|
||||
}
|
||||
if (stringWidth(title) <= width) return
|
||||
if (!marqueeOverflows(title, width)) return
|
||||
cycleWidth = marqueeCycleWidth(title)
|
||||
setActive(sessionID)
|
||||
setOffset(0)
|
||||
|
|
@ -258,7 +258,8 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
|||
return value ? data.project.get(value.projectID) : undefined
|
||||
})
|
||||
const numberWidth = () => 2
|
||||
const titleWidth = () => Math.max(1, width() - numberWidth() - 2 - (hovered() === tab.sessionID ? 1 : 0))
|
||||
const restingTitleWidth = () => Math.max(1, width() - numberWidth() - 2)
|
||||
const titleWidth = () => Math.max(1, restingTitleWidth() - (hovered() === tab.sessionID ? 1 : 0))
|
||||
const title = () => tab.title ?? "Untitled session"
|
||||
const scrolling = () => marquee.active() === tab.sessionID && marquee.offset() > 0
|
||||
const visibleTitle = createMemo(() =>
|
||||
|
|
@ -267,7 +268,9 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
|||
: Locale.takeWidth(title(), titleWidth()),
|
||||
)
|
||||
const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
||||
const titleFades = createMemo(() => stringWidth(title()) >= titleWidth() && titleWidth() > FADE_WIDTH)
|
||||
const titleFades = createMemo(
|
||||
() => marqueeOverflows(title(), restingTitleWidth()) && titleWidth() > FADE_WIDTH,
|
||||
)
|
||||
const detail = createMemo(() => {
|
||||
const value = session()
|
||||
return Locale.takeWidth(projectName(project(), value?.location.directory) ?? "", titleWidth())
|
||||
|
|
@ -349,10 +352,10 @@ function VerticalSessionTabs(props: { controller?: SessionTabsController; animat
|
|||
position="relative"
|
||||
flexDirection="column"
|
||||
backgroundColor={background()}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), titleWidth())}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), restingTitleWidth())}
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={() => {
|
||||
marquee.enter(tab.sessionID, title(), titleWidth())
|
||||
marquee.enter(tab.sessionID, title(), restingTitleWidth())
|
||||
setDragging(tab.sessionID)
|
||||
}}
|
||||
onMouseUp={release}
|
||||
|
|
@ -759,8 +762,8 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|||
// Shortcut labels stay one cell wide: 1-9, 0 for ten, then a neutral dot.
|
||||
const numberWidth = () => 2
|
||||
// Hovering reveals the close mark, so the title's right bound shifts left of it.
|
||||
const availableTitleWidth = () =>
|
||||
Math.max(1, width() - 1 - numberWidth() - (hovered() === tab.sessionID ? 2 : 0))
|
||||
const restingTitleWidth = () => Math.max(1, width() - 1 - numberWidth())
|
||||
const availableTitleWidth = () => Math.max(1, restingTitleWidth() - (hovered() === tab.sessionID ? 2 : 0))
|
||||
const scrolling = () => marquee.active() === tab.sessionID && marquee.offset() > 0
|
||||
const visibleTitle = createMemo(() =>
|
||||
scrolling()
|
||||
|
|
@ -769,7 +772,7 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|||
)
|
||||
const visibleTitleParts = createMemo(() => Locale.graphemes(visibleTitle()))
|
||||
const titleFades = createMemo(
|
||||
() => stringWidth(title()) >= availableTitleWidth() && availableTitleWidth() > FADE_WIDTH,
|
||||
() => marqueeOverflows(title(), restingTitleWidth()) && availableTitleWidth() > FADE_WIDTH,
|
||||
)
|
||||
const foreground = () => {
|
||||
if (hovered() === tab.sessionID) return theme.text.default
|
||||
|
|
@ -820,10 +823,10 @@ function HorizontalSessionTabs(props: { controller?: SessionTabsController; anim
|
|||
position="relative"
|
||||
flexDirection="row"
|
||||
backgroundColor={background()}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), availableTitleWidth())}
|
||||
onMouseOver={() => marquee.enter(tab.sessionID, title(), restingTitleWidth())}
|
||||
onMouseOut={() => marquee.leave(tab.sessionID)}
|
||||
onMouseDown={() => {
|
||||
marquee.enter(tab.sessionID, title(), availableTitleWidth())
|
||||
marquee.enter(tab.sessionID, title(), restingTitleWidth())
|
||||
setDragging(tab.sessionID)
|
||||
}}
|
||||
onMouseUp={release}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,10 @@ export function marqueeCycleWidth(value: string) {
|
|||
return stringWidth(value + GAP)
|
||||
}
|
||||
|
||||
export function marqueeOverflows(value: string, width: number) {
|
||||
return stringWidth(value) > width
|
||||
}
|
||||
|
||||
export function marqueeText(value: string, width: number, offset: number) {
|
||||
if (width <= 0) return ""
|
||||
if (stringWidth(value) <= width || offset <= 0) return Locale.takeWidth(value, width)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { describe, expect, test } from "bun:test"
|
||||
import { marqueeCycleWidth, marqueeText } from "../../src/util/marquee"
|
||||
import { marqueeCycleWidth, marqueeOverflows, marqueeText } from "../../src/util/marquee"
|
||||
import { stringWidth } from "../../src/util/string-width"
|
||||
|
||||
describe("marquee text", () => {
|
||||
|
|
@ -7,6 +7,11 @@ describe("marquee text", () => {
|
|||
expect(marqueeText("Short", 10, 8)).toBe("Short")
|
||||
})
|
||||
|
||||
test("does not classify an exact fit as overflow", () => {
|
||||
expect(marqueeOverflows("Exact fit", 9)).toBe(false)
|
||||
expect(marqueeOverflows("Exact fit", 8)).toBe(true)
|
||||
})
|
||||
|
||||
test("starts clipped and scrolls through a long title", () => {
|
||||
expect(marqueeText("A long session title", 8, 0)).toBe("A long s")
|
||||
expect(marqueeText("A long session title", 8, 2)).toBe("long ses")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue