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
99 changes: 68 additions & 31 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15862,10 +15862,11 @@ struct TabItemView: View, Equatable {
// percent here and applying a primitive `.font(...)` keeps magnification
// working while dropping those per-label modifier bodies.
@Environment(\.cmuxGlobalFontMagnificationPercent) private var globalFontMagnificationPercent
// Window activation and Increase Contrast only: the selection wash dims to
// neutral when the window is inactive, like Finder. Neither changes per
// keystroke.
@Environment(\.controlActiveState) private var controlActiveState
// Increase Contrast strengthens the multi-selection and subtle selection
// washes; it changes only when the accessibility option does. Window
// activation is read by `SidebarSelectionWindowActivationReader`, and
// only for subtle-selection rows that are selected, so focus changes
// don't invalidate every row.
@Environment(\.colorSchemeContrast) private var colorSchemeContrast
#if DEBUG
// Plain-value environment probe (closure struct, not an object reference):
Expand Down Expand Up @@ -16166,10 +16167,6 @@ struct TabItemView: View, Equatable {
#endif
let signpost = SidebarProfilingSignposts.begin("sidebar-tab-item-body", "index=\(index) workspace=\(sidebarShortTabId(workspaceId)) active=\(isActive) unread=\(unreadCount)")
let workspaceSnapshot = self.workspaceSnapshot
let rowBackgroundStyle = backgroundStyle(for: workspaceSnapshot)
let rowBackgroundColor = rowBackgroundStyle.color.map {
Color(nsColor: $0).opacity(rowBackgroundStyle.opacity)
} ?? .clear
let rowRailColor = railColor(for: workspaceSnapshot)
let accessibilityTitle = workspaceSnapshot.accessibilityLabel(index: index, workspaceCount: accessibilityWorkspaceCount)
let closeWorkspaceTooltip = String(localized: "sidebar.closeWorkspace.tooltip", defaultValue: "Close Workspace")
Expand Down Expand Up @@ -16599,27 +16596,7 @@ struct TabItemView: View, Equatable {
// they appear; content changes now apply in one discrete layout pass.
.padding(.horizontal, SidebarWorkspaceListMetrics.rowContentHorizontalPadding)
.padding(.vertical, 8)
.background(
RoundedRectangle(cornerRadius: 6)
.fill(rowBackgroundColor)
.overlay {
RoundedRectangle(cornerRadius: 6)
.strokeBorder(
activeBorderColor(for: rowBackgroundStyle),
lineWidth: activeBorderLineWidth(for: rowBackgroundStyle)
)
}
.overlay(alignment: .leading) {
if showsLeadingRail(for: workspaceSnapshot) {
Capsule(style: .continuous)
.fill(rowRailColor)
.frame(width: 3)
.padding(.leading, 4)
.padding(.vertical, 5)
.offset(x: -1)
}
}
)
.background(rowBackground(for: workspaceSnapshot, railColor: rowRailColor))
.sidebarShortcutHintOverlay(
text: showsWorkspaceShortcutHint ? workspaceShortcutLabel : nil,
emphasis: shortcutHintEmphasis,
Expand Down Expand Up @@ -16719,8 +16696,68 @@ struct TabItemView: View, Equatable {
isEditing = true
}

/// Only a selected row painted with the subtle selection changes with
/// window activation. Legacy and unselected rows never read it.
private var selectionChromeTracksWindowActivation: Bool {
(isActive || isMultiSelected)
&& sidebarUsesSubtleSelection(
activeTabIndicatorStyle: activeTabIndicatorStyle,
subtleSelection: settings.subtleSelection,
sidebarSelectionColorHex: sidebarSelectionColorHex
)
}

@ViewBuilder
private func rowBackground(
for workspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot,
railColor: Color
) -> some View {
if selectionChromeTracksWindowActivation {
SidebarSelectionWindowActivationReader { isEmphasized in
rowBackgroundShape(
style: backgroundStyle(for: workspaceSnapshot, isEmphasized: isEmphasized),
workspaceSnapshot: workspaceSnapshot,
railColor: railColor
)
}
} else {
rowBackgroundShape(
style: backgroundStyle(for: workspaceSnapshot, isEmphasized: true),
workspaceSnapshot: workspaceSnapshot,
railColor: railColor
)
}
}

private func rowBackgroundShape(
style: SidebarWorkspaceRowBackgroundStyle,
workspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot,
railColor: Color
) -> some View {
RoundedRectangle(cornerRadius: 6)
.fill(style.color.map { Color(nsColor: $0).opacity(style.opacity) } ?? .clear)
.overlay {
RoundedRectangle(cornerRadius: 6)
.strokeBorder(
activeBorderColor(for: style),
lineWidth: activeBorderLineWidth(for: style)
)
}
.overlay(alignment: .leading) {
if showsLeadingRail(for: workspaceSnapshot) {
Capsule(style: .continuous)
.fill(railColor)
.frame(width: 3)
.padding(.leading, 4)
.padding(.vertical, 5)
.offset(x: -1)
}
}
}

private func backgroundStyle(
for workspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot
for workspaceSnapshot: SidebarWorkspaceSnapshotBuilder.Snapshot,
isEmphasized: Bool
) -> SidebarWorkspaceRowBackgroundStyle {
sidebarWorkspaceRowBackgroundStyle(
activeTabIndicatorStyle: activeTabIndicatorStyle,
Expand All @@ -16730,7 +16767,7 @@ struct TabItemView: View, Equatable {
colorScheme: colorScheme,
sidebarSelectionColorHex: sidebarSelectionColorHex,
subtleSelection: settings.subtleSelection,
isEmphasized: controlActiveState != .inactive,
isEmphasized: isEmphasized,
increaseContrast: colorSchemeContrast == .increased,
accent: settings.accentColor
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ struct SidebarGroupHeaderRowModel: Equatable, Hashable {
let isAnchorActive: Bool
let isMultiSelected: Bool
let multiSelectionBackgroundStyle: SidebarWorkspaceRowBackgroundStyle
/// Hairline painted while this header is anchor-active; nil when subtle
/// selection is off.
var anchorActiveEdgeColor: NSColor? = nil
let memberCount: Int
var anchorUnreadCount: Int
var canMarkRead: Bool
Expand Down
19 changes: 18 additions & 1 deletion Sources/Sidebar/AppKitList/Cells/SidebarGroupHeaderRowView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import SwiftUI
final class SidebarGroupHeaderTableCellView: NSTableCellView {
static let reuseIdentifier = NSUserInterfaceItemIdentifier("SidebarGroupHeaderTableCellView")

private let backgroundView = NSView()
/// Selection fill and edge layer. Internal so tests read its paint directly.
let backgroundView = NSView()
private let pinImageView = NSImageView()
private let chevronButton = SidebarHeaderGlyphButton()
private let iconImageView = NSImageView()
Expand Down Expand Up @@ -225,6 +226,7 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
? 6
: 4
backgroundView.layer?.backgroundColor = headerBackgroundColor(for: model).cgColor
applySelectionEdge(headerSelectionEdgeColor(for: model))

topDropIndicator.accentColor = model.accentColor
bottomDropIndicator.accentColor = model.accentColor
Expand Down Expand Up @@ -290,6 +292,7 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
CATransaction.setDisableActions(true)
backgroundView.layer?.cornerRadius = 4
backgroundView.layer?.backgroundColor = labelColor.withAlphaComponent(0.08).cgColor
applySelectionEdge(model.anchorActiveEdgeColor)
CATransaction.commit()
nameField.textColor = labelColor
}
Expand All @@ -302,6 +305,7 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
CATransaction.setDisableActions(true)
backgroundView.layer?.cornerRadius = 6
backgroundView.layer?.backgroundColor = headerMultiSelectionBackgroundColor(for: model).cgColor
applySelectionEdge(model.multiSelectionBackgroundStyle.edgeColor)
CATransaction.commit()
}

Expand All @@ -313,6 +317,7 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
CATransaction.setDisableActions(true)
backgroundView.layer?.cornerRadius = 4
backgroundView.layer?.backgroundColor = NSColor.clear.cgColor
applySelectionEdge(nil)
CATransaction.commit()
let colorScheme: ColorScheme = model.colorSchemeIsDark ? .dark : .light
nameField.textColor = SidebarAppearanceColorResolver().resolvedColor(
Expand Down Expand Up @@ -348,6 +353,18 @@ final class SidebarGroupHeaderTableCellView: NSTableCellView {
return .clear
}

/// Subtle-selection hairline, matching selected workspace rows.
private func headerSelectionEdgeColor(for model: SidebarGroupHeaderRowModel) -> NSColor? {
if model.isAnchorActive { return model.anchorActiveEdgeColor }
if model.isMultiSelected { return model.multiSelectionBackgroundStyle.edgeColor }
return nil
}

private func applySelectionEdge(_ edgeColor: NSColor?) {
backgroundView.layer?.borderWidth = edgeColor == nil ? 0 : 1
backgroundView.layer?.borderColor = edgeColor?.cgColor
}

private func headerMultiSelectionBackgroundColor(
for model: SidebarGroupHeaderRowModel
) -> NSColor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,9 +249,11 @@ final class SidebarWorkspaceRowTableCellView: NSTableCellView {
// Only the subtle left-rail treatment depends on window activation and
// Increase Contrast; the default solid fill never changes here.
let settings = painted.settings
guard settings.subtleSelection,
settings.activeTabIndicatorStyle != .solidFill,
settings.selectionColorHex.flatMap({ NSColor(hex: $0) }) == nil else { return }
guard sidebarUsesSubtleSelection(
activeTabIndicatorStyle: settings.activeTabIndicatorStyle,
subtleSelection: settings.subtleSelection,
sidebarSelectionColorHex: settings.selectionColorHex
) else { return }
// Selection-derived foregrounds must resolve from the same window and
// accessibility state as the fill. These notifications are rare, so
// repaint the row from its existing model instead of leaving text
Expand Down
62 changes: 59 additions & 3 deletions Sources/Sidebar/SidebarAppearanceSupport.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,42 @@ func sidebarSelectedWorkspaceForegroundNSColor(
return cmuxReadableForegroundNSColor(on: backgroundColor, opacity: clampedOpacity)
}

/// Whether selected rows paint the subtle tint and hairline. Only the
/// left-rail indicator style uses it, and a configured selection color is an
/// explicit request for a solid fill.
func sidebarUsesSubtleSelection(
activeTabIndicatorStyle: WorkspaceIndicatorStyle,
subtleSelection: Bool,
sidebarSelectionColorHex: String?
) -> Bool {
subtleSelection
&& activeTabIndicatorStyle == .leftRail
&& sidebarSelectionColorHex.flatMap { NSColor(hex: $0) } == nil
}

/// Hairline for a group header whose anchor workspace is selected, so group
/// headers carry the same edge as selected workspace rows in subtle-selection
/// mode. The header keeps its neutral wash, so the edge is the neutral
/// selection edge. Nil when subtle selection is off.
func sidebarGroupHeaderAnchorActiveEdgeNSColor(
activeTabIndicatorStyle: WorkspaceIndicatorStyle,
subtleSelection: Bool,
sidebarSelectionColorHex: String?,
colorScheme: ColorScheme,
increaseContrast: Bool
) -> NSColor? {
guard sidebarUsesSubtleSelection(
activeTabIndicatorStyle: activeTabIndicatorStyle,
subtleSelection: subtleSelection,
sidebarSelectionColorHex: sidebarSelectionColorHex
) else { return nil }
return CmuxSelectionFill.resolve(
colorScheme: colorScheme,
isEmphasized: false,
increaseContrast: increaseContrast
).edgeColor
}

struct SidebarWorkspaceRowBackgroundStyle: Equatable, Hashable {
let color: NSColor?
let opacity: Double
Expand Down Expand Up @@ -402,9 +438,11 @@ func sidebarWorkspaceRowBackgroundStyle(
accent: accent
)
let accentBackground = accent.nsColor(for: colorScheme)
// A configured selection color is an explicit request for a solid fill.
let usesSubtleSelection = subtleSelection
&& sidebarSelectionColorHex.flatMap { NSColor(hex: $0) } == nil
let usesSubtleSelection = sidebarUsesSubtleSelection(
activeTabIndicatorStyle: activeTabIndicatorStyle,
subtleSelection: subtleSelection,
sidebarSelectionColorHex: sidebarSelectionColorHex
)
func calmFill(isSecondary: Bool) -> SidebarWorkspaceRowBackgroundStyle {
let fill = CmuxSelectionFill.resolve(
colorScheme: colorScheme,
Expand Down Expand Up @@ -465,3 +503,21 @@ extension WorkspaceIndicatorStyle {
isActive && (self == .solidFill || increaseContrast)
}
}

/// Reads window activation for the subtle selection wash, which dims to
/// neutral when the window is inactive the way Finder's selection does.
/// Rows wrap only their selection background in this reader, and only while
/// the subtle selection paints them, so activation changes invalidate that
/// background instead of every sidebar row.
struct SidebarSelectionWindowActivationReader<Content: View>: View {
@Environment(\.controlActiveState) private var controlActiveState
let content: (_ isEmphasized: Bool) -> Content

init(@ViewBuilder content: @escaping (_ isEmphasized: Bool) -> Content) {
self.content = content
}

var body: some View {
content(controlActiveState != .inactive)
}
}
23 changes: 22 additions & 1 deletion Sources/SidebarWorkspaceGroupHeaderView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
lhs.isAnchorActive == rhs.isAnchorActive &&
lhs.isMultiSelected == rhs.isMultiSelected &&
lhs.multiSelectionBackgroundStyle == rhs.multiSelectionBackgroundStyle &&
lhs.anchorActiveEdgeColor == rhs.anchorActiveEdgeColor &&
lhs.memberCount == rhs.memberCount &&
lhs.anchorUnreadCount == rhs.anchorUnreadCount &&
lhs.canMarkRead == rhs.canMarkRead &&
Expand Down Expand Up @@ -55,6 +56,9 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
let isAnchorActive: Bool
let isMultiSelected: Bool
let multiSelectionBackgroundStyle: SidebarWorkspaceRowBackgroundStyle
/// Hairline painted while this header is anchor-active; nil when subtle
/// selection is off.
var anchorActiveEdgeColor: NSColor? = nil
let memberCount: Int
let anchorUnreadCount: Int
let canMarkRead: Bool
Expand Down Expand Up @@ -127,6 +131,17 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
return Color(nsColor: color).opacity(multiSelectionBackgroundStyle.opacity)
}

/// Subtle-selection hairline, matching selected workspace rows.
private var selectionEdgeColor: NSColor? {
if isAnchorActive { return anchorActiveEdgeColor }
if isMultiSelected { return multiSelectionBackgroundStyle.edgeColor }
return nil
}

private var selectionCornerRadius: CGFloat {
isMultiSelected && !isAnchorActive ? 6 : 4
}

var body: some View {
#if DEBUG
let _ = { sidebarLazyContractProbe.groupHeaderRowBody?() }()
Expand Down Expand Up @@ -280,9 +295,15 @@ struct SidebarWorkspaceGroupHeaderView: View, Equatable {
: Color.clear
)
.clipShape(RoundedRectangle(
cornerRadius: isMultiSelected && !isAnchorActive ? 6 : 4,
cornerRadius: selectionCornerRadius,
style: .continuous
))
.overlay {
if let selectionEdgeColor {
RoundedRectangle(cornerRadius: selectionCornerRadius, style: .continuous)
.strokeBorder(Color(nsColor: selectionEdgeColor), lineWidth: 1)
}
}
.sidebarShortcutHintOverlay(
text: shortcutHintPillText,
emphasis: isAnchorActive ? 1.0 : 0.9,
Expand Down
3 changes: 3 additions & 0 deletions Sources/SidebarWorkspaceGroupRowSnapshot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ struct SidebarWorkspaceGroupRowSnapshot {
let isAnchorActive: Bool
let isMultiSelected: Bool
let multiSelectionBackgroundStyle: SidebarWorkspaceRowBackgroundStyle
/// Hairline painted while this header is anchor-active; nil when subtle
/// selection is off.
var anchorActiveEdgeColor: NSColor? = nil
let memberCount: Int
let anchorUnreadCount: Int
let canMarkRead: Bool
Expand Down
Loading