Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .claude/scheduled_tasks.lock
Original file line number Diff line number Diff line change
@@ -1 +1 @@
{"sessionId":"9c9943de-bc79-4f2c-9b82-cbd58c3a1d51","pid":32319,"procStart":"Sun May 31 00:20:09 2026","acquiredAt":1780187614705}
{"sessionId":"5c2a62d0-99c2-42c0-9452-f67b6fbaacda","pid":75304,"procStart":"Thu Jun 4 23:34:54 2026","acquiredAt":1780616564932}
76 changes: 76 additions & 0 deletions Sources/SidebarWorkspaceGroupHeaderMetrics.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import CoreGraphics

/// Font sizes, icon/control frames, and badge padding for
/// ``SidebarWorkspaceGroupHeaderView``, derived from the sidebar font scale.
///
/// The collapsible workspace group/folder header must grow proportionally with
/// the configurable sidebar font size, just like the workspace rows below it.
/// `TabItemView` already scales its subviews by `settings.sidebarFontScale`
/// (see ``SidebarTabItemFontScale``); this type is the single place that
/// applies the same scale to the group header so the two grow at the same rate
/// from one scaling path.
///
/// The `base*` constants are the design sizes at the default sidebar font size,
/// where ``SidebarTabItemFontScale/scale(for:)`` returns `1.0`. Every metric is
/// the corresponding base value multiplied by ``fontScale``.
///
/// ```swift
/// let metrics = SidebarWorkspaceGroupHeaderMetrics(fontScale: settings.sidebarFontScale)
/// Image(systemName: "chevron.down")
/// .font(.system(size: metrics.chevronFontSize, weight: .semibold))
/// .frame(width: metrics.chevronFrame, height: metrics.chevronFrame)
/// ```
struct SidebarWorkspaceGroupHeaderMetrics: Equatable {
/// The sidebar font scale; `1.0` at the default sidebar font size and
/// proportionally larger as the configured `sidebar-font-size` grows.
let fontScale: CGFloat

/// Creates header metrics for the given sidebar font scale.
/// - Parameter fontScale: The scale from ``SidebarTabItemFontScale/scale(for:)``;
/// `1.0` at the default sidebar font size.
init(fontScale: CGFloat) {
self.fontScale = fontScale
}

/// Chevron glyph point size at the default sidebar font size.
static let baseChevronFontSize: CGFloat = 9
/// Chevron tap-target frame edge at the default sidebar font size.
static let baseChevronFrame: CGFloat = 14
/// Folder/group icon point size at the default sidebar font size.
static let baseIconFontSize: CGFloat = 11
/// Folder/group icon frame edge at the default sidebar font size.
static let baseIconFrame: CGFloat = 14
/// Group name point size at the default sidebar font size.
static let baseNameFontSize: CGFloat = 11
/// Unread badge point size at the default sidebar font size.
static let baseUnreadFontSize: CGFloat = 10
/// Unread badge horizontal padding at the default sidebar font size.
static let baseUnreadHorizontalPadding: CGFloat = 5
/// Unread badge vertical padding at the default sidebar font size.
static let baseUnreadVerticalPadding: CGFloat = 1
/// Plus-button glyph point size at the default sidebar font size.
static let basePlusFontSize: CGFloat = 11
/// Plus-button frame edge at the default sidebar font size.
static let basePlusFrame: CGFloat = 18

/// Scaled chevron glyph point size.
var chevronFontSize: CGFloat { Self.baseChevronFontSize * fontScale }
/// Scaled chevron tap-target frame edge.
var chevronFrame: CGFloat { Self.baseChevronFrame * fontScale }
/// Scaled folder/group icon point size.
var iconFontSize: CGFloat { Self.baseIconFontSize * fontScale }
/// Scaled folder/group icon frame edge.
var iconFrame: CGFloat { Self.baseIconFrame * fontScale }
/// Scaled group name point size.
var nameFontSize: CGFloat { Self.baseNameFontSize * fontScale }
/// Scaled unread badge point size.
var unreadFontSize: CGFloat { Self.baseUnreadFontSize * fontScale }
/// Scaled unread badge horizontal padding.
var unreadHorizontalPadding: CGFloat { Self.baseUnreadHorizontalPadding * fontScale }
/// Scaled unread badge vertical padding.
var unreadVerticalPadding: CGFloat { Self.baseUnreadVerticalPadding * fontScale }
/// Scaled plus-button glyph point size.
var plusFontSize: CGFloat { Self.basePlusFontSize * fontScale }
/// Scaled plus-button frame edge.
var plusFrame: CGFloat { Self.basePlusFrame * fontScale }
}
28 changes: 17 additions & 11 deletions Sources/SidebarWorkspaceGroupHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
lhs.showsShortcutHint == rhs.showsShortcutHint &&
lhs.shortcutHintXOffset == rhs.shortcutHintXOffset &&
lhs.shortcutHintYOffset == rhs.shortcutHintYOffset &&
lhs.fontScale == rhs.fontScale &&
lhs.cwdContextMenuItems == rhs.cwdContextMenuItems &&
lhs.newWorkspacePlacement == rhs.newWorkspacePlacement &&
lhs.rowSpacing == rhs.rowSpacing &&
Expand All @@ -45,6 +46,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
let showsShortcutHint: Bool
let shortcutHintXOffset: Double
let shortcutHintYOffset: Double
let fontScale: CGFloat
let cwdContextMenuItems: [CmuxResolvedConfigContextMenuItem]
let newWorkspacePlacement: WorkspaceGroupNewPlacement?
let rowSpacing: CGFloat
Expand All @@ -67,6 +69,10 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
@State private var isHovered = false
@State private var rowHeight: CGFloat = 1

private var metrics: SidebarWorkspaceGroupHeaderMetrics {
SidebarWorkspaceGroupHeaderMetrics(fontScale: fontScale)
}

private var iconColor: Color {
if let tintHex, let nsColor = NSColor(hex: tintHex) {
return Color(nsColor: nsColor)
Expand Down Expand Up @@ -100,9 +106,9 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
var body: some View {
HStack(spacing: 4) {
Image(systemName: isCollapsed ? "chevron.right" : "chevron.down")
.font(.system(size: 9, weight: .semibold))
.font(.system(size: metrics.chevronFontSize, weight: .semibold))
.foregroundStyle(.secondary)
.frame(width: 14, height: 14)
.frame(width: metrics.chevronFrame, height: metrics.chevronFrame)
.contentShape(Rectangle())
.onTapGesture { onToggleCollapsed() }
.accessibilityAddTraits(.isButton)
Expand All @@ -116,21 +122,21 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {

HStack(spacing: 6) {
Image(systemName: displayedIconSymbol)
.font(.system(size: 11, weight: .semibold))
.font(.system(size: metrics.iconFontSize, weight: .semibold))
.foregroundStyle(iconColor)
.frame(width: 14, height: 14)
.frame(width: metrics.iconFrame, height: metrics.iconFrame)
.accessibilityHidden(true)
Text(name)
.font(.system(size: 11, weight: .semibold))
.font(.system(size: metrics.nameFontSize, weight: .semibold))
.foregroundStyle(isAnchorActive ? Color.primary : Color.primary.opacity(0.9))
.lineLimit(1)
.truncationMode(.tail)
if anchorUnreadCount > 0 {
Text("\(anchorUnreadCount)")
.font(.system(size: 10, weight: .semibold))
.font(.system(size: metrics.unreadFontSize, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, 5)
.padding(.vertical, 1)
.padding(.horizontal, metrics.unreadHorizontalPadding)
.padding(.vertical, metrics.unreadVerticalPadding)
.background(Capsule().fill(Color.accentColor))
.accessibilityLabel(Text(String.localizedStringWithFormat(
String(localized: "workspaceGroup.unread.a11y", defaultValue: "%lld unread"),
Expand All @@ -151,14 +157,14 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
let plusVisible = isHovered && !showsShortcutHint
Button(action: onTapPlus) {
Image(systemName: "plus")
.font(.system(size: 11, weight: .medium))
.font(.system(size: metrics.plusFontSize, weight: .medium))
.foregroundStyle(.secondary)
.frame(width: 18, height: 18)
.frame(width: metrics.plusFrame, height: metrics.plusFrame)
.contentShape(Rectangle())
.opacity(plusVisible ? 1 : 0)
}
.buttonStyle(.plain)
.frame(width: 18, height: 18)
.frame(width: metrics.plusFrame, height: metrics.plusFrame)
.allowsHitTesting(plusVisible)
.accessibilityHidden(!plusVisible)
.accessibilityLabel(Text(String(
Expand Down
1 change: 1 addition & 0 deletions Sources/VerticalTabsSidebar+WorkspaceGroups.swift
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ extension VerticalTabsSidebar {
showsShortcutHint: showsHintForAnchor,
shortcutHintXOffset: settings.sidebarShortcutHintXOffset,
shortcutHintYOffset: settings.sidebarShortcutHintYOffset,
fontScale: settings.sidebarFontScale,
cwdContextMenuItems: cwdContextMenuItems,
newWorkspacePlacement: newWorkspacePlacement,
rowSpacing: tabRowSpacing,
Expand Down
8 changes: 8 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,8 @@
C135190000000000000000B1 /* SidebarWorkspaceGroupConfigOpenerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C135190000000000000000B2 /* SidebarWorkspaceGroupConfigOpenerTests.swift */; };
C9A57105C9A57105C9A57105 /* SidebarWorkspaceGroupContextMenuRunner.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57106C9A57106C9A57106 /* SidebarWorkspaceGroupContextMenuRunner.swift */; };
C9A57107C9A57107C9A57107 /* SidebarWorkspaceGroupDialogs.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57108C9A57108C9A57108 /* SidebarWorkspaceGroupDialogs.swift */; };
C9A57301C9A57301C9A57301 /* SidebarWorkspaceGroupHeaderMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57302C9A57302C9A57302 /* SidebarWorkspaceGroupHeaderMetrics.swift */; };
C9A57303C9A57303C9A57303 /* SidebarWorkspaceGroupHeaderMetricsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57304C9A57304C9A57304 /* SidebarWorkspaceGroupHeaderMetricsTests.swift */; };
C9A57101C9A57101C9A57101 /* SidebarWorkspaceGroupHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */; };
C9A57109C9A57109C9A57109 /* SidebarWorkspaceGroupingMetrics.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */; };
C9A57201C9A57201C9A57201 /* SidebarWorkspaceRenderItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */; };
Expand Down Expand Up @@ -1129,6 +1131,8 @@
C135190000000000000000B2 /* SidebarWorkspaceGroupConfigOpenerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupConfigOpenerTests.swift; sourceTree = "<group>"; };
C9A57106C9A57106C9A57106 /* SidebarWorkspaceGroupContextMenuRunner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupContextMenuRunner.swift; sourceTree = "<group>"; };
C9A57108C9A57108C9A57108 /* SidebarWorkspaceGroupDialogs.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupDialogs.swift; sourceTree = "<group>"; };
C9A57302C9A57302C9A57302 /* SidebarWorkspaceGroupHeaderMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupHeaderMetrics.swift; sourceTree = "<group>"; };
C9A57304C9A57304C9A57304 /* SidebarWorkspaceGroupHeaderMetricsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupHeaderMetricsTests.swift; sourceTree = "<group>"; };
C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupHeaderView.swift; sourceTree = "<group>"; };
C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceGroupingMetrics.swift; sourceTree = "<group>"; };
C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarWorkspaceRenderItem.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1519,6 +1523,7 @@
C9A57108C9A57108C9A57108 /* SidebarWorkspaceGroupDialogs.swift */,
CA1F0A02CA1F0A02CA1F0A02 /* CmuxModalAlertPresentation.swift */,
C9A57102C9A57102C9A57102 /* SidebarWorkspaceGroupHeaderView.swift */,
C9A57302C9A57302C9A57302 /* SidebarWorkspaceGroupHeaderMetrics.swift */,
C9A5710AC9A5710AC9A5710A /* SidebarWorkspaceGroupingMetrics.swift */,
D5037010000000000000002 /* RenderableSystemSymbol.swift */,
C9A57202C9A57202C9A57202 /* SidebarWorkspaceRenderItem.swift */,
Expand Down Expand Up @@ -1962,6 +1967,7 @@
FEEDC0DEC0DEC0DEC0DE0002 /* FeedCoordinatorTests.swift */,
1D301919B10F22B8708E8883 /* WorkspaceManualUnreadTests.swift */,
EE0171AF1F49F7547191CEE5 /* SidebarWidthPolicyTests.swift */,
C9A57304C9A57304C9A57304 /* SidebarWorkspaceGroupHeaderMetricsTests.swift */,
51D800000000000000000002 /* SidebarIdentifierFormattingTests.swift */,
C7A507000000000000000001 /* TaskManagerResourcesTests.swift */,
C7A507000000000000004530 /* TaskManagerViewSnapshotBoundaryTests.swift */,
Expand Down Expand Up @@ -2607,6 +2613,7 @@
C9A57103C9A57103C9A57103 /* SidebarWorkspaceGroupConfigOpener.swift in Sources */,
C9A57105C9A57105C9A57105 /* SidebarWorkspaceGroupContextMenuRunner.swift in Sources */,
C9A57107C9A57107C9A57107 /* SidebarWorkspaceGroupDialogs.swift in Sources */,
C9A57301C9A57301C9A57301 /* SidebarWorkspaceGroupHeaderMetrics.swift in Sources */,
C9A57101C9A57101C9A57101 /* SidebarWorkspaceGroupHeaderView.swift in Sources */,
C9A57109C9A57109C9A57109 /* SidebarWorkspaceGroupingMetrics.swift in Sources */,
C9A57201C9A57201C9A57201 /* SidebarWorkspaceRenderItem.swift in Sources */,
Expand Down Expand Up @@ -2906,6 +2913,7 @@
CA39C0304FE351A21C372429 /* SidebarWidthPolicyTests.swift in Sources */,
D7AB34300000000000000005 /* SidebarWorkspaceDropPlannerTests.swift in Sources */,
C135190000000000000000B1 /* SidebarWorkspaceGroupConfigOpenerTests.swift in Sources */,
C9A57303C9A57303C9A57303 /* SidebarWorkspaceGroupHeaderMetricsTests.swift in Sources */,
62270F3DCECB4787D789CCE3 /* SidebarWorkspaceSnapshotRefreshPolicyTests.swift in Sources */,
F8000000A1B2C3D4E5F60718 /* SocketControlPasswordStoreTests.swift in Sources */,
F6355600A1B2C3D4E5F60718 /* SSHStartupSignalLifecycleTests.swift in Sources */,
Expand Down
69 changes: 69 additions & 0 deletions cmuxTests/SidebarWorkspaceGroupHeaderMetricsTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import CoreGraphics
import Testing

#if canImport(cmux_DEV)
@testable import cmux_DEV
#elseif canImport(cmux)
@testable import cmux
#endif

@Suite struct SidebarWorkspaceGroupHeaderMetricsTests {
@Test func metricsMatchBaseSizesAtDefaultScale() {
let metrics = SidebarWorkspaceGroupHeaderMetrics(fontScale: 1)

#expect(metrics.chevronFontSize == SidebarWorkspaceGroupHeaderMetrics.baseChevronFontSize)
#expect(metrics.chevronFrame == SidebarWorkspaceGroupHeaderMetrics.baseChevronFrame)
#expect(metrics.iconFontSize == SidebarWorkspaceGroupHeaderMetrics.baseIconFontSize)
#expect(metrics.iconFrame == SidebarWorkspaceGroupHeaderMetrics.baseIconFrame)
#expect(metrics.nameFontSize == SidebarWorkspaceGroupHeaderMetrics.baseNameFontSize)
#expect(metrics.unreadFontSize == SidebarWorkspaceGroupHeaderMetrics.baseUnreadFontSize)
#expect(metrics.unreadHorizontalPadding == SidebarWorkspaceGroupHeaderMetrics.baseUnreadHorizontalPadding)
#expect(metrics.unreadVerticalPadding == SidebarWorkspaceGroupHeaderMetrics.baseUnreadVerticalPadding)
#expect(metrics.plusFontSize == SidebarWorkspaceGroupHeaderMetrics.basePlusFontSize)
#expect(metrics.plusFrame == SidebarWorkspaceGroupHeaderMetrics.basePlusFrame)
}

@Test func metricsScaleProportionallyWhenSidebarFontEnlarged() {
let scale: CGFloat = 2
let metrics = SidebarWorkspaceGroupHeaderMetrics(fontScale: scale)

#expect(metrics.chevronFontSize == SidebarWorkspaceGroupHeaderMetrics.baseChevronFontSize * scale)
#expect(metrics.chevronFrame == SidebarWorkspaceGroupHeaderMetrics.baseChevronFrame * scale)
#expect(metrics.iconFontSize == SidebarWorkspaceGroupHeaderMetrics.baseIconFontSize * scale)
#expect(metrics.iconFrame == SidebarWorkspaceGroupHeaderMetrics.baseIconFrame * scale)
#expect(metrics.nameFontSize == SidebarWorkspaceGroupHeaderMetrics.baseNameFontSize * scale)
#expect(metrics.unreadFontSize == SidebarWorkspaceGroupHeaderMetrics.baseUnreadFontSize * scale)
#expect(metrics.unreadHorizontalPadding == SidebarWorkspaceGroupHeaderMetrics.baseUnreadHorizontalPadding * scale)
#expect(metrics.unreadVerticalPadding == SidebarWorkspaceGroupHeaderMetrics.baseUnreadVerticalPadding * scale)
#expect(metrics.plusFontSize == SidebarWorkspaceGroupHeaderMetrics.basePlusFontSize * scale)
#expect(metrics.plusFrame == SidebarWorkspaceGroupHeaderMetrics.basePlusFrame * scale)
}

@Test func metricsScaleProportionallyWhenSidebarFontShrunk() {
let scale: CGFloat = 0.5
let metrics = SidebarWorkspaceGroupHeaderMetrics(fontScale: scale)

#expect(metrics.chevronFontSize == SidebarWorkspaceGroupHeaderMetrics.baseChevronFontSize * scale)
#expect(metrics.chevronFrame == SidebarWorkspaceGroupHeaderMetrics.baseChevronFrame * scale)
#expect(metrics.iconFontSize == SidebarWorkspaceGroupHeaderMetrics.baseIconFontSize * scale)
#expect(metrics.iconFrame == SidebarWorkspaceGroupHeaderMetrics.baseIconFrame * scale)
#expect(metrics.nameFontSize == SidebarWorkspaceGroupHeaderMetrics.baseNameFontSize * scale)
#expect(metrics.unreadFontSize == SidebarWorkspaceGroupHeaderMetrics.baseUnreadFontSize * scale)
#expect(metrics.unreadHorizontalPadding == SidebarWorkspaceGroupHeaderMetrics.baseUnreadHorizontalPadding * scale)
#expect(metrics.unreadVerticalPadding == SidebarWorkspaceGroupHeaderMetrics.baseUnreadVerticalPadding * scale)
#expect(metrics.plusFontSize == SidebarWorkspaceGroupHeaderMetrics.basePlusFontSize * scale)
#expect(metrics.plusFrame == SidebarWorkspaceGroupHeaderMetrics.basePlusFrame * scale)
}

@Test func headerAndRowFontScaleShareOneScalingPath() {
// The header must grow at the same rate as the workspace rows, which
// derive their scale from SidebarTabItemFontScale. Reusing that scale
// keeps the two in lockstep instead of introducing a second path.
let enlargedSize = GhosttyConfig.defaultSidebarFontSize * 1.6
let rowScale = SidebarTabItemFontScale.scale(for: enlargedSize)
let metrics = SidebarWorkspaceGroupHeaderMetrics(fontScale: rowScale)

#expect(metrics.nameFontSize == SidebarWorkspaceGroupHeaderMetrics.baseNameFontSize * rowScale)
#expect(rowScale > 1)
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
Loading