Skip to content

Add opt-in fill tab-width mode to the tab strip#141

Open
austinywang wants to merge 1 commit into
mainfrom
issue-5091-tab-fill-pane-width
Open

Add opt-in fill tab-width mode to the tab strip#141
austinywang wants to merge 1 commit into
mainfrom
issue-5091-tab-fill-pane-width

Conversation

@austinywang

@austinywang austinywang commented Jun 1, 2026

Copy link
Copy Markdown

Summary

Adds an opt-in tab-width mode so surface tabs can stretch to fill their pane's available tab-bar width instead of using a fixed width. The current fixed-width behavior remains the default — this is strictly opt-in.

New public API: BonsplitConfiguration.Appearance.tabWidthMode: TabWidthMode

  • .fixed (default) — historical fixed-width (tabMinWidth...tabMaxWidth) + horizontal-scroll layout, unchanged byte-for-byte.
  • .fill — tabs stretch to fill the pane's available tab-bar width, distributing the slack equally. A single tab spans the full width; multiple tabs share it evenly. When tabs would overflow at their natural width, the strip falls back to fixed sizing and scrolls (fill never shrinks tabs below their natural width).

Implementation

The fixed width is enforced by a per-tab .frame(maxWidth:) inside a horizontal ScrollView whose HStack sizes to content, so naively setting maxWidth: .infinity would not fill the viewport. Instead:

  • In .fill, each TabItemView becomes flexible (maxWidth: .infinity, min unchanged).
  • The tab row is given a minimum width equal to the measured viewport (tabRowFillMinWidth) so the horizontal ScrollView hands it the full width; SwiftUI then distributes the slack equally across the flexible tabs. When natural content exceeds the viewport, the row grows past it and scrolls as before.
  • The drop zone and all overflow/scroll machinery are untouched.
  • Extracted the scroll content into tabScrollContent to keep TabBarView.body within the SwiftUI type-checker's budget.

Tests

  • testTabWidthModeDefaultsToFixed — asserts the opt-in default (every preset stays .fixed).
  • testTabWidthModeFillIsSettableAndDistinct — fill is settable and distinct.

Related

Consumed by cmux issue manaflow-ai/cmux#5091 (cmux PR linked below once opened), which exposes this via the surface-tabs-fill-pane-width Ghostty config key + a Settings toggle.

🤖 Generated with Claude Code


View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Summary by cubic

Adds an opt-in tab-width mode that lets tabs stretch to fill their pane’s tab bar. Fixed-width remains the default, and overflow still scrolls.

  • New Features

    • New API: BonsplitConfiguration.Appearance.tabWidthMode (TabWidthMode).
    • .fixed keeps the historical fixed-width + horizontal scroll.
    • .fill stretches tabs to share available width; falls back to fixed and scroll when natural width would overflow.
  • Refactors

    • Extracted tab row into tabScrollContent to reduce body complexity.
    • Added tabRowFillMinWidth(_:) to give the row the viewport width in fill mode.

Written for commit 887ece7. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features
    • Added configurable tab width mode option. Tabs can now be set to fixed (default, with horizontal scrolling) or fill (stretches to evenly fill the tab bar, with automatic fallback to fixed scrolling if natural sizing would overflow).

Introduces BonsplitConfiguration.Appearance.tabWidthMode (TabWidthMode):

- .fixed (default) preserves the historical fixed-width + horizontal-scroll
  layout byte-for-byte.
- .fill stretches tabs to fill the pane's available tab-bar width, distributing
  the slack equally across tabs. A single tab spans the full width; multiple
  tabs share it evenly. When tabs would overflow at their natural width, the
  strip falls back to fixed sizing and scrolls.

Implementation: in fill mode each TabItemView becomes flexible (maxWidth:
.infinity) and the tab row is given a minimum width equal to the viewport
(tabRowFillMinWidth) so the horizontal ScrollView hands it the full width and
SwiftUI distributes the slack. The drop zone and overflow/scroll machinery are
untouched. Extracted the scroll content into tabScrollContent to keep the body
within the type-checker's budget.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 1, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: f81d6d50-7038-4fbd-9044-4634b4ac843f

📥 Commits

Reviewing files that changed from the base of the PR and between ddb46fe and 887ece7.

📒 Files selected for processing (5)
  • CHANGELOG.md
  • Sources/Bonsplit/Internal/Views/TabBarView.swift
  • Sources/Bonsplit/Internal/Views/TabItemView.swift
  • Sources/Bonsplit/Public/BonsplitConfiguration.swift
  • Tests/BonsplitTests/BonsplitTests.swift

📝 Walkthrough

Walkthrough

This PR introduces a TabWidthMode configuration option to BonsplitConfiguration.Appearance, enabling users to choose between fixed-width tabs with horizontal scrolling (default) or fill-width tabs that stretch to available space. The setting is threaded from the configuration contract through TabBarView to individual TabItemView layouts, with new view helpers supporting conditional width constraints and tests validating the default and explicit behaviors.

Changes

Tab Width Mode Configuration and Rendering

Layer / File(s) Summary
Configuration contract and schema
Sources/Bonsplit/Public/BonsplitConfiguration.swift, CHANGELOG.md
BonsplitConfiguration.Appearance adds a public TabWidthMode enum with .fixed (default) and .fill cases, a tabWidthMode property, and an initializer parameter. Changelog documents the feature.
Tab item fill-width layout support
Sources/Bonsplit/Internal/Views/TabItemView.swift
TabItemView adds a fillsWidth: Bool property to control width expansion and a new tabRowFillMinWidth(_:) view modifier that conditionally applies minimum width constraints when in fill mode.
Tab bar fill-mode orchestration
Sources/Bonsplit/Internal/Views/TabBarView.swift
TabBarView derives fillsTabsToWidth from the appearance configuration, computes fillRowMinWidth, extracts the scrollable tab row into a tabScrollContent ViewBuilder with fill constraints applied, and passes fillsWidth to each TabItemView.
Configuration and behavior validation
Tests/BonsplitTests/BonsplitTests.swift
Unit tests confirm that tabWidthMode defaults to .fixed for all appearance presets and that .fill can be set and remains distinct.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Tabs now stretch with care,
From config to view,
Fixed or fill mode—
The rabbit chose true,
Scrolling or filling the air! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.86% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Add opt-in fill tab-width mode to the tab strip' accurately and concisely describes the main change: introducing an optional fill mode for tab width configuration.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5091-tab-fill-pane-width

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No issues found across 5 files

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented Jun 1, 2026

Copy link
Copy Markdown

Greptile Summary

Adds an opt-in .fill tab-width mode that stretches tabs to share the pane's available tab-bar width. The current fixed-width behavior (tabMinWidth…tabMaxWidth + horizontal scroll) remains the default and is untouched byte-for-byte.

  • New public APIBonsplitConfiguration.Appearance.tabWidthMode: TabWidthMode (.fixed default, .fill opt-in). All existing presets pass no argument and inherit .fixed.
  • Layout mechanism — in fill mode each TabItemView gets maxWidth: .infinity; the tab row is given frame(minWidth: containerWidth) so the ScrollView hands it the full viewport and SwiftUI distributes slack equally. When natural tab widths would overflow the viewport, the tabMinWidth floor prevents tabs from shrinking further and the strip scrolls as in fixed mode.
  • Refactor — the inline HStack in body is extracted into tabScrollContent (@ViewBuilder) for SwiftUI type-checker budget reasons; no semantic changes to the scroll, drag-drop, or split-button lane machinery.

Confidence Score: 4/5

Safe to merge. The change is strictly opt-in, the fixed-width path is preserved without modification, and there are no data or correctness risks.

The fill-mode layout math is correct: trailingTabContentInset reserves the split-button lane before fillRowMinWidth imposes containerWidth, so tabs never overflow into the button area. The one notable rough edge is that tabRowFillMinWidth uses a @ViewBuilder if let producing two structurally distinct view types. A runtime switch between .fill and .fixed would unmount and remount the entire tab-row subtree, resetting per-tab @State (hover, favicon, loading) momentarily. Because tabWidthMode is a configuration-level setting rather than a per-interaction toggle, this remount is unlikely to be noticed in practice, but the fix is straightforward.

Sources/Bonsplit/Internal/Views/TabItemView.swift — specifically the tabRowFillMinWidth helper and its conditional view-type branching.

Important Files Changed

Filename Overview
Sources/Bonsplit/Public/BonsplitConfiguration.swift Adds TabWidthMode enum and tabWidthMode: TabWidthMode = .fixed property to Appearance; backwards-compatible opt-in addition, all presets keep .fixed default via the initializer default value.
Sources/Bonsplit/Internal/Views/TabBarView.swift Extracts tab scroll content into tabScrollContent, adds fillsTabsToWidth/fillRowMinWidth helpers, and forwards fillsWidth to each TabItemView. The fill logic is correct: trailing-inset padding reserves the split-button lane before fillRowMinWidth imposes containerWidth, so tabs never extend under buttons.
Sources/Bonsplit/Internal/Views/TabItemView.swift Adds fillsWidth: Bool property and the tabRowFillMinWidth view-extension helper; changes maxWidth to .infinity in fill mode while keeping minWidth unchanged so overflow tabs fall back to their natural floor and scroll.
Tests/BonsplitTests/BonsplitTests.swift Adds two focused unit tests: default is .fixed for all existing presets and the direct initializer, and .fill is settable and distinct.
CHANGELOG.md Adds correct [Unreleased] section describing the new tabWidthMode API and both enum cases.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[TabBarView body] --> B[GeometryReader containerWidth]
    B --> C[ScrollView horizontal]
    C --> D[tabScrollContent ViewBuilder]
    D --> E[HStack with tabs and drop zone]
    E --> E2[ForEach TabItemViews]
    E --> E3[dropZoneAfterTabs 30pt fixed]
    D --> F[padding horizontal barPadding]
    F --> G[padding trailing trailingTabContentInset]
    G --> H{fillRowMinWidth}
    H -- fill mode AND containerWidth gt 0 --> I[frame minWidth containerWidth]
    H -- fixed mode OR containerWidth is 0 --> J[no-op self]
    I --> K[frame height tabBarHeight]
    J --> K
    E2 --> L[TabItemView]
    L --> M{fillsWidth}
    M -- fill --> N[maxWidth infinity]
    M -- fixed --> O[maxWidth tabMaxWidth]
Loading

Reviews (1): Last reviewed commit: "Add opt-in fill tab-width mode to the ta..." | Re-trigger Greptile

Comment on lines +26 to +33
@ViewBuilder
func tabRowFillMinWidth(_ minWidth: CGFloat?) -> some View {
if let minWidth {
frame(minWidth: minWidth, alignment: .leading)
} else {
self
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 The @ViewBuilder if let produces two structurally different view types — self vs self.frame(minWidth:alignment:). When tabWidthMode changes at runtime the swap causes SwiftUI to unmount and remount the entire tab-row subtree, resetting @State in every TabItemView (hover, favicon, loading states). Replacing the conditional with an unconditional frame(minWidth:) where nil maps to 0 keeps the view type stable across mode changes, eliminating the remount.

Suggested change
@ViewBuilder
func tabRowFillMinWidth(_ minWidth: CGFloat?) -> some View {
if let minWidth {
frame(minWidth: minWidth, alignment: .leading)
} else {
self
}
}
func tabRowFillMinWidth(_ minWidth: CGFloat?) -> some View {
frame(minWidth: minWidth ?? 0, alignment: .leading)
}

@austinywang

Copy link
Copy Markdown
Author

Consumer PR: manaflow-ai/cmux#5147 (bumps the submodule pointer to this branch and exposes the mode via the surface-tabs-fill-pane-width Ghostty config key + a Settings toggle).

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