Skip to content

Fix tab indicator selection chrome - #125

Merged
austinywang merged 13 commits into
mainfrom
issue-3465-tab-indicator-glitch-main
May 19, 2026
Merged

austinywang merged 13 commits into
mainfrom
issue-3465-tab-indicator-glitch-main

Conversation

@austinywang

@austinywang austinywang commented May 19, 2026 •

Copy link
Copy Markdown

Summary

  • Merges the tab indicator glitch fix onto the current Bonsplit main line.
  • Keeps minimal-mode tab hit-region handling from main while deriving selected chrome from the current selected tab id and measured tab frames.
  • Suppresses incidental tab-bar hover/press/visibility animations so selection chrome snaps immediately during rapid switching.

Testing

  • Not run locally per cmux repo policy; this PR is opened to satisfy protected-branch flow for the cmux submodule pointer.

Supports manaflow-ai/cmux#3969.


Summary by cubic

Fixes the tab indicator so it always follows the current selection and snaps instantly during rapid switches. Disables tab-bar button animations and hard-contains the split action lane to prevent flicker, bleed, and startup layout jumps.

  • Bug Fixes
    • Derive the indicator frame from pane.selectedTabId and measured tab frames via TabBarStyling.selectedTabFrame(...), removing SelectedTabFramePreferenceKey. Added a unit test confirming it tracks the current selection and returns nil when none is selected.
    • Disable button/hover/press animations with .tabBarButtonAnimationsDisabled() and use no-animation transactions for hover tracking and tab-frame preference updates (including split buttons).
    • Contain the split action lane: mask and hard-clip it, frame it to the backdrop width, disable its visibility animations, return 0 width when the container is unmeasured, and ignore zero tab-content width when computing whitespace. Added tests for the zero-width and crowded-overflow cases, and updated the overflow sampler to check the lane edge.

Written for commit e093031. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

  • Refactor

    • Optimized tab bar animation handling to prevent unnecessary animations during tab selection and hover interactions.
    • Improved tab hover state updates with more precise animation control.
  • Tests

    • Added test to verify tab bar selected indicator positioning follows the currently selected tab.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 19, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Refactored tab-bar frame tracking from preference-key aggregation to on-demand computation from a persisted frames dictionary, centralized animation suppression for hover and split-button state changes using transaction-wrapped updates, and added unit test coverage for selected tab frame selection.

Changes

Tab bar frame and animation refactoring

Layer / File(s) Summary
Selected tab frame on-demand computation
Sources/Bonsplit/Internal/Views/TabBarView.swift
SelectedTabFramePreferenceKey is removed. New TabBarStyling.selectedTabFrame(selectedTabId:tabFrames:) helper derives the selected tab's CGRect? directly from the tabFramesInBar dictionary. Stored state selectedTabFrameInBar is replaced with a computed property that delegates to the helper using the current pane.selectedTabId. The TabItemView geometry measurement now emits only TabFramePreferenceKey and no longer reports the selected frame via preference key.
Animation suppression for hover and split-button state changes
Sources/Bonsplit/Internal/Views/TabItemView.swift, Sources/Bonsplit/Internal/Views/TabBarView.swift
New View.tabBarButtonAnimationsDisabled() modifier wraps state updates in a transaction with animations disabled. All three hover handlers in TabItemView (zoom indicator, main tab, and close button) now set state inside withTransaction(Transaction(animation: nil)). Tab-bar hover updates are centralized through a new updateTabBarHover(_:) method that applies the same transaction suppression. Frame dictionary updates and split-button chrome visibility changes are wrapped in transactions to prevent incidental animations. Previous explicit .animation(..., value: ...) modifiers for hover states are removed.
Test coverage for selected tab frame selection
Tests/BonsplitTests/BonsplitTests.swift
New testTabBarSelectedChromeFrameFollowsCurrentSelection test verifies that TabBarLayout.selectedIndicatorFrame correctly updates its x-position (minX) based on the selected tab id, and returns nil when no tab is selected.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Poem

🐰 Frames now flow from stored dictionaries true,
No preference keys chasing the view.
Hover states settle without a dance,
Transactions silence the animation's glance.
Clean and precise, the tab bar's refrain! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% 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
Title check ✅ Passed The title "Fix tab indicator selection chrome" directly addresses the main change: correcting how the tab indicator (selection chrome) is computed and animated based on the currently selected tab, as evidenced by the changes to TabBarView and the new test.
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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ 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-3465-tab-indicator-glitch-main

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 3 files

Re-trigger cubic

@greptile-apps

greptile-apps Bot commented May 19, 2026

Copy link
Copy Markdown

Greptile Summary

This PR fixes a tab indicator chrome selection glitch by replacing a @State-backed selectedTabFrameInBar (previously populated via an onPreferenceChange observer on a now-removed SelectedTabFramePreferenceKey) with a computed property that derives the selected frame directly from the full tabFramesInBar dictionary and pane.selectedTabId. This removes the one-render-cycle lag between a tab selection and the indicator's position update during rapid switching.

  • Core fix (TabBarView.swift): selectedTabFrameInBar is now a var computed from tabFramesInBar[pane.selectedTabId] via a new static helper TabBarStyling.selectedTabFrame; the SelectedTabFramePreferenceKey type and its observation are deleted. The tabFramesInBar and isHoveringTabBar state mutations are wrapped in withTransaction(Transaction(animation: nil)) so layout-pass frame updates never trigger animation side effects on the indicator.
  • Animation suppression (TabItemView.swift): Introduces tabBarButtonAnimationsDisabled() (sets transaction.animation = nil and transaction.disablesAnimations = true on the propagating transaction) and applies it to hover/press-sensitive views; all per-value .animation(.easeInOut…) modifiers on accessory and split-button views are replaced with this helper, so hover and press state changes snap immediately.
  • Test (BonsplitTests.swift): Adds testTabBarSelectedChromeFrameFollowsCurrentSelection covering first-tab, second-tab, and nil-selection frame lookup.

Confidence Score: 5/5

Safe to merge — the change is a targeted, well-understood fix with direct test coverage and no altered external API surface.

The logic change is small and correct: replacing a state variable populated by a lagging preference-key observer with a computed property that reads directly from the live tab-frames dictionary eliminates the root cause of the glitch. Animation suppression is applied surgically via the new helper and by wrapping state mutations in no-animation transactions. The new test covers the frame-lookup logic path including the nil-selection edge case.

No files require special attention.

Important Files Changed

Filename Overview
Sources/Bonsplit/Internal/Views/TabBarView.swift Core fix: converts selectedTabFrameInBar from a @State var fed by a now-removed SelectedTabFramePreferenceKey into a computed property derived from tabFramesInBar and pane.selectedTabId, eliminating selection-chrome lag. Also wraps tabFramesInBar and isHoveringTabBar mutations in withTransaction(Transaction(animation: nil)) to prevent animation propagation, and replaces explicit .animation(.easeInOut) modifiers on split-button visibility with tabBarButtonAnimationsDisabled().
Sources/Bonsplit/Internal/Views/TabItemView.swift Introduces the tabBarButtonAnimationsDisabled() View extension (sets transaction.animation = nil and transaction.disablesAnimations = true). Replaces all onHover closures that were directly assigning isHovered / isZoomHovered / isCloseHovered with withTransaction(Transaction(animation: nil)) wrappers, and removes the two .animation(.easeInOut, value:) modifiers from the accessory slot view, replacing them with .tabBarButtonAnimationsDisabled().
Tests/BonsplitTests/BonsplitTests.swift Adds testTabBarSelectedChromeFrameFollowsCurrentSelection, which exercises TabBarStyling.selectedTabFrame and layout.selectedIndicatorFrame for both a first-tab and second-tab selection, plus the nil-selectedTabId case. Coverage is appropriate for the changed logic path.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[pane.selectedTabId changes] --> B[selectedTabFrameInBar computed property]
    C[Layout pass: GeometryReader emits frames] --> D[TabFramePreferenceKey collects all tab frames]
    D --> E[onPreferenceChange withTransaction animation=nil tabFramesInBar = frames]
    E --> B
    B --> F{Lookup tabFramesInBar selectedTabId}
    F -->|found| G[CGRect for selected tab]
    F -->|nil or missing| H[nil: no indicator]
    G --> I[selectedIndicatorFrame renders maskedSelectedTabIndicatorChrome]
Loading

Reviews (1): Last reviewed commit: "fix: narrow tab bar animation transactio..." | Re-trigger Greptile

@coderabbitai coderabbitai 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.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
Sources/Bonsplit/Internal/Views/TabItemView.swift (1)

140-145: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Use the macOS 14 onChange closure signature to clear deprecation warnings.

Lines 139, 140, and 144 use the deprecated single-parameter closure form. Update them to use the two-parameter closure signature.

♻️ Proposed fix
-                .onChange(of: tab.isLoading) { _ in updateGlobeFallback() }
-                .onChange(of: tab.iconImageData) { _ in
+                .onChange(of: tab.isLoading) { _, _ in updateGlobeFallback() }
+                .onChange(of: tab.iconImageData) { _, _ in
                     updateRenderedFaviconImage()
                     updateGlobeFallback()
                 }
-                .onChange(of: tab.icon) { _ in updateGlobeFallback() }
+                .onChange(of: tab.icon) { _, _ in updateGlobeFallback() }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/Bonsplit/Internal/Views/TabItemView.swift` around lines 140 - 145,
Replace the deprecated single-parameter onChange closures for tab.isLoading,
tab.iconImageData, and tab.icon with the macOS 14 two-parameter signature;
update the three calls (.onChange(of: tab.isLoading), .onChange(of:
tab.iconImageData), .onChange(of: tab.icon)) to use the (oldValue, newValue)
closure form and call updateGlobeFallback() and updateRenderedFaviconImage()
inside that closure (you may ignore oldValue/newValue by using underscores if
not needed), ensuring the closures reference the same updateGlobeFallback() and
updateRenderedFaviconImage() functions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@Sources/Bonsplit/Internal/Views/TabItemView.swift`:
- Around line 140-145: Replace the deprecated single-parameter onChange closures
for tab.isLoading, tab.iconImageData, and tab.icon with the macOS 14
two-parameter signature; update the three calls (.onChange(of: tab.isLoading),
.onChange(of: tab.iconImageData), .onChange(of: tab.icon)) to use the (oldValue,
newValue) closure form and call updateGlobeFallback() and
updateRenderedFaviconImage() inside that closure (you may ignore
oldValue/newValue by using underscores if not needed), ensuring the closures
reference the same updateGlobeFallback() and updateRenderedFaviconImage()
functions.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eedd938e-bc16-4a09-b2b7-e4595172aa4e

📥 Commits

Reviewing files that changed from the base of the PR and between 02db30f and b6fdb61.

📒 Files selected for processing (3)
  • Sources/Bonsplit/Internal/Views/TabBarView.swift
  • Sources/Bonsplit/Internal/Views/TabItemView.swift
  • Tests/BonsplitTests/BonsplitTests.swift

@austinywang
austinywang merged commit 78de1d7 into main May 19, 2026
5 checks passed
@austinywang
austinywang deleted the issue-3465-tab-indicator-glitch-main branch May 19, 2026 05:09
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.

2 participants