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
13 changes: 2 additions & 11 deletions Sources/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14587,18 +14587,9 @@ private struct SidebarHelpMenuButton: View {
isPopoverPresented.toggle()
} label: {
Image(systemName: "questionmark.circle")
.resizable()
.scaledToFit()
.fontWeight(.medium)
.symbolRenderingMode(.monochrome)
.cmuxSymbolRasterSize(iconSize, weight: .medium)
.foregroundStyle(Color(nsColor: .secondaryLabelColor))
// Drive the symbol's raster size from an explicit frame rather
// than font metrics. During the window's pre-visible layout pass
// the font metrics aren't resolved yet, so a `.font`-sized symbol
// rasterizes at 0×0 — which macOS 26+ rejects with an uncaught
// `NSInvalidArgumentException` (targetSizeInPoints.width/height>0),
// crashing on launch. A fixed frame keeps the raster size positive.
.frame(width: iconSize, height: iconSize, alignment: .center)
.frame(width: buttonSize, height: buttonSize, alignment: .center)
}
.buttonStyle(SidebarFooterIconButtonStyle())
Expand Down Expand Up @@ -17389,7 +17380,7 @@ private struct SidebarMetadataEntryRow: View {
symbolName = iconRaw
}
guard !symbolName.isEmpty else { return nil }
return AnyView(Image(systemName: symbolName).font(.system(size: 8 * fontScale, weight: .medium)))
return AnyView(Image(systemName: symbolName).cmuxSymbolRasterSize(8 * fontScale, weight: .medium))
}

@ViewBuilder
Expand Down
30 changes: 15 additions & 15 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -325,8 +325,8 @@ private struct OmnibarAddressButtonStyleBody: View {
}
}

private extension View {
func cmuxFlatSymbolColorRendering() -> some View {
private extension Image {
func cmuxFlatSymbolColorRendering() -> Image {
// `symbolColorRenderingMode(.flat)` is not available in the current SDK
// used by CI/local builds. Keep this modifier as a compatibility no-op.
self
Expand Down Expand Up @@ -1265,7 +1265,7 @@ struct BrowserPanelView: View {
panel.goBack()
}) {
Image(systemName: "chevron.left")
.font(.system(size: chromeMetrics.navigationIconFontSize, weight: .medium))
.cmuxSymbolRasterSize(chromeMetrics.navigationIconFontSize, weight: .medium)
.frame(width: addressBarButtonHitSize, height: addressBarButtonHitSize, alignment: .center)
.contentShape(Rectangle())
}
Expand All @@ -1281,7 +1281,7 @@ struct BrowserPanelView: View {
panel.goForward()
}) {
Image(systemName: "chevron.right")
.font(.system(size: chromeMetrics.navigationIconFontSize, weight: .medium))
.cmuxSymbolRasterSize(chromeMetrics.navigationIconFontSize, weight: .medium)
.frame(width: addressBarButtonHitSize, height: addressBarButtonHitSize, alignment: .center)
.contentShape(Rectangle())
}
Expand All @@ -1292,7 +1292,7 @@ struct BrowserPanelView: View {

Button(action: handleReloadOrStopButtonAction) {
Image(systemName: panel.isLoading ? "xmark" : "arrow.clockwise")
.font(.system(size: chromeMetrics.navigationIconFontSize, weight: .medium))
.cmuxSymbolRasterSize(chromeMetrics.navigationIconFontSize, weight: .medium)
.frame(width: addressBarButtonHitSize, height: addressBarButtonHitSize, alignment: .center)
.contentShape(Rectangle())
}
Expand All @@ -1318,7 +1318,7 @@ struct BrowserPanelView: View {
Image(systemName: screenshotPageCopied ? "checkmark" : "camera")
.symbolRenderingMode(.monochrome)
.cmuxFlatSymbolColorRendering()
.font(.system(size: devToolsButtonIconSize, weight: .medium))
.cmuxSymbolRasterSize(devToolsButtonIconSize, weight: .medium)
.foregroundStyle(screenshotPageButtonColor)
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
}
Expand Down Expand Up @@ -1372,7 +1372,7 @@ struct BrowserPanelView: View {
Button(action: handleBrowserFocusModeButtonAction) {
HStack(spacing: 5) {
Image(systemName: "keyboard")
.font(.system(size: devToolsButtonIconSize, weight: .medium))
.cmuxSymbolRasterSize(devToolsButtonIconSize, weight: .medium)
.scaleEffect(panel.isBrowserFocusModeActive ? 1.08 : 1.0)
.animation(.spring(response: 0.18, dampingFraction: 0.82), value: panel.isBrowserFocusModeActive)
if panel.isBrowserFocusModeActive {
Expand Down Expand Up @@ -1420,7 +1420,7 @@ struct BrowserPanelView: View {
Image(systemName: "cursorarrow.click.2")
.symbolRenderingMode(.monochrome)
.cmuxFlatSymbolColorRendering()
.font(.system(size: devToolsButtonIconSize, weight: .medium))
.cmuxSymbolRasterSize(devToolsButtonIconSize, weight: .medium)
.foregroundStyle(panel.isReactGrabActive ? Color.accentColor : Color.secondary)
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
}
Expand All @@ -1437,7 +1437,7 @@ struct BrowserPanelView: View {
Image(systemName: devToolsIconOption.rawValue)
.symbolRenderingMode(.monochrome)
.cmuxFlatSymbolColorRendering()
.font(.system(size: devToolsButtonIconSize, weight: .medium))
.cmuxSymbolRasterSize(devToolsButtonIconSize, weight: .medium)
.foregroundStyle(devToolsColorOption.color)
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
}
Expand All @@ -1454,7 +1454,7 @@ struct BrowserPanelView: View {
Image(systemName: "person.crop.circle")
.symbolRenderingMode(.monochrome)
.cmuxFlatSymbolColorRendering()
.font(.system(size: devToolsButtonIconSize, weight: .medium))
.cmuxSymbolRasterSize(devToolsButtonIconSize, weight: .medium)
.foregroundStyle(devToolsColorOption.color)
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
}
Expand Down Expand Up @@ -1482,7 +1482,7 @@ struct BrowserPanelView: View {
Image(systemName: browserThemeMode.iconName)
.symbolRenderingMode(.monochrome)
.cmuxFlatSymbolColorRendering()
.font(.system(size: devToolsButtonIconSize, weight: .medium))
.cmuxSymbolRasterSize(devToolsButtonIconSize, weight: .medium)
.foregroundStyle(browserThemeModeIconColor)
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
}
Expand All @@ -1509,7 +1509,7 @@ struct BrowserPanelView: View {
}) {
HStack(spacing: 4) {
Image(systemName: "square.and.arrow.down.on.square")
.font(.system(size: 10, weight: .medium))
.cmuxSymbolRasterSize(10, weight: .medium)
Text(String(localized: "browser.import.hint.toolbar", defaultValue: "Import"))
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
Expand Down Expand Up @@ -1539,7 +1539,7 @@ struct BrowserPanelView: View {
} label: {
HStack(spacing: 8) {
Image(systemName: profile.id == panel.profileID ? "checkmark" : "circle")
.font(.system(size: 10, weight: .semibold))
.cmuxSymbolRasterSize(10, weight: .semibold)
.opacity(profile.id == panel.profileID ? 1.0 : 0.0)
.frame(width: 12, alignment: .center)
Text(profile.displayName)
Expand Down Expand Up @@ -1602,7 +1602,7 @@ struct BrowserPanelView: View {
} label: {
HStack(spacing: 8) {
Image(systemName: mode == browserThemeMode ? "checkmark" : "circle")
.font(.system(size: 10, weight: .semibold))
.cmuxSymbolRasterSize(10, weight: .semibold)
.opacity(mode == browserThemeMode ? 1.0 : 0.0)
.frame(width: 12, alignment: .center)
Text(mode.displayName)
Expand Down Expand Up @@ -1635,7 +1635,7 @@ struct BrowserPanelView: View {
return HStack(spacing: 4) {
if showSecureBadge {
Image(systemName: "lock.fill")
.font(.system(size: chromeMetrics.secureBadgeFontSize))
.cmuxSymbolRasterSize(chromeMetrics.secureBadgeFontSize)
.foregroundColor(.secondary)
}

Expand Down
24 changes: 24 additions & 0 deletions Sources/RenderableSystemSymbol.swift
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import AppKit
import SwiftUI

enum RenderableSystemSymbol {
static let defaultWorkspaceGroupIcon = "folder.fill"
static let defaultSurfaceTabIcon = "doc.text"
private static let minimumRasterPointSize: CGFloat = 1
@MainActor
private static var renderabilityCache: [String: Bool] = [:]

Expand Down Expand Up @@ -49,10 +51,32 @@ enum RenderableSystemSymbol {
return resolved
}

static func clampedRasterPointSize(_ pointSize: CGFloat) -> CGFloat {
guard pointSize.isFinite else {
return minimumRasterPointSize
}
return max(minimumRasterPointSize, pointSize)
}

#if DEBUG
@MainActor
static func resetRenderabilityCacheForTesting() {
renderabilityCache.removeAll()
}
#endif
}

extension Image {
/// Sizes SF Symbols from an explicit positive frame instead of transient font metrics.
func cmuxSymbolRasterSize(
_ pointSize: CGFloat,
weight: Font.Weight? = nil,
alignment: Alignment = .center
) -> some View {
let rasterSize = RenderableSystemSymbol.clampedRasterPointSize(pointSize)
return resizable()
.scaledToFit()
.fontWeight(weight)
.frame(width: rasterSize, height: rasterSize, alignment: alignment)
}
}
6 changes: 5 additions & 1 deletion Sources/RightSidebarChromeStyle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ enum HeaderChromeIconStyle {
HeaderChromeControlMetrics.iconFrameSize(forIconSize: iconSize)
}

static func symbol(_ systemName: String) -> some View {
Image(systemName: systemName)
.cmuxSymbolRasterSize(RightSidebarChromeMetrics.headerIconSize, weight: weight)
}

static func foregroundOpacity(isHovering: Bool, isPressed: Bool, isEnabled: Bool = true) -> Double {
guard isEnabled else { return disabledOpacity }
if isPressed {
Expand Down Expand Up @@ -164,7 +169,6 @@ private struct RightSidebarHeaderIconButtonStyleBody: View {
var body: some View {
configuration.label
.symbolRenderingMode(.monochrome)
.font(.system(size: RightSidebarChromeMetrics.headerIconSize, weight: HeaderChromeIconStyle.weight))
.frame(
width: RightSidebarChromeMetrics.headerIconFrameSize,
height: RightSidebarChromeMetrics.headerIconFrameSize
Expand Down
4 changes: 2 additions & 2 deletions Sources/RightSidebarPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ struct RightSidebarPanelView: View {
Button {
onOpenAsPane(mode)
} label: {
Image(systemName: "rectangle.split.2x1")
HeaderChromeIconStyle.symbol("rectangle.split.2x1")
}
.buttonStyle(RightSidebarHeaderIconButtonStyle(iconGeometryKeyPrefix: "rightSidebarHeaderOpenAsPaneIcon"))
.frame(
Expand Down Expand Up @@ -355,7 +355,7 @@ struct RightSidebarPanelView: View {
)
return ZStack {
Button(action: onClose) {
Image(systemName: "xmark")
HeaderChromeIconStyle.symbol("xmark")
}
.buttonStyle(RightSidebarHeaderIconButtonStyle(iconGeometryKeyPrefix: "rightSidebarHeaderCloseIcon"))
.frame(
Expand Down
8 changes: 4 additions & 4 deletions Sources/Update/UpdateTitlebarAccessory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1161,7 +1161,7 @@ struct TitlebarControlsView: View {
titlebarIconChrome(config: config, iconGeometryKeyPrefix: iconGeometryKeyPrefix) {
Image(systemName: systemName)
.symbolRenderingMode(.monochrome)
.font(.system(size: config.iconSize, weight: TitlebarControlIconStyle.weight))
.cmuxSymbolRasterSize(config.iconSize, weight: TitlebarControlIconStyle.weight)
}
}

Expand Down Expand Up @@ -2347,7 +2347,7 @@ private struct NotificationsPopoverView: View {
Button(action: jumpToLatestUnread) {
HStack(spacing: 5) {
Image(systemName: "arrow.down.to.line")
.font(.system(size: 10, weight: .semibold))
.cmuxSymbolRasterSize(10, weight: .semibold)
Text(String(localized: "notifications.jumpToLatest", defaultValue: "Jump to Latest"))
.font(.system(size: 11))
if !jumpToUnreadShortcut.displayString.isEmpty {
Expand Down Expand Up @@ -2471,7 +2471,7 @@ private struct NotificationsPopoverView: View {
private func emptyState(systemImage: String, title: String, subtitle: String?) -> some View {
VStack(spacing: 10) {
Image(systemName: systemImage)
.font(.system(size: 30, weight: .light))
.cmuxSymbolRasterSize(30, weight: .light)
.foregroundColor(.secondary.opacity(0.7))
Text(title)
.font(.system(size: 14, weight: .medium))
Expand Down Expand Up @@ -2655,7 +2655,7 @@ private struct NotificationPopoverRow: View {
Circle()
.fill(Color.primary.opacity(0.1))
Image(systemName: "xmark")
.font(.system(size: 9, weight: .bold))
.cmuxSymbolRasterSize(9, weight: .bold)
.foregroundColor(.primary.opacity(0.7))
}
.frame(width: 20, height: 20)
Expand Down
4 changes: 4 additions & 0 deletions cmux.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -509,6 +509,7 @@
B9000027A1B2C3D4E5F60719 /* RemoteRelayZshBootstrap.swift in Sources */ = {isa = PBXBuildFile; fileRef = A5001641 /* RemoteRelayZshBootstrap.swift */; };
E4321000E4321000E4321001 /* RemoteShellSessionParsing.swift in Sources */ = {isa = PBXBuildFile; fileRef = E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */; };
D5037010000000000000003 /* RenderableSystemSymbol.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5037010000000000000002 /* RenderableSystemSymbol.swift */; };
C58410010000000000000001 /* RenderableSystemSymbolTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = C58410010000000000000002 /* RenderableSystemSymbolTests.swift */; };
D36A00040000000000000001 /* RendererRealizationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00040000000000000002 /* RendererRealizationController.swift */; };
D36A00060000000000000001 /* RendererRealizationPlanner.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00060000000000000002 /* RendererRealizationPlanner.swift */; };
D36A00050000000000000001 /* RendererRealizationPlannerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D36A00050000000000000002 /* RendererRealizationPlannerTests.swift */; };
Expand Down Expand Up @@ -1314,6 +1315,7 @@
A5001641 /* RemoteRelayZshBootstrap.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteRelayZshBootstrap.swift; sourceTree = "<group>"; };
E4321000E4321000E4321002 /* RemoteShellSessionParsing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RemoteShellSessionParsing.swift; sourceTree = "<group>"; };
D5037010000000000000002 /* RenderableSystemSymbol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderableSystemSymbol.swift; sourceTree = "<group>"; };
C58410010000000000000002 /* RenderableSystemSymbolTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RenderableSystemSymbolTests.swift; sourceTree = "<group>"; };
D36A00040000000000000002 /* RendererRealizationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationController.swift; sourceTree = "<group>"; };
D36A00060000000000000002 /* RendererRealizationPlanner.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App/RendererRealizationPlanner.swift; sourceTree = "<group>"; };
D36A00050000000000000002 /* RendererRealizationPlannerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RendererRealizationPlannerTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2311,6 +2313,7 @@
A9E040000000000000000001 /* CodexAppServerSessionTests.swift */,
D36A00020000000000000002 /* AgentHibernationTests.swift */,
D36A00050000000000000002 /* RendererRealizationPlannerTests.swift */,
C58410010000000000000002 /* RenderableSystemSymbolTests.swift */,
F5410001A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift */,
F5410005A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift */,
F5410003A1B2C3D4E5F60718 /* RestorableAgentNonInteractiveTests.swift */,
Expand Down Expand Up @@ -3550,6 +3553,7 @@
F4100000A1B2C3D4E5F60718 /* PortScannerTests.swift in Sources */,
C135190000000000000000A1 /* PreferredEditorSettingsTests.swift in Sources */,
C47110010000000000000001 /* ProcessPipeReadCrashRegressionTests.swift in Sources */,
C58410010000000000000001 /* RenderableSystemSymbolTests.swift in Sources */,
D36A00050000000000000001 /* RendererRealizationPlannerTests.swift in Sources */,
F5410004A1B2C3D4E5F60718 /* RestorableAgentHookProviderHermesTests.swift in Sources */,
F5410000A1B2C3D4E5F60718 /* RestorableAgentHookProviderResumeTests.swift in Sources */,
Expand Down
20 changes: 20 additions & 0 deletions cmuxTests/RenderableSystemSymbolTests.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import Foundation
import Testing

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

@Suite("Renderable system symbols")
struct RenderableSystemSymbolTests {
@Test func rasterPointSizeClampsInvalidInputs() {
#expect(RenderableSystemSymbol.clampedRasterPointSize(0) == 1)
#expect(RenderableSystemSymbol.clampedRasterPointSize(-8) == 1)
#expect(RenderableSystemSymbol.clampedRasterPointSize(11) == 11)
#expect(RenderableSystemSymbol.clampedRasterPointSize(.nan) == 1)
#expect(RenderableSystemSymbol.clampedRasterPointSize(.infinity) == 1)
#expect(RenderableSystemSymbol.clampedRasterPointSize(-.infinity) == 1)
}
}
Loading