Skip to content

A tab strip shrinks its labels to fit its pane - #37

Merged
HarryCordewener merged 1 commit into
mainfrom
feat/tab-strip-fits-its-pane
Aug 14, 2026
Merged

A tab strip shrinks its labels to fit its pane#37
HarryCordewener merged 1 commit into
mainfrom
feat/tab-strip-fits-its-pane

Conversation

@HarryCordewener

Copy link
Copy Markdown
Member

The report

When there are enough tabs, how do I see that there are more tabs with more content? Or is there a way to automatically shrink tabs and their text to make room for other tabs?

The answer to the first was you can't, and to the second no. TabControl draws its tabs left to right from x = headerLeft and writes each one with WriteCellsClipped against the header rectangle (TabControl.Rendering.cs:75,103). There is no first-visible-tab offset anywhere in the control — I grepped it — so a strip narrower than its tabs simply stops drawing. The tabs past the edge are gone, the selected one included, and so are the × and the , each gated on x < headerRight. The framework's own " ← → " hint (:124) cannot cover for it either: it is drawn only when HasFocus and there is slack left after the tabs, and focus here is pinned to the armed command line.

Measured on the new tabs-many view: five of one character's captures in a split pane drew two tabs and half of a third.

The fix

The titles are ours, so the titles shrink. TabStripFit decides how many cells of name each tab may draw:

  1. The owner prefix goes first, and for the whole strip. A pane full of one character's captures repeats Corvid - on every tab — the cheapest thing there is to lose and the most there is of it. What it said is still said twice over, by the chip the tab is painted on and by the a window belonging to another character wears. Dropping it alone is often the whole fix.
  2. Then names share by water-filling, not in proportion. A cap is lowered until the strip fits, so O-Gatecrashers gives up cells and Chat keeps them all.
  3. The selected tab is spared, and keeps a floor of its own (MinimumSelectedName) when it cannot be. It is the tab the strip exists to name.
  4. Only the name shrinks. The badge, pen, and focus are facts about the window, each one or two cells against a name that is routinely twenty.

Past the floors the tabs do overflow, and that is left alone rather than papered over: eight tabs cannot be drawn in forty cells however they are labelled. Making the last tab reachable needs a scroll offset in the control, which is upstream work.

Frame, before and after

before   Corvid - Chat (2) ×│ Corvid - Public │ Corv
after    Chat (2) ×│ Pub… │ O-G… (1) │ Rad… │ Tel… ─

Two things that could have gone wrong and are guarded

  • A second arithmetic for the decorations would drift from TabTitles.For and overflow the pane while every pure test still passed. The fixed cost is measured off a rendered title (VisibleLength less the name) plus the framework's own cells, and TabStripElisionTests asserts the sum against a real PaneOutputRects width rather than against its own model.
  • A title that changed its pane's width would loop. TabControl.CalculateSize returns the constraint's width and never its content's, so the dependency runs one way. The sync is gated on the width having actually moved, and goes through RetitlePane rather than RefreshTabTitles so it does not drag RefreshRail with it — the rail resizes the sidebar's column, which moves the very widths that triggered the sync.

Verification

dotnet build -c Release clean and warning-free; all five suites green — Core 938, Graphics 83, Scripting 42, Web 37, Tui 1814. New tests: TabStripFitTests (7, the rule) and TabStripElisionTests (5, through a real layout and a real frame). tabs and tint-tabs render byte-identically — those strips have room, and a strip with room is left exactly as it was.

🤖 Generated with Claude Code

TabControl draws its tabs left to right from index 0 and writes each one
clipped to the header rectangle. There is no first-visible-tab offset
anywhere in the control, so a strip narrower than its tabs stops drawing
— the tabs past the edge are gone, the selected one included, and so are
the ×, the separator and the framework's own ← → hint, which only draws
when there is slack for it and the strip has keyboard focus (never, here:
focus is pinned to the armed command line). Five captures in a split pane
drew two tabs and half of a third, and said nothing about the rest.

The titles are ours, so the titles shrink. TabStripFit drops the repeated
owner prefix across the whole strip first — a pane of one character's
captures repeats "Corvid - " on every tab, and the chip behind the tab
already says whose it is — then water-fills what is left, so the long
name gives up cells and the short one keeps them all. The tab the pane is
showing is spared while any other tab can still pay, and keeps a floor of
its own when it cannot be.

A tab's fixed cost is measured off a rendered title rather than derived a
second time, so the badge, pen, ⌁ and focus ▌ keep one definition; the
width is synced on PostBufferPaint, gated on having moved, and through
RetitlePane rather than RefreshTabTitles so it does not drag the rail —
which resizes the sidebar column, which moves the very widths that
triggered it. TabControl.CalculateSize returns the constraint's width and
never its content's, so there is no loop to guard against beyond that.

Past the floors the tabs do overflow and that is left alone: eight tabs
cannot be drawn in forty cells however they are labelled. Making the last
tab reachable is a scroll offset, which is upstream work.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 14, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 22 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 126be41d-0b93-425e-93b5-af24fbd1f315

📥 Commits

Reviewing files that changed from the base of the PR and between 99a03eb and da60685.

📒 Files selected for processing (7)
  • CLAUDE.md
  • src/SharpMUTerm.Tui/DemoScene.cs
  • src/SharpMUTerm.Tui/SharpMUTermApp.cs
  • src/SharpMUTerm.Tui/TabStripFit.cs
  • src/SharpMUTerm.Tui/TabTitles.cs
  • tests/SharpMUTerm.Tui.Tests/TabStripElisionTests.cs
  • tests/SharpMUTerm.Tui.Tests/TabStripFitTests.cs

Comment @coderabbitai help to get the list of available commands.

@HarryCordewener
HarryCordewener merged commit 57eaaa7 into main Aug 14, 2026
3 checks passed
@HarryCordewener
HarryCordewener deleted the feat/tab-strip-fits-its-pane branch August 14, 2026 16:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant