Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 68 additions & 8 deletions Sources/Bonsplit/Internal/Views/TabBarView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -959,7 +959,7 @@ struct TabBarView: View {
tabBarHeight: appearance.tabBarHeight,
availableWidth: containerWidth,
tabContentWidthExcludingSplitButtonLane: tabContentWidthExcludingSplitButtonLane,
splitButtonCount: visibleSplitButtons.count,
splitButtonCount: effectiveSplitButtonCount,
splitButtonLaneVisible: shouldShowSplitButtons,
reservesSplitButtonLane: showSplitButtons && !isMinimalMode,
measuredSplitButtonLaneWidth: measuredSplitButtonLaneWidth
Expand All @@ -981,6 +981,15 @@ struct TabBarView: View {
return appearance.splitButtons
}

/// Effective button count for reserving the trailing action lane. When the
/// buttons are collapsed into the "⋯" overflow menu, only that single menu
/// button occupies the lane, so reserve width for one — not all of them.
private var effectiveSplitButtonCount: Int {
let count = visibleSplitButtons.count
guard count > 0 else { return 0 }
return appearance.collapseSplitButtonsIntoMenu ? 1 : count
}

private var shouldRenderSplitButtons: Bool {
!visibleSplitButtons.isEmpty
}
Expand Down Expand Up @@ -1727,22 +1736,73 @@ struct TabBarView: View {
private var splitButtonRow: some View {
let tooltips = controller.configuration.appearance.splitButtonTooltips
let buttons = visibleSplitButtons
HStack(spacing: TabBarStyling.splitButtonsSpacing) {
Group {
if controller.configuration.appearance.collapseSplitButtonsIntoMenu, !buttons.isEmpty {
splitButtonOverflowMenu(buttons: buttons, tooltips: tooltips)
} else {
HStack(spacing: TabBarStyling.splitButtonsSpacing) {
ForEach(buttons.indices, id: \.self) { index in
let button = buttons[index]
Button {
performSplitActionButton(button)
} label: {
splitActionButtonIcon(button.icon)
}
.buttonStyle(SplitActionButtonStyle(appearance: appearance, layout: tabBarLayout))
.accessibilityIdentifier(splitActionButtonAccessibilityIdentifier(button))
.safeHelp(splitActionButtonTooltip(button, tooltips: tooltips))
}
}
}
}
.padding(.leading, TabBarStyling.splitButtonsLeadingPadding)
.padding(.trailing, TabBarStyling.splitButtonsTrailingPadding)
.frame(height: tabBarHeight, alignment: .center)
}

/// Single "⋯" button that expands the split actions into a menu. Reuses the
/// exact same `performSplitActionButton` dispatch as the inline row, so
/// every action (including custom ones like New Note) behaves identically.
@ViewBuilder
private func splitButtonOverflowMenu(
buttons: [BonsplitConfiguration.SplitActionButton],
tooltips: BonsplitConfiguration.SplitButtonTooltips
) -> some View {
Menu {
ForEach(buttons.indices, id: \.self) { index in
let button = buttons[index]
Button {
performSplitActionButton(button)
} label: {
splitActionButtonIcon(button.icon)
splitActionButtonMenuLabel(button, tooltips: tooltips)
}
.buttonStyle(SplitActionButtonStyle(appearance: appearance, layout: tabBarLayout))
.accessibilityIdentifier(splitActionButtonAccessibilityIdentifier(button))
Comment on lines 1772 to 1779

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Accessibility identifiers on menu items may be unreachable in UI tests

On macOS, Button views inside a SwiftUI Menu {} block are bridged to NSMenuItem rather than rendered as standalone NSViews. .accessibilityIdentifier(_:) modifiers applied to those Button instances typically do not propagate to the resulting NSMenuItem's AXIdentifier in the accessibility hierarchy — the accessibility tree for NSMenuItems is structured differently from regular view hierarchies. Any existing XCTest UI-automation that queries paneTabBarControl.newTerminal, paneTabBarControl.splitRight, etc. by identifier will silently fail to find the elements when collapseSplitButtonsIntoMenu is true, because those identifiers will no longer exist in the expected place in the tree.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

.safeHelp(splitActionButtonTooltip(button, tooltips: tooltips))
}
} label: {
Image(systemName: "ellipsis")
.font(.system(size: 12))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P3: Scale the overflow ellipsis icon with controlIconFontScale so it remains proportionate when the host app uses a non-default tab title font size.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Bonsplit/Internal/Views/TabBarView.swift, line 1783:

<comment>Scale the overflow ellipsis icon with `controlIconFontScale` so it remains proportionate when the host app uses a non-default tab title font size.</comment>

<file context>
@@ -1727,22 +1736,73 @@ struct TabBarView: View {
             }
+        } label: {
+            Image(systemName: "ellipsis")
+                .font(.system(size: 12))
+        }
+        .menuStyle(.button)
</file context>
Suggested change
.font(.system(size: 12))
.font(.system(size: 12 * controlIconFontScale))

}
.menuStyle(.button)
Comment on lines +1781 to +1785

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 ellipsis trigger icon is hardcoded to 12pt, but every other split-action icon is rendered at 12 * controlIconFontScale so it adapts when the host app sets a custom tabTitleFontSize. With this change, enabling the overflow menu alongside a non-default font size will produce an icon that is visually out of proportion with the rest of the tab bar controls.

Suggested change
} label: {
Image(systemName: "ellipsis")
.font(.system(size: 12))
}
.menuStyle(.button)
} label: {
Image(systemName: "ellipsis")
.font(.system(size: 12 * controlIconFontScale))
}
.menuStyle(.button)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

.menuIndicator(.hidden)
.buttonStyle(SplitActionButtonStyle(appearance: appearance, layout: tabBarLayout))
.fixedSize()
.accessibilityIdentifier("paneTabBarControl.overflowMenu")
}

@ViewBuilder
private func splitActionButtonMenuLabel(
_ button: BonsplitConfiguration.SplitActionButton,
tooltips: BonsplitConfiguration.SplitButtonTooltips
) -> some View {
let title = splitActionButtonTooltip(button, tooltips: tooltips)
switch button.icon {
case .systemImage(let name):
Label(title, systemImage: name)
case .emoji(let value, _):
Text("\(value) \(title)")
case .imageData:
Text(title)
}
.padding(.leading, TabBarStyling.splitButtonsLeadingPadding)
.padding(.trailing, TabBarStyling.splitButtonsTrailingPadding)
.frame(height: tabBarHeight, alignment: .center)
}

private func splitActionButtonAccessibilityIdentifier(_ button: BonsplitConfiguration.SplitActionButton) -> String {
Expand Down
7 changes: 7 additions & 0 deletions Sources/Bonsplit/Public/BonsplitConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -490,6 +490,11 @@ extension BonsplitConfiguration {
/// When true, split buttons are only visible on hover
public var splitButtonsOnHover: Bool

/// When true, the tab bar's right-side action buttons collapse into a
/// single overflow ("⋯") menu instead of rendering as a row of icons.
/// Menu item labels come from each button's `tooltip`.
public var collapseSplitButtonsIntoMenu: Bool

/// Optional explicit backdrop style for the tab bar's right-side action buttons.
/// When unset, Bonsplit uses the host app's debug override if one is configured.
public var splitButtonBackdropStyle: SplitButtonBackdropStyle?
Expand Down Expand Up @@ -555,6 +560,7 @@ extension BonsplitConfiguration {
showSplitButtons: Bool = true,
splitButtons: [SplitActionButton] = SplitActionButton.defaults,
splitButtonsOnHover: Bool = false,
collapseSplitButtonsIntoMenu: Bool = false,
splitButtonBackdropStyle: SplitButtonBackdropStyle? = nil,
splitButtonBackdropEffect: SplitButtonBackdropEffect? = nil,
tabBarLeadingInset: CGFloat = 0,
Expand All @@ -574,6 +580,7 @@ extension BonsplitConfiguration {
self.showSplitButtons = showSplitButtons
self.splitButtons = Self.uniqueSplitButtons(splitButtons)
self.splitButtonsOnHover = splitButtonsOnHover
self.collapseSplitButtonsIntoMenu = collapseSplitButtonsIntoMenu
self.splitButtonBackdropStyle = splitButtonBackdropStyle
self.splitButtonBackdropEffect = splitButtonBackdropEffect
self.tabBarLeadingInset = tabBarLeadingInset
Expand Down
Loading