diff --git a/Resources/Localizable.xcstrings b/Resources/Localizable.xcstrings index 9aa55cc7b..f6f05924f 100644 --- a/Resources/Localizable.xcstrings +++ b/Resources/Localizable.xcstrings @@ -14944,23 +14944,6 @@ } } }, - "tabBar.autoTitle.pullRequest": { - "extractionState": "manual", - "localizations": { - "en": { - "stringUnit": { - "state": "translated", - "value": "PR #%lld" - } - }, - "ja": { - "stringUnit": { - "state": "translated", - "value": "PR #%lld" - } - } - } - }, "tabBar.newTerminalTab": { "extractionState": "manual", "localizations": { diff --git a/Sources/WindowPaneChromePortal.swift b/Sources/WindowPaneChromePortal.swift index 35b2733da..fd918bd5d 100644 --- a/Sources/WindowPaneChromePortal.swift +++ b/Sources/WindowPaneChromePortal.swift @@ -494,7 +494,18 @@ final class WindowPaneChromePortalRegistry: NSObject, BonsplitPaneChromePortalBr var hostViewForTesting: NSView { hostView } func nativeTabFramesForTesting(in paneID: PaneID) -> [NSRect] { - bars[paneID]?.tabFramesForTesting ?? [] + bars[paneID]?.layoutSubtreeIfNeeded() + return bars[paneID]?.tabFramesForTesting ?? [] + } + + func nativeVisibleTabRectForTesting(in paneID: PaneID) -> NSRect { + bars[paneID]?.layoutSubtreeIfNeeded() + return bars[paneID]?.visibleDocumentRectForTesting ?? .zero + } + + func nativeTabDocumentWidthForTesting(in paneID: PaneID) -> CGFloat { + bars[paneID]?.layoutSubtreeIfNeeded() + return bars[paneID]?.documentWidthForTesting ?? 0 } func nativeTabInsertionIndexForTesting(in paneID: PaneID, documentX: CGFloat) -> Int? { @@ -595,7 +606,9 @@ private final class TabStripScrollView: NSScrollView { private final class NativePaneTabBarView: PaneChromeDragBackgroundView { private static let tabTransferPasteboardType = NSPasteboard.PasteboardType("com.splittabbar.tabtransfer") + private static let edgeFadeWidth: CGFloat = 24 private let scrollView = TabStripScrollView(frame: .zero) + private let edgeFadeMask = CAGradientLayer() // Unflipped on purpose: a flipped document view mirrors the glass pills' // built-in shadow upward (layer geometry flip flips shadowOffset), while // the control capsules in the unflipped host cast theirs downward. Layout @@ -649,6 +662,16 @@ private final class NativePaneTabBarView: PaneChromeDragBackgroundView { scrollView.horizontalScrollElasticity = .automatic scrollView.verticalScrollElasticity = .none scrollView.documentView = documentView + scrollView.wantsLayer = true + scrollView.contentView.postsBoundsChangedNotifications = true + NotificationCenter.default.addObserver( + self, + selector: #selector(clipBoundsDidChange), + name: NSView.boundsDidChangeNotification, + object: scrollView.contentView + ) + edgeFadeMask.startPoint = CGPoint(x: 0, y: 0.5) + edgeFadeMask.endPoint = CGPoint(x: 1, y: 0.5) addSubview(scrollView) documentView.addSubview(dropIndicator, positioned: .above, relativeTo: nil) registerForDraggedTypes([Self.tabTransferPasteboardType]) @@ -659,6 +682,8 @@ private final class NativePaneTabBarView: PaneChromeDragBackgroundView { @objc private func newTabPressed() { newTabAction?() } + @objc private func clipBoundsDidChange(_ note: Notification) { updateEdgeFades() } + required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func layout() { @@ -737,7 +762,9 @@ private final class NativePaneTabBarView: PaneChromeDragBackgroundView { newTabButton.frame = NSRect(x: x, y: 0, width: plusWidth, height: height) x += plusWidth + gap documentView.frame = NSRect(x: 0, y: 0, width: max(x, scrollView.contentSize.width), height: height) + constrainClipOrigin() revealPendingTab() + updateEdgeFades() if let dropIndicatorIndex { positionDropIndicator(at: dropIndicatorIndex) } @@ -758,6 +785,43 @@ private final class NativePaneTabBarView: PaneChromeDragBackgroundView { scrollView.reflectScrolledClipView(clip) } + /// Closing tabs shrinks the document under a scrolled clip view; pull the + /// origin back inside the new bounds. + private func constrainClipOrigin() { + let clip = scrollView.contentView + let constrained = clip.constrainBoundsRect(clip.bounds).origin + guard constrained.x != clip.bounds.origin.x else { return } + clip.setBoundsOrigin(NSPoint(x: constrained.x, y: 0)) + scrollView.reflectScrolledClipView(clip) + } + + /// Fades the edge that has hidden tabs. The mask exists only while the row + /// overflows so a row that fits renders exactly as before. + private func updateEdgeFades() { + let bounds = scrollView.contentView.bounds + let hasLeading = bounds.minX > 0.5 + let hasTrailing = bounds.maxX < documentView.frame.width - 0.5 + let width = scrollView.bounds.width + CATransaction.begin() + CATransaction.setDisableActions(true) + defer { CATransaction.commit() } + guard hasLeading || hasTrailing, width > Self.edgeFadeWidth * 2 else { + scrollView.layer?.mask = nil + return + } + let fraction = Self.edgeFadeWidth / width + let opaque = NSColor.black.cgColor + let clear = NSColor.clear.cgColor + edgeFadeMask.frame = scrollView.bounds + edgeFadeMask.colors = [hasLeading ? clear : opaque, opaque, opaque, hasTrailing ? clear : opaque] + edgeFadeMask.locations = [0, fraction, 1 - fraction, 1].map { NSNumber(value: Double($0)) } + scrollView.layer?.mask = edgeFadeMask + } + + fileprivate var visibleDocumentRectForTesting: NSRect { scrollView.contentView.bounds } + + fileprivate var documentWidthForTesting: CGFloat { documentView.frame.width } + fileprivate var tabFramesForTesting: [NSRect] { guard let descriptor else { return [] } return descriptor.tabs.compactMap { pillViews[$0.id]?.frame } diff --git a/Sources/Workspace.swift b/Sources/Workspace.swift index 4926ebe82..4cfd10061 100644 --- a/Sources/Workspace.swift +++ b/Sources/Workspace.swift @@ -831,39 +831,9 @@ final class Workspace: Identifiable, ObservableObject { !custom.isEmpty { return custom } - if let pullRequest = panelPullRequests[panelId] { - let panelBranch = normalizedSidebarBranchName(panelGitBranches[panelId]?.branch) - let pullRequestBranch = normalizedSidebarBranchName(pullRequest.branch) - if pullRequestBranch == nil || panelBranch == pullRequestBranch { - return String( - localized: "tabBar.autoTitle.pullRequest", - defaultValue: "PR #\(pullRequest.number)" - ) - } - } - if let branch = normalizedSidebarBranchName(panelGitBranches[panelId]?.branch) { - return Self.ticketIdentifier(in: branch) ?? branch - } return fallbackTitle } - private static let ticketIdentifierPattern = try? NSRegularExpression( - pattern: #"(? String? { - guard let ticketIdentifierPattern, - let match = ticketIdentifierPattern.firstMatch( - in: branch, - range: NSRange(branch.startIndex..., in: branch) - ), - match.numberOfRanges > 1, - let range = Range(match.range(at: 1), in: branch) else { - return nil - } - return String(branch[range]) - } - func syncResolvedPanelTitle(panelId: UUID) { guard let panel = panels[panelId], let tabId = surfaceIdFromPanelId(panelId) else { return } let baseTitle = panelTitles[panelId] ?? panel.displayTitle diff --git a/programaTests/WindowAndDragTests.swift b/programaTests/WindowAndDragTests.swift index 93351adb7..0af2129d8 100644 --- a/programaTests/WindowAndDragTests.swift +++ b/programaTests/WindowAndDragTests.swift @@ -209,6 +209,73 @@ final class WindowGlassEffectTests: XCTestCase { throw XCTSkip("Native Liquid Glass requires the macOS 26 SDK and runtime") } + func testNativePaneTabBarKeepsTitlesReadableAndScrollsSelectedTabIntoView() throws { + #if compiler(>=6.2) + if #available(macOS 26.0, *) { + _ = NSApplication.shared + let root = NSView(frame: NSRect(x: 0, y: 0, width: 360, height: 320)) + let terminalHost = WindowTerminalHostView(frame: root.bounds) + let anchor = NSView(frame: NSRect(x: 20, y: 270, width: 300, height: 40)) + root.addSubview(anchor) + root.addSubview(terminalHost, positioned: .above, relativeTo: anchor) + let window = NSWindow( + contentRect: root.bounds, + styleMask: [.titled, .closable, .resizable], + backing: .buffered, + defer: false + ) + defer { window.orderOut(nil) } + window.contentView = root + + let bridge = WindowPaneChromePortalRegistry.bridge(for: window) + let paneID = PaneID() + let ids = (0..<15).map { _ in TabID() } + func publish(selected: Int) { + let tabs = ids.enumerated().map { index, id in + BonsplitPaneChromeTabDescriptor( + id: id, title: "A long terminal title number \(index)", icon: "terminal", + iconImageData: nil, isSelected: index == selected, isPinned: false, + isDirty: false, showsNotificationBadge: false, accessibilityValue: "", + menuItems: [] + ) + } + bridge.updatePaneChrome(BonsplitPaneChromeDescriptor( + paneID: paneID, anchorView: anchor, tabs: tabs, isFocused: true, + isVisible: true, leadingInset: 0, showsSplitButtons: false, + onSelect: { _ in }, onClose: { _ in }, onContextAction: { _, _ in }, + dragPasteboardData: { _ in nil }, onDragStateChanged: { _, _ in }, + validatedDropIndex: { (0...tabs.count).contains($0) ? $0 : nil }, + onDropTab: { _ in true }, + onNewTab: {}, onSplitRight: {}, onSplitDown: {} + )) + root.layoutSubtreeIfNeeded() + } + + publish(selected: 0) + let readableMinimumWidth: CGFloat = 78 + let frames = bridge.nativeTabFramesForTesting(in: paneID) + XCTAssertEqual(frames.count, ids.count) + for frame in frames { + XCTAssertGreaterThanOrEqual(frame.width, readableMinimumWidth) + } + let visible = bridge.nativeVisibleTabRectForTesting(in: paneID) + XCTAssertGreaterThan(visible.width, 0) + XCTAssertGreaterThan(bridge.nativeTabDocumentWidthForTesting(in: paneID), visible.width) + XCTAssertLessThanOrEqual(try XCTUnwrap(frames.first).minX, visible.minX + 0.5) + + publish(selected: ids.count - 1) + let scrolledVisible = bridge.nativeVisibleTabRectForTesting(in: paneID) + let lastFrame = try XCTUnwrap(bridge.nativeTabFramesForTesting(in: paneID).last) + XCTAssertGreaterThan(scrolledVisible.minX, 0) + XCTAssertGreaterThanOrEqual(lastFrame.minX, scrolledVisible.minX) + XCTAssertLessThanOrEqual(lastFrame.maxX, scrolledVisible.maxX) + return + } + #endif + + throw XCTSkip("Native Liquid Glass requires the macOS 26 SDK and runtime") + } + func testNativePaneTabInsertionIndicatorStaysInsideDocumentBounds() throws { #if compiler(>=6.2) let bounds = NSRect(x: 0, y: 0, width: 240, height: 28) diff --git a/programaTests/WorkspaceUnitTests.swift b/programaTests/WorkspaceUnitTests.swift index 77d5fa868..6c9dac271 100644 --- a/programaTests/WorkspaceUnitTests.swift +++ b/programaTests/WorkspaceUnitTests.swift @@ -3066,7 +3066,7 @@ final class WorkspacePanelGitBranchTests: XCTestCase { XCTAssertEqual(tab.title, expected, file: file, line: line) } - func testPanelTitleUsesPRTicketBranchManualPrecedenceWithoutRenamingWorkspace() throws { + func testPanelTitleKeepsLiveTitleWhenBranchAndPullRequestArriveWithoutRenamingWorkspace() throws { let workspace = Workspace() let panelId = try XCTUnwrap(workspace.focusedPanelId) let pullRequestURL = try XCTUnwrap(URL(string: "https://github.com/darkroomengineering/programa/pull/482")) @@ -3080,9 +3080,6 @@ final class WorkspacePanelGitBranchTests: XCTestCase { branch: "feature/PROG-123-summary", isDirty: false ) - assertPanelAndTabTitle("PROG-123", workspace: workspace, panelId: panelId) - XCTAssertEqual(workspace.title, workspaceTitle) - workspace.updatePanelPullRequest( panelId: panelId, number: 482, @@ -3091,167 +3088,54 @@ final class WorkspacePanelGitBranchTests: XCTestCase { status: .open, branch: "feature/PROG-123-summary" ) - assertPanelAndTabTitle("PR #482", workspace: workspace, panelId: panelId) - XCTAssertEqual(workspace.title, workspaceTitle) - - workspace.setPanelCustomTitle(panelId: panelId, title: " Manual lane ") - assertPanelAndTabTitle("Manual lane", workspace: workspace, panelId: panelId) - - workspace.updatePanelGitBranch( - panelId: panelId, - branch: "feature/OPS9-77-follow-up", - isDirty: true - ) - workspace.updatePanelPullRequest( - panelId: panelId, - number: 483, - label: "Follow-up", - url: pullRequestURL, - status: .open, - branch: "feature/OPS9-77-follow-up" - ) - assertPanelAndTabTitle("Manual lane", workspace: workspace, panelId: panelId) - - workspace.setPanelCustomTitle(panelId: panelId, title: nil) - assertPanelAndTabTitle("PR #483", workspace: workspace, panelId: panelId) - - workspace.clearPanelPullRequest(panelId: panelId) - assertPanelAndTabTitle("OPS9-77", workspace: workspace, panelId: panelId) - - workspace.clearPanelGitBranch(panelId: panelId) assertPanelAndTabTitle("agent-shell", workspace: workspace, panelId: panelId) XCTAssertEqual( workspace.title, workspaceTitle, - "Panel metadata must not rename the workspace shown in the sidebar" - ) - } - - func testAutomaticPanelTitleUsesOnlyConservativeUppercaseTicketTokens() throws { - let workspace = Workspace() - let panelId = try XCTUnwrap(workspace.focusedPanelId) - XCTAssertTrue(workspace.updatePanelTitle(panelId: panelId, title: "live-process")) - let workspaceTitle = workspace.title - - workspace.updatePanelGitBranch( - panelId: panelId, - branch: " feature/lowercase-summary ", - isDirty: false - ) - assertPanelAndTabTitle( - "feature/lowercase-summary", - workspace: workspace, - panelId: panelId - ) - - workspace.updatePanelGitBranch( - panelId: panelId, - branch: "feature/prog-123-summary", - isDirty: false - ) - assertPanelAndTabTitle( - "feature/prog-123-summary", - workspace: workspace, - panelId: panelId - ) - - workspace.updatePanelGitBranch( - panelId: panelId, - branch: "feature/PROG-123-summary", - isDirty: false + "Branch and pull request metadata must not rename the workspace shown in the sidebar" ) - assertPanelAndTabTitle("PROG-123", workspace: workspace, panelId: panelId) - XCTAssertEqual(workspace.title, workspaceTitle) - } - func testPanelTitleUsesOnlyPullRequestMetadataScopedToTheCurrentBranch() throws { - let workspace = Workspace() - let panelId = try XCTUnwrap(workspace.focusedPanelId) - let pullRequestURL = try XCTUnwrap(URL(string: "https://github.com/darkroomengineering/programa/pull/484")) + workspace.setPanelCustomTitle(panelId: panelId, title: " Manual lane ") + assertPanelAndTabTitle("Manual lane", workspace: workspace, panelId: panelId) - XCTAssertTrue(workspace.updatePanelTitle(panelId: panelId, title: "live-process")) - workspace.updatePanelPullRequest( - panelId: panelId, - number: 484, - label: "Other lane", - url: pullRequestURL, - status: .open, - branch: "feature/OTHER-484-summary" - ) - assertPanelAndTabTitle( - "live-process", - workspace: workspace, - panelId: panelId - ) + workspace.setPanelCustomTitle(panelId: panelId, title: nil) + assertPanelAndTabTitle("agent-shell", workspace: workspace, panelId: panelId) - workspace.updatePanelGitBranch( - panelId: panelId, - branch: "feature/CURRENT-21-summary", - isDirty: false - ) - assertPanelAndTabTitle("CURRENT-21", workspace: workspace, panelId: panelId) + XCTAssertTrue(workspace.updatePanelTitle(panelId: panelId, title: "claude")) + assertPanelAndTabTitle("claude", workspace: workspace, panelId: panelId) + workspace.clearPanelPullRequest(panelId: panelId) workspace.clearPanelGitBranch(panelId: panelId) - workspace.updatePanelPullRequest( - panelId: panelId, - number: 485, - label: "Unscoped lane", - url: pullRequestURL, - status: .open - ) - assertPanelAndTabTitle( - "PR #485", - workspace: workspace, - panelId: panelId, - file: #filePath, - line: #line - ) + assertPanelAndTabTitle("claude", workspace: workspace, panelId: panelId) } - func testResetSidebarContextClearsAutomaticPanelTitlesButPreservesManualNames() throws { + func testTabsInTheSameRepositoryKeepTheirOwnLiveTitles() throws { let workspace = Workspace() - let panelId = try XCTUnwrap(workspace.focusedPanelId) - let pullRequestURL = try XCTUnwrap(URL(string: "https://github.com/darkroomengineering/programa/pull/486")) - - XCTAssertTrue(workspace.updatePanelTitle(panelId: panelId, title: "live-process")) - workspace.updatePanelGitBranch( - panelId: panelId, - branch: "feature/RESET-12-summary", - isDirty: false - ) - workspace.updatePanelPullRequest( - panelId: panelId, - number: 486, - label: "Reset lane", - url: pullRequestURL, - status: .open, - branch: "feature/RESET-12-summary" - ) - assertPanelAndTabTitle("PR #486", workspace: workspace, panelId: panelId) - - workspace.resetSidebarContext(reason: "test") - assertPanelAndTabTitle("live-process", workspace: workspace, panelId: panelId) - - workspace.setPanelCustomTitle(panelId: panelId, title: "Manual lane") - workspace.updatePanelGitBranch( - panelId: panelId, - branch: "feature/RESET-13-summary", - isDirty: false - ) - workspace.updatePanelPullRequest( - panelId: panelId, - number: 487, - label: "Reset manual lane", - url: pullRequestURL, - status: .open, - branch: "feature/RESET-13-summary" - ) + let firstPanelId = try XCTUnwrap(workspace.focusedPanelId) + let secondPanel = try XCTUnwrap( + workspace.newTerminalSplit(from: firstPanelId, orientation: .horizontal, focus: false) + ) + let pullRequestURL = try XCTUnwrap(URL(string: "https://github.com/darkroomengineering/programa/pull/840")) + + XCTAssertTrue(workspace.updatePanelTitle(panelId: firstPanelId, title: "claude")) + XCTAssertTrue(workspace.updatePanelTitle(panelId: secondPanel.id, title: "vim README.md")) + for panelId in [firstPanelId, secondPanel.id] { + workspace.updatePanelGitBranch(panelId: panelId, branch: "fix/shared-branch", isDirty: false) + workspace.updatePanelPullRequest( + panelId: panelId, + number: 840, + label: "Shared lane", + url: pullRequestURL, + status: .open, + branch: "fix/shared-branch" + ) + } - workspace.resetSidebarContext(reason: "test-manual") - assertPanelAndTabTitle("Manual lane", workspace: workspace, panelId: panelId) + assertPanelAndTabTitle("claude", workspace: workspace, panelId: firstPanelId) + assertPanelAndTabTitle("vim README.md", workspace: workspace, panelId: secondPanel.id) } - func testDetachAttachDoesNotPersistAutomaticPanelTitleButPreservesManualName() throws { + func testDetachAttachPreservesLiveAndManualPanelTitles() throws { let source = Workspace() let destination = Workspace() let finalDestination = Workspace() @@ -3261,7 +3145,6 @@ final class WorkspacePanelGitBranchTests: XCTestCase { finalDestination.teardownAllPanels() } let panelId = try XCTUnwrap(source.focusedPanelId) - let pullRequestURL = try XCTUnwrap(URL(string: "https://github.com/darkroomengineering/programa/pull/488")) XCTAssertTrue(source.updatePanelTitle(panelId: panelId, title: "live-process")) source.updatePanelGitBranch( @@ -3269,27 +3152,14 @@ final class WorkspacePanelGitBranchTests: XCTestCase { branch: "feature/MOVE-33-summary", isDirty: false ) - source.updatePanelPullRequest( - panelId: panelId, - number: 488, - label: "Move lane", - url: pullRequestURL, - status: .open, - branch: "feature/MOVE-33-summary" - ) - assertPanelAndTabTitle("PR #488", workspace: source, panelId: panelId) - let detachedAutomatic = try XCTUnwrap(source.detachSurface(panelId: panelId)) + let detachedLive = try XCTUnwrap(source.detachSurface(panelId: panelId)) let destinationPane = try XCTUnwrap(destination.bonsplitController.allPaneIds.first) XCTAssertEqual( - destination.attachDetachedSurface(detachedAutomatic, inPane: destinationPane, focus: false), + destination.attachDetachedSurface(detachedLive, inPane: destinationPane, focus: false), panelId ) - assertPanelAndTabTitle( - "live-process", - workspace: destination, - panelId: panelId - ) + assertPanelAndTabTitle("live-process", workspace: destination, panelId: panelId) destination.setPanelCustomTitle(panelId: panelId, title: "Manual moved lane") let detachedManual = try XCTUnwrap(destination.detachSurface(panelId: panelId)) @@ -3298,14 +3168,10 @@ final class WorkspacePanelGitBranchTests: XCTestCase { finalDestination.attachDetachedSurface(detachedManual, inPane: finalPane, focus: false), panelId ) - assertPanelAndTabTitle( - "Manual moved lane", - workspace: finalDestination, - panelId: panelId - ) + assertPanelAndTabTitle("Manual moved lane", workspace: finalDestination, panelId: panelId) } - func testSessionRestoreKeepsRawLiveTitleSeparateFromAutomaticAndManualTitles() throws { + func testSessionRestoreKeepsLiveTitleSeparateFromManualTitle() throws { let source = Workspace() let restored = Workspace() defer { @@ -3324,26 +3190,14 @@ final class WorkspacePanelGitBranchTests: XCTestCase { let snapshot = source.sessionSnapshot(includeScrollback: false) let panelSnapshot = try XCTUnwrap(snapshot.panels.first { $0.id == panelId }) - XCTAssertEqual( - panelSnapshot.title, - "live-process", - "The persisted base title must remain the live title so transient metadata cannot become stale state" - ) + XCTAssertEqual(panelSnapshot.title, "live-process") XCTAssertEqual(panelSnapshot.customTitle, "Manual restored lane") - XCTAssertEqual(panelSnapshot.gitBranch?.branch, "feature/RESTORE-91-summary") restored.restoreSessionSnapshot(snapshot) let restoredPanelId = try XCTUnwrap(restored.focusedPanelId) - assertPanelAndTabTitle( - "Manual restored lane", - workspace: restored, - panelId: restoredPanelId - ) + assertPanelAndTabTitle("Manual restored lane", workspace: restored, panelId: restoredPanelId) restored.setPanelCustomTitle(panelId: restoredPanelId, title: nil) - assertPanelAndTabTitle("RESTORE-91", workspace: restored, panelId: restoredPanelId) - - restored.clearPanelGitBranch(panelId: restoredPanelId) assertPanelAndTabTitle("live-process", workspace: restored, panelId: restoredPanelId) } diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Styling/TabBarMetrics.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Styling/TabBarMetrics.swift index 410fd44a2..5587f8c04 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Styling/TabBarMetrics.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Styling/TabBarMetrics.swift @@ -19,7 +19,9 @@ enum TabBarMetrics { // MARK: - Individual Tabs static var tabHeight: CGFloat { resolved("tabBarHeight", default: 26) } - static let tabMinWidth: CGFloat = 48 + /// Floor for a tab's width: below it a title shows only a letter or two, so + /// the strip scrolls instead of squeezing tabs further. + static let tabMinWidth: CGFloat = 120 static let tabMaxWidth: CGFloat = 220 static let tabCornerRadius: CGFloat = 0 static var tabHorizontalPadding: CGFloat { resolved("tabHorizontalPadding", default: 8) } diff --git a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift index 7a599aa19..ab5838e1d 100644 --- a/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift +++ b/vendor/bonsplit/Sources/Bonsplit/Internal/Views/TabItemView.swift @@ -193,6 +193,8 @@ struct TabItemView: View { minHeight: TabBarMetrics.tabHeight, maxHeight: TabBarMetrics.tabHeight ) + // Size to the title within [min, max]; the strip's proposal never shrinks a tab. + .fixedSize(horizontal: true, vertical: false) .padding(.bottom, isSelected ? 1 : 0) .background(tabBackground.saturation(saturation)) .animation(.easeOut(duration: 0.14), value: showsShortcutHint)