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
5 changes: 4 additions & 1 deletion Sources/CodeCaps/ConsoleViews.swift
Original file line number Diff line number Diff line change
Expand Up @@ -443,7 +443,10 @@ private struct ConsoleSidebarSplitter: View {
)
}
.frame(width: 8)
.background(isHovering ? Color.accentColor.opacity(0.15) : Color.clear)
// `Color.accentColor` is the macOS system accent, not the one the
// owner picked, so the grabber glowed system blue next to an accent
// that was doing everything else in the sidebar.
.background(isHovering ? Theme.accent.opacity(0.15) : Color.clear)
}
}

Expand Down
13 changes: 11 additions & 2 deletions Sources/CodeCaps/QuotaComponents.swift
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,17 @@ enum Theme {
: dyn(NSColor.black.withAlphaComponent(0.08), NSColor.white.withAlphaComponent(0.14))

/// Fill behind a selected or highlighted row.
static let selection = dyn(hex(0x087370).withAlphaComponent(0.12),
hex(0x4FD1C5).withAlphaComponent(0.18))
///
/// Derived from the owner's accent rather than pinned to the teal the app
/// shipped with: this is the fill *behind* the row's accent border, so a
/// hardcoded value left a violet border sitting on a teal wash the moment
/// any other accent was picked. Same alphas as before, so the fill keeps
/// the contrast it had against the surface.
static var selection: Color {
let choice = AccentChoice.current
return dyn(hex(choice.lightHex).withAlphaComponent(0.12),
hex(choice.darkHex).withAlphaComponent(0.18))
}

/// The outline of a small header control: the From Mac / From Fleet switch.
static let controlBorder: Color = highContrast
Expand Down
53 changes: 53 additions & 0 deletions Tests/CodeCapsTests/ThemePreferenceTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import AppKit
import SwiftUI
import XCTest
@testable import CodeCaps

Expand Down Expand Up @@ -90,4 +91,56 @@ final class ThemePreferenceTests: XCTestCase {
AccentChoice.current = .teal
XCTAssertNotNil(Theme.barRemaining)
}

/// `Theme.selection` is the fill behind a selected sidebar row and behind
/// the selected segment of the Glance header toggle. It was declared with
/// the teal hexes inlined, so it stayed teal under every accent while the
/// row's border and label moved — a violet border around a teal wash.
func testSelectionFollowsTheChosenAccentInBothAppearances() {
func components(_ color: Color, _ appearance: NSAppearance.Name) -> (r: Double, g: Double, b: Double, a: Double) {
let saved = NSAppearance.current
NSAppearance.current = NSAppearance(named: appearance)!
defer { NSAppearance.current = saved }
// `Theme`'s colours are dynamic `NSColor`s, so they have to be
// resolved against an appearance and moved into sRGB before their
// components can be read at all.
guard let ns = NSColor(color).usingColorSpace(.sRGB) else {
XCTFail("selection did not resolve to an RGB colour under \(appearance.rawValue)")
return (0, 0, 0, 0)
}
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
ns.getRed(&r, green: &g, blue: &b, alpha: &a)
return (Double(r), Double(g), Double(b), Double(a))
}
func expected(_ hex: UInt32, alpha: Double) -> (Double, Double, Double, Double) {
(Double((hex >> 16) & 0xFF) / 255,
Double((hex >> 8) & 0xFF) / 255,
Double(hex & 0xFF) / 255,
alpha)
}
func close(_ a: (r: Double, g: Double, b: Double, a: Double),
_ b: (r: Double, g: Double, b: Double, a: Double),
_ label: String) {
XCTAssertEqual(a.r, b.r, accuracy: 0.01, label)
XCTAssertEqual(a.g, b.g, accuracy: 0.01, label)
XCTAssertEqual(a.b, b.b, accuracy: 0.01, label)
XCTAssertEqual(a.a, b.a, accuracy: 0.01, label)
}

// Teal is unchanged, so the default app looks exactly as it shipped.
AccentChoice.current = .teal
close(components(Theme.selection, .aqua), expected(AccentChoice.teal.lightHex, alpha: 0.12), "teal light")
close(components(Theme.selection, .darkAqua), expected(AccentChoice.teal.darkHex, alpha: 0.18), "teal dark")

// Any other accent moves both the hue and the alpha is kept per appearance.
for accent in AccentChoice.allCases where accent != .teal {
AccentChoice.current = accent
close(components(Theme.selection, .aqua), expected(accent.lightHex, alpha: 0.12),
"\(accent.title) light must tint the selection, not stay teal")
close(components(Theme.selection, .darkAqua), expected(accent.darkHex, alpha: 0.18),
"\(accent.title) dark must tint the selection, not stay teal")
}

AccentChoice.current = .teal
}
}
Loading