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
4 changes: 2 additions & 2 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16050,6 +16050,8 @@ struct TabItemView: View, Equatable {
.transition(.opacity)
}

SidebarCloudWorkspaceBadgeView(label: workspaceSnapshot.cloudWorkspaceLabel, pointSize: scaledFontSize(10), tint: activeSecondaryColor(0.7))

if isEditing {
SidebarInlineRenameField(
initialText: renameDraft,
Expand Down Expand Up @@ -16091,8 +16093,6 @@ struct TabItemView: View, Equatable {
.layoutPriority(1)
}

SidebarCloudWorkspaceBadgeView(label: workspaceSnapshot.cloudWorkspaceLabel, pointSize: scaledFontSize(10), tint: activeSecondaryColor(0.7))

if trailingStatusActive || canCloseWorkspace {
SidebarWorkspaceTrailingStatusSlot(showsSpinner: spinnerOnTrailing, showsBadge: badgeOnTrailing, unreadCount: unreadCount, side: scaledUnreadBadgeSize, width: scaledCloseButtonWidth, height: scaledCloseButtonHitSize, badgeFont: badgeFont, badgeFillColor: activeUnreadBadgeFillColor, badgeTextColor: activeUnreadBadgeTextColor, spinnerColor: spinnerColor, spinnerTooltip: spinnerTooltip, canCloseWorkspace: canCloseWorkspace, showsCloseButton: showCloseButton, closeButtonTooltip: closeButtonTooltip, closeButtonColor: activeSecondaryColor(0.7), closeButtonFontSize: scaledFontSize(9), closeAction: actions.closeWorkspace)
}
Expand Down
12 changes: 6 additions & 6 deletions Sources/NSImageView+SidebarWorkspaceAccessory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,15 @@ extension NSImageView {
contentTintColor = tint
}

/// Reserves one fixed accessory slot without changing the row's vertical layout.
func layoutSidebarWorkspaceAccessory(
maxX: CGFloat, centerY: CGFloat, side: CGFloat, spacing: CGFloat, apply: Bool
/// Reserves one leading accessory slot without changing the row's vertical layout.
func layoutLeadingSidebarWorkspaceAccessory(
minX: CGFloat, centerY: CGFloat, side: CGFloat, spacing: CGFloat, apply: Bool
) -> CGFloat {
guard !isHidden else { return maxX }
guard !isHidden else { return minX }
if apply {
frame = NSRect(x: maxX - side, y: centerY - side / 2, width: side, height: side)
frame = NSRect(x: minX, y: centerY - side / 2, width: side, height: side)
}
return maxX - side - spacing
return minX + side + spacing
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -1146,14 +1146,14 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {
x += glyphSize.width + titleRowSpacing
}

x = cloudImageView.layoutLeadingSidebarWorkspaceAccessory(
minX: x, centerY: firstLineCenter, side: model.scaled(10) + 4, spacing: titleRowSpacing, apply: apply
)
// Trailing slot
let closeHit = max(16, 16 * model.fontScale)
let closeWidth = max(16, closeHit)
let trailingSlotActive = !trailingBadge.isHidden || (trailingSpinner?.isHidden == false) || model.canCloseWorkspace
let accessoryMaxX = trailingSlotActive ? (trailing - closeWidth - titleRowSpacing) : trailing
let titleMaxX = cloudImageView.layoutSidebarWorkspaceAccessory(
maxX: accessoryMaxX, centerY: firstLineCenter, side: model.scaled(10) + 4, spacing: titleRowSpacing, apply: apply
)
let titleMaxX = trailingSlotActive ? (trailing - closeWidth - titleRowSpacing) : trailing
let titleWidth = max(10, titleMaxX - x)
let renameField = renameSession?.field
let titleHeight = renameField.map { ceil($0.intrinsicContentSize.height) }
Expand Down
87 changes: 71 additions & 16 deletions cmuxTests/SidebarCloudWorkspaceBadgeTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -63,41 +63,94 @@ struct SidebarCloudWorkspaceBadgeTests {
}
}

/// Ensures the secondary badge keeps title space at narrow widths.
@Test(arguments: [false, true], [180.0, 280.0])
func cloudBadgeIsSecondaryAndKeepsNarrowTitlesVisible(dark: Bool, width: Double) throws {
/// Exercises the real row geometry across selection, density, scaling, and pinning.
@Test(arguments: [180.0, 280.0], [false, true])
func cloudBadgeLeadsTitleWithoutDisplacingPin(width: Double, isPinned: Bool) throws {
for dark in [false, true] {
for isActive in [false, true] {
for compact in [false, true] {
for magnification in [100, 150] {
try verifyCloudBadge(width: width, isPinned: isPinned, dark: dark,
isActive: isActive, compact: compact, magnification: magnification)
}
}
}
}
}

private func verifyCloudBadge(
width: Double, isPinned: Bool, dark: Bool, isActive: Bool, compact: Bool, magnification: Int
) throws {
let defaults = Self.makeDefaults()
defaults.set(false, forKey: "sidebarWrapWorkspaceTitles")
defaults.set(true, forKey: "sidebarHideAllDetails")
defaults.set(compact, forKey: "sidebarHideAllDetails")
let settings = SidebarTabItemSettingsSnapshot(defaults: defaults)
let workspace = Workspace(title: "Same project with a long workspace name", initialSurface: .cloudVMLoading)
let workspace = Workspace(title: "Same project with a long workspace name",
workingDirectory: "/home/cmux", initialSurface: .cloudVMLoading)
workspace.isPinned = isPinned
let factory = SidebarWorkspaceSnapshotFactory(workspace: workspace, settings: settings, showsAgentActivity: false)
let localSnapshot = factory.makeSnapshot()
workspace.cloudVMBinding = WorkspaceCloudVMBinding(vmID: "vivid-newt", isBase: false)
workspace.updateCloudPanelDirectory(panelId: try #require(workspace.focusedPanelId), directory: "/home/cmux")
let cloudSnapshot = factory.makeSnapshot()
let cell = SidebarAppKitRowCellTests.configuredCell(model: Self.makeModel(settings: settings, workspaceSnapshot: cloudSnapshot, colorSchemeIsDark: dark))
let model = Self.makeModel(settings: settings, workspaceSnapshot: cloudSnapshot,
colorSchemeIsDark: dark, isActive: isActive, magnification: magnification)
let cell = SidebarAppKitRowCellTests.configuredCell(model: model, tab: workspace)
cell.appearance = NSAppearance(named: dark ? .darkAqua : .aqua)
cell.frame = NSRect(x: 0, y: 0, width: width, height: 100)
let height = cell.layoutContent(model: model, width: width, apply: false)
cell.frame = NSRect(x: 0, y: 0, width: width, height: height)
let window = NSWindow(contentRect: cell.frame, styleMask: [], backing: .buffered, defer: false)
window.contentView = cell
defer { window.contentView = nil }
cell.layoutSubtreeIfNeeded()
let badge = try #require(SidebarAppKitRowCellTests.descendants(of: cell).compactMap { $0 as? NSImageView }.first {
let images = SidebarAppKitRowCellTests.descendants(of: cell).compactMap { $0 as? NSImageView }
let badges = images.filter {
$0.accessibilityIdentifier() == "sidebarCloudBadge"
})
}
#expect(badges.count == 1)
let badge = try #require(badges.first)
let title = try #require(SidebarAppKitRowCellTests.descendants(of: cell).compactMap { $0 as? SidebarRowTextView }.first {
$0.stringValue == cloudSnapshot.title
})
let bitmap = try #require(cell.bitmapImageRepForCachingDisplay(in: cell.bounds))
cell.cacheDisplay(in: cell.bounds, to: bitmap)
#if compiler(>=6.2)
Attachment.record(try #require(bitmap.representation(using: .png, properties: [:])),
named: "cloud-\(Int(width))-pin\(isPinned)-active\(isActive)-compact\(compact)-dark\(dark)-scale\(magnification).png")
#endif
#expect(!badge.isHidden)
#expect(badge.image != nil)
#expect(badge.toolTip == "Cloud workspace on vivid-newt")
#expect(badge.contentTintColor != title.textColor)
#expect(title.frame.width > 60)
#expect(title.frame.maxX <= badge.frame.minX)
#expect(badge.frame.maxX <= width)
let height = cell.layoutContent(model: try #require(cell.currentModelForMeasurement), width: width, apply: false)
cell.applyRebuiltModel(Self.makeModel(settings: settings, workspaceSnapshot: localSnapshot, colorSchemeIsDark: dark))
#expect(badge.frame.maxX + 8 == title.frame.minX)
#expect(title.frame.maxX <= width)
#expect(title.lineBreakMode == .byTruncatingTail)
#expect(cell.accessibilityLabel()?.contains("Cloud workspace on vivid-newt") == true)
let pins = images.filter { !$0.isHidden && $0.toolTip == String(
localized: "sidebar.pinnedWorkspaceProtected.tooltip", defaultValue: "Pinned workspace — protected from Close") }
#expect(pins.count == (isPinned ? 1 : 0))
if isPinned {
let pin = try #require(pins.first)
#expect(pin.frame.maxX + 8 == badge.frame.minX)
#expect(pin.frame.midY == badge.frame.midY)
}
let cloudTitleFrame = title.frame
let directoryFrames = SidebarAppKitRowCellTests.descendants(of: cell)
.compactMap { $0 as? SidebarRowTextView }
.filter { !$0.isHidden && $0.stringValue.contains("/home/cmux") }
.map(\.frame)
#expect(directoryFrames.isEmpty == compact)
cell.applyRebuiltModel(Self.makeModel(settings: settings, workspaceSnapshot: localSnapshot,
colorSchemeIsDark: dark, isActive: isActive, magnification: magnification))
cell.layoutSubtreeIfNeeded()
#expect(badge.isHidden)
#expect(title.frame.minX < cloudTitleFrame.minX)
#expect(title.frame.maxX == cloudTitleFrame.maxX)
#expect(cell.accessibilityLabel()?.contains("Cloud workspace") == false)
#expect(cell.layoutContent(model: try #require(cell.currentModelForMeasurement), width: width, apply: false) == height)
#expect(SidebarAppKitRowCellTests.descendants(of: cell).compactMap { $0 as? SidebarRowTextView }
.filter { !$0.isHidden && $0.stringValue.contains("/home/cmux") }.map(\.frame) == directoryFrames)
}

/// SwiftUI consumers track Cloud identity through Workspace's existing read facade.
Expand Down Expand Up @@ -173,14 +226,16 @@ struct SidebarCloudWorkspaceBadgeTests {
private static func makeModel(
settings: SidebarTabItemSettingsSnapshot,
workspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot,
colorSchemeIsDark: Bool = true
colorSchemeIsDark: Bool = true,
isActive: Bool = false,
magnification: Int = 100
) -> SidebarWorkspaceRowModel {
return SidebarWorkspaceRowModel(
workspaceId: UUID(),
index: 0,
snapshot: workspaceSnapshot,
settings: settings,
isActive: false,
isActive: isActive,
isMultiSelected: false,
hasUserCustomTitle: false,
canCloseWorkspace: true,
Expand All @@ -197,7 +252,7 @@ struct SidebarCloudWorkspaceBadgeTests {
shortcutHintText: nil,
showsShortcutHints: false,
colorSchemeIsDark: colorSchemeIsDark,
globalFontMagnificationPercent: 100,
globalFontMagnificationPercent: magnification,
isChecklistExpanded: false,
checklistAddFieldActivationToken: 0,
isChecklistPopoverPresented: false,
Expand Down
Loading