Skip to content
Closed
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
1 change: 0 additions & 1 deletion Sources/Panels/FilePreviewPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,6 @@ private struct FileExternalOpenHeaderMenuButton: View {
PanelHeaderIconGlyph(systemName: "square.and.arrow.up")
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
.disabled(isDisabled)
.help(helpText)
.accessibilityLabel(helpText)
Expand Down
1 change: 0 additions & 1 deletion Sources/Panels/MarkdownTypographyControl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ struct MarkdownTypographyControl: View {
PanelHeaderIconGlyph(systemName: "textformat.size")
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
.help(buttonLabel)
.accessibilityLabel(buttonLabel)
.popover(isPresented: $isPresented, arrowEdge: .bottom) {
Expand Down
70 changes: 63 additions & 7 deletions Sources/Panels/PanelContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -382,9 +382,12 @@ struct PanelFilePathHeader<TrailingContent: View>: View {

var body: some View {
HStack(spacing: 8) {
CmuxSystemSymbolImage(systemName: iconSystemName, pointSize: 16)
.foregroundStyle(.secondary)
.frame(width: 16)
CmuxResolvedIconImage(request: CmuxResolvedIconRequest(
source: .systemSymbol(name: iconSystemName, accessibilityDescription: nil),
size: NSSize(width: 16, height: 16),
tintColor: PanelHeaderIconGlyph.tint(headerForeground: foregroundColor)
))
.frame(width: 16, height: 16)
Text(filePath)
.cmuxFont(size: 11, design: .monospaced)
.foregroundStyle(Color(nsColor: foregroundColor).opacity(0.68))
Expand All @@ -397,6 +400,7 @@ struct PanelFilePathHeader<TrailingContent: View>: View {
.padding(.horizontal, 12)
.frame(height: 30)
.background(Color.clear)
.environment(\.panelHeaderIconTint, PanelHeaderIconGlyph.tint(headerForeground: foregroundColor))
}
}

Expand All @@ -411,19 +415,71 @@ struct PanelHeaderIconButton: View {
PanelHeaderIconGlyph(systemName: systemName)
}
.buttonStyle(.plain)
.foregroundColor(.secondary)
.disabled(isDisabled)
.help(label)
.accessibilityLabel(label)
}
}

/// Panel-header glyph drawn through the appearance-resolved AppKit renderer.
///
/// The SwiftUI symbol path (`Image(systemName:)` and the shared template
/// `NSImage` behind `CmuxSystemSymbolImage`) rasterizes fully transparent in
/// this header on macOS 15 while the button keeps its frame and hit area, so
/// the controls stay clickable but invisible (#8558, #8352, #7725, #4476).
/// `CmuxResolvedIconRenderer` draws into an explicit bitmap context under the
/// resolved appearance, verifies the output has visible pixels, and re-renders
/// when the window or effective appearance changes.
struct PanelHeaderIconGlyph: View {
let systemName: String
@Environment(\.panelHeaderIconTint) private var headerTint
@Environment(\.isEnabled) private var isEnabled

var body: some View {
CmuxSystemSymbolImage(systemName: systemName, pointSize: 13)
.frame(width: 20, height: 20, alignment: .center)
.contentShape(Rectangle())
CmuxResolvedIconImage(
request: Self.request(systemName: systemName, tint: headerTint, isEnabled: isEnabled)
)
.frame(width: 13, height: 13)
.frame(width: 20, height: 20, alignment: .center)
.contentShape(Rectangle())
}

/// Builds one header glyph's render request.
///
/// The tint is always an explicit color rather than a semantic style: the
/// AppKit-backed icon resolves the window appearance, not the panel's
/// SwiftUI `colorScheme` override, so a hierarchical style would not track
/// the panel theme. Disabled glyphs keep the color and drop alpha.
@MainActor
static func request(systemName: String, tint: NSColor?, isEnabled: Bool) -> CmuxResolvedIconRequest {
let baseTint = tint ?? .secondaryLabelColor
let resolvedTint = isEnabled
? baseTint
: baseTint.withAlphaComponent(baseTint.alphaComponent * 0.45)
return CmuxResolvedIconRequest(
source: .systemSymbol(name: systemName, accessibilityDescription: nil),
size: NSSize(width: 13, height: 13),
tintColor: resolvedTint
)
}

/// Secondary-emphasis tint derived from the header's theme foreground,
/// matching the emphasis the previous `.secondary` template tint produced.
static func tint(headerForeground: NSColor) -> NSColor {
headerForeground.withAlphaComponent(headerForeground.alphaComponent * 0.55)
}
}

/// Explicit glyph tint published by ``PanelFilePathHeader`` so header controls
/// follow the panel's theme foreground. `nil` falls back to the
/// appearance-resolved secondary label color.
private struct PanelHeaderIconTintKey: EnvironmentKey {
static let defaultValue: NSColor? = nil
}

extension EnvironmentValues {
var panelHeaderIconTint: NSColor? {
get { self[PanelHeaderIconTintKey.self] }
set { self[PanelHeaderIconTintKey.self] = newValue }
}
}
4 changes: 4 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1546,6 +1546,7 @@ C0DE71B10000000000000001 /* AppDelegate+AgentChatNotifications.swift in Sources
D0B1001CA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = D0B1001DA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift */; };
A5001400 /* Panel.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001410 /* Panel.swift */; };
A5001405 /* PanelContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001415 /* PanelContentView.swift */; };
C58558010000000000000001 /* PanelHeaderIconGlyphTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58558010000000000000002 /* PanelHeaderIconGlyphTests.swift */; };
986900050000000000000001 /* PanelHost.swift in Sources */ = {isa = PBXBuildFile; fileRef = 986900050000000000000002 /* PanelHost.swift */; };
BB49DF25341706C2A892C27C /* PanelOwnedNativeViewSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = CE314D9700F6F1C8D4A2FE11 /* PanelOwnedNativeViewSession.swift */; };
C44550000000000000000001 /* PanelOwnedNativeViewSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C44550000000000000000002 /* PanelOwnedNativeViewSessionTests.swift */; };
Expand Down Expand Up @@ -4326,6 +4327,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
D0B1001DA1B2C3D4E5F60001 /* PaneDropRoutingSupport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaneDropRoutingSupport.swift; sourceTree = "<group>"; };
A5001410 /* Panel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/Panel.swift; sourceTree = "<group>"; };
A5001415 /* PanelContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/PanelContentView.swift; sourceTree = "<group>"; };
C58558010000000000000002 /* PanelHeaderIconGlyphTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelHeaderIconGlyphTests.swift; sourceTree = "<group>"; };
986900050000000000000002 /* PanelHost.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelHost.swift; sourceTree = "<group>"; };
CE314D9700F6F1C8D4A2FE11 /* PanelOwnedNativeViewSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Panels/PanelOwnedNativeViewSession.swift; sourceTree = "<group>"; };
C44550000000000000000002 /* PanelOwnedNativeViewSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PanelOwnedNativeViewSessionTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -7865,6 +7867,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
D36A00020000000000000002 /* AgentHibernationTests.swift */,
D36A00050000000000000002 /* RendererRealizationPlannerTests.swift */,
C58410010000000000000002 /* RenderableSystemSymbolTests.swift */,
C58558010000000000000002 /* PanelHeaderIconGlyphTests.swift */,
DCDC0000000000000000B002 /* DockControlDefinitionDecodingTests.swift */,
859100000000000000000002 /* TerminalLinkOpenCoordinatorTests.swift */,
D7529002A1B2C3D4E5F60718 /* DockPaneDropUnfocusedRoutingTests.swift */,
Expand Down Expand Up @@ -11350,6 +11353,7 @@ C0DE71B10000000000000002 /* AppDelegate+AgentChatNotifications.swift */ = {isa =
9637C6D3170F4BE1A7EF6243 /* OmnibarSubmitDecisionTests.swift in Sources */,
0A0F00550000000000000001 /* OmpSupportTests.swift in Sources */,
A5E01203A1B2C3D4E5F60718 /* OpenCodeHookRegressionTests.swift in Sources */,
C58558010000000000000001 /* PanelHeaderIconGlyphTests.swift in Sources */,
C44550000000000000000001 /* PanelOwnedNativeViewSessionTests.swift in Sources */,
6313FACE0000000000000000 /* PaneMemoryGuardrailTests.swift in Sources */,
B88350000000000000000003 /* PasteboardThreadSignalingDataProvider.swift in Sources */,
Expand Down
100 changes: 100 additions & 0 deletions cmuxTests/PanelHeaderIconGlyphTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import AppKit
import CmuxAppKitSupportUI
import Testing

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

@MainActor
@Suite("Panel header icon glyphs")
struct PanelHeaderIconGlyphTests {
/// Every symbol the markdown viewer and file preview headers render.
private static let headerSymbols = [
"doc.richtext",
"textformat.size",
"arrow.clockwise",
"arrow.counterclockwise",
"square.and.arrow.down",
"square.and.arrow.up",
"doc.plaintext",
"doc.on.doc",
"chevron.left.forwardslash.chevron.right",
"eye",
]

@Test func requestCarriesAnExplicitTintAtGlyphSize() {
let tint = NSColor.systemTeal
let request = PanelHeaderIconGlyph.request(systemName: "eye", tint: tint, isEnabled: true)

#expect(request.size == NSSize(width: 13, height: 13))
#expect(request.tintColor == tint)
guard case .systemSymbol(let name, _) = request.source else {
Issue.record("expected a system symbol source")
return
}
#expect(name == "eye")
}

@Test func requestFallsBackToSecondaryLabelWhenHeaderTintIsMissing() {
let request = PanelHeaderIconGlyph.request(systemName: "eye", tint: nil, isEnabled: true)

#expect(request.tintColor == NSColor.secondaryLabelColor)
}

@Test func disabledRequestKeepsTheColorAndDropsAlpha() throws {
let tint = NSColor.white.withAlphaComponent(0.8)
let request = PanelHeaderIconGlyph.request(systemName: "eye", tint: tint, isEnabled: false)
let resolved = try #require(request.tintColor)

#expect(abs(resolved.alphaComponent - 0.8 * 0.45) < 0.0001)
}

@Test func themeTintKeepsSecondaryEmphasis() {
let foreground = NSColor(calibratedWhite: 0.9, alpha: 1)
let tint = PanelHeaderIconGlyph.tint(headerForeground: foreground)

#expect(abs(tint.alphaComponent - 0.55) < 0.0001)
}

/// Regression guard for #8558: the SwiftUI symbol path rasterized these
/// glyphs fully transparent while their buttons stayed clickable. The
/// resolved renderer must produce visible pixels in either appearance.
@Test(arguments: headerSymbols)
func headerSymbolsRenderVisiblePixels(symbol: String) throws {
let renderer = CmuxResolvedIconRenderer()
let request = PanelHeaderIconGlyph.request(
systemName: symbol,
tint: PanelHeaderIconGlyph.tint(headerForeground: .white),
isEnabled: true
)

for appearanceName in [NSAppearance.Name.aqua, .darkAqua] {
let appearance = try #require(NSAppearance(named: appearanceName))
switch renderer.render(for: request, appearance: appearance) {
case .success(let image):
#expect(visiblePixelCount(in: image) > 0)
case .failure(let failure):
Issue.record("\(symbol) failed to render in \(appearanceName.rawValue): \(failure)")
}
}
}

private func visiblePixelCount(in image: NSImage) -> Int {
guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
return 0
}
let representation = NSBitmapImageRep(cgImage: cgImage)
var count = 0
for y in 0..<representation.pixelsHigh {
for x in 0..<representation.pixelsWide {
if let color = representation.colorAt(x: x, y: y), color.alphaComponent > 0.01 {
count += 1
}
}
}
return count
}
}