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
69 changes: 40 additions & 29 deletions Sources/Panels/BrowserPanelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -187,14 +187,14 @@ func resolvedBrowserChromeBackgroundColor(
}

func resolvedBrowserChromeColorScheme(
for colorScheme: ColorScheme,
themeBackgroundColor: NSColor,
windowBackgroundColor: NSColor
for surfaceColorScheme: ColorScheme,
ambientColorScheme: ColorScheme? = nil
) -> ColorScheme {
let perceivedBackgroundColor = themeBackgroundColor.alphaComponent < 0.999
? cmuxCompositedNSColor(themeBackgroundColor, over: windowBackgroundColor)
: themeBackgroundColor
return cmuxReadableColorScheme(for: perceivedBackgroundColor)
// Browser chrome is composed inside the already-resolved cmux surface.
// The inherited scheme can change when SwiftUI/AppKit hosting or focus
// changes, but it must not replace the surface authority.
_ = ambientColorScheme
return surfaceColorScheme
}

func resolvedBrowserOmnibarPillBackgroundColor(
Expand All @@ -217,7 +217,6 @@ func resolvedBrowserOmnibarPillBackgroundColor(

private struct BrowserChromeStyle {
let backgroundColor: NSColor
let colorScheme: ColorScheme
let omnibarPillBackgroundColor: NSColor

static func resolve(
Expand All @@ -230,18 +229,13 @@ private struct BrowserChromeStyle {
themeBackgroundColor: themeBackgroundColor,
drawsBackground: drawsBackground
)
// The browser is hosted inside the window/Dock chrome. Its semantic
// colors must follow the resolved cmux scheme injected by the parent,
// even when a translucent theme would otherwise be composited against
// AppKit's ambient window appearance.
let chromeColorScheme = colorScheme
let chromeColorScheme = resolvedBrowserChromeColorScheme(for: colorScheme)
let omnibarPillBackgroundColor = resolvedBrowserOmnibarPillBackgroundColor(
for: chromeColorScheme,
themeBackgroundColor: themeBackgroundColor
)
return BrowserChromeStyle(
backgroundColor: backgroundColor,
colorScheme: chromeColorScheme,
omnibarPillBackgroundColor: omnibarPillBackgroundColor
)
}
Expand All @@ -268,8 +262,12 @@ struct BrowserPanelView: View {
/// panels in `DockSplitStore`). When set, it overrides the workspace lookup
/// in `isCurrentPaneOwner`; `nil` preserves the main-area behavior.
let paneOwnershipOverride: Bool?
private let resolvedColorScheme: ColorScheme
private let resolvedThemeBackgroundColor: NSColor
@Environment(\.colorScheme) private var colorScheme
/// Appearance captured from the host before the parent injects the
/// surface-resolved scheme. It is observed only to refresh WebKit's system
/// theme and is never used to resolve browser toolbar colors.
private let inheritedColorScheme: ColorScheme
@Environment(\.cmuxCanvasInlineBrowserHosting) private var canvasInlineBrowserHosting
@Environment(\.paneDropZone) private var paneDropZone
/// Held detector instance used to summarize installed browsers rather than
Expand Down Expand Up @@ -364,6 +362,7 @@ struct BrowserPanelView: View {
portalPriority: Int,
paneOwnershipOverride: Bool? = nil,
resolvedColorScheme: ColorScheme,
inheritedColorScheme: ColorScheme,
resolvedThemeBackgroundColor: NSColor,
onRequestPanelFocus: @escaping () -> Void
) {
Expand All @@ -374,6 +373,8 @@ struct BrowserPanelView: View {
self.isVisibleInUI = isVisibleInUI
self.portalPriority = portalPriority
self.paneOwnershipOverride = paneOwnershipOverride
self.resolvedColorScheme = resolvedColorScheme
self.inheritedColorScheme = inheritedColorScheme
self.resolvedThemeBackgroundColor = resolvedThemeBackgroundColor
self.onRequestPanelFocus = onRequestPanelFocus
self._browserChromeStyle = State(initialValue: BrowserChromeStyle.resolve(
Expand Down Expand Up @@ -459,10 +460,6 @@ struct BrowserPanelView: View {
browserChromeStyle.backgroundColor
}

private var browserChromeColorScheme: ColorScheme {
browserChromeStyle.colorScheme
}

private var browserContentAccessibilityIdentifier: String {
"BrowserPanelContent.\(panel.id.uuidString)"
}
Expand Down Expand Up @@ -508,7 +505,7 @@ struct BrowserPanelView: View {
return BrowserPortalOmnibarSuggestionsConfiguration(
panelId: panel.id,
popupFrame: frame,
colorScheme: browserChromeColorScheme,
colorScheme: resolvedColorScheme,
engineName: searchConfiguration.displayName,
items: omnibarState.suggestions,
selectedIndex: omnibarState.selectedSuggestionIndex,
Expand Down Expand Up @@ -831,7 +828,11 @@ struct BrowserPanelView: View {
panel.setBrowserThemeMode(normalizedMode)
}

private func handleSystemColorSchemeChange() {
private func handleInheritedColorSchemeChange() {
panel.refreshAppearanceDrivenColors()
}

private func handleResolvedColorSchemeChange() {
refreshBrowserChromeStyle()
panel.refreshAppearanceDrivenColors()
}
Expand Down Expand Up @@ -1003,7 +1004,7 @@ struct BrowserPanelView: View {
.frame(width: omnibarPillFrame.width)
.offset(x: omnibarPillFrame.minX, y: omnibarPillFrame.maxY + 3)
.zIndex(1000)
.environment(\.colorScheme, browserChromeColorScheme)
.environment(\.colorScheme, resolvedColorScheme)
}
}

Expand Down Expand Up @@ -1084,8 +1085,11 @@ struct BrowserPanelView: View {
.onChange(of: browserThemeModeRaw) { _ in
handleBrowserThemeModeRawChange()
}
.onChange(of: colorScheme) { _ in
handleSystemColorSchemeChange()
.onChange(of: inheritedColorScheme) { _ in
handleInheritedColorSchemeChange()
}
.onChange(of: resolvedColorScheme) { _ in
handleResolvedColorSchemeChange()
}
.onChange(of: resolvedThemeBackgroundIdentity) { _, _ in
refreshBrowserChromeStyle()
Expand Down Expand Up @@ -1130,6 +1134,10 @@ struct BrowserPanelView: View {
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)) { _ in
refreshBrowserChromeStyle()
}
// Keep every SwiftUI browser control on the resolved cmux surface
// scheme, even when this view is rehosted under an ambient AppKit
// appearance during a focus transition.
.environment(\.colorScheme, resolvedColorScheme)
}

private var addressBar: some View {
Expand Down Expand Up @@ -1196,7 +1204,7 @@ struct BrowserPanelView: View {
}
// Keep the omnibar stack above WKWebView so the suggestions popup is visible.
.zIndex(1)
.environment(\.colorScheme, browserChromeColorScheme)
.environment(\.colorScheme, resolvedColorScheme)
}

private var addressBarButtonBar: some View {
Expand Down Expand Up @@ -1412,7 +1420,7 @@ struct BrowserPanelView: View {
.buttonStyle(OmnibarAddressButtonStyle())
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
.popover(isPresented: $isBrowserProfileMenuPresented, arrowEdge: .bottom) {
browserProfilePopover.browserChromePopoverAppearance(browserChromeColorScheme)
browserProfilePopover.browserChromePopoverAppearance(resolvedColorScheme)
}
.safeHelp(
String(
Expand Down Expand Up @@ -1486,7 +1494,7 @@ struct BrowserPanelView: View {
.buttonStyle(OmnibarAddressButtonStyle())
.frame(width: addressBarButtonSize, height: addressBarButtonSize, alignment: .center)
.popover(isPresented: $isBrowserThemeMenuPresented, arrowEdge: .bottom) {
browserThemeModePopover.browserChromePopoverAppearance(browserChromeColorScheme)
browserThemeModePopover.browserChromePopoverAppearance(resolvedColorScheme)
}
.safeHelp(
String(
Expand Down Expand Up @@ -1516,7 +1524,7 @@ struct BrowserPanelView: View {
}
.buttonStyle(OmnibarAddressButtonStyle())
.popover(isPresented: $isBrowserImportHintPopoverPresented, arrowEdge: .bottom) {
browserImportHintPopover.browserChromePopoverAppearance(browserChromeColorScheme)
browserImportHintPopover.browserChromePopoverAppearance(resolvedColorScheme)
}
.safeHelp(String(localized: "browser.import.hint.toolbar.help", defaultValue: "Import browser data"))
.accessibilityIdentifier("BrowserImportHintToolbarChip")
Expand Down Expand Up @@ -1855,7 +1863,10 @@ struct BrowserPanelView: View {

private func refreshBrowserChromeStyle() {
browserChromeStyle = BrowserChromeStyle.resolve(
for: colorScheme,
for: resolvedBrowserChromeColorScheme(
for: resolvedColorScheme,
ambientColorScheme: inheritedColorScheme
),
themeBackgroundColor: resolvedThemeBackgroundColor,
drawsBackground: panel.drawsConfiguredWebViewBackgroundForCurrentPage()
)
Expand Down
5 changes: 5 additions & 0 deletions Sources/Panels/PanelContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ struct PanelContentView: View {
let customSidebarUnread: SidebarUnreadModel = TerminalNotificationStore.shared.sidebarUnread
let hasUnreadNotification: Bool
let terminalAgentContext: String
/// Appearance inherited from the host before this view injects the
/// surface-resolved scheme for its rendered panel subtree. Browser WebKit
/// system theming may observe this value; browser chrome must not.
@Environment(\.colorScheme) private var inheritedColorScheme
/// Explicit browser pane-ownership signal for hosts whose panels live outside
/// the main `Workspace` tree (the Dock). `nil` keeps the main-area behavior.
var paneOwnershipOverride: Bool? = nil
Expand Down Expand Up @@ -77,6 +81,7 @@ struct PanelContentView: View {
portalPriority: portalPriority,
paneOwnershipOverride: paneOwnershipOverride,
resolvedColorScheme: windowAppearance.resolvedColorScheme,
inheritedColorScheme: inheritedColorScheme,
resolvedThemeBackgroundColor: windowAppearance.resolvedChromeBackgroundColor,
onRequestPanelFocus: onRequestPanelFocus
)
Expand Down
75 changes: 75 additions & 0 deletions cmuxTests/BrowserChromeMetricsTests.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import CoreGraphics
import AppKit
import SwiftUI
import Testing

#if canImport(cmux_DEV)
Expand Down Expand Up @@ -81,4 +83,77 @@ import Testing
#expect(metrics.scale == 1)
#expect(metrics.buttonIconSize == Self.legacy.buttonIconSize)
}

@Test
func browserToolbarAndDockTextUseSurfaceAuthorityAcrossAppearanceAndFocus() {
let cases: [(surface: ColorScheme, app: ColorScheme)] = [
(.light, .light),
(.light, .dark),
(.dark, .light),
(.dark, .dark),
]

for testCase in cases {
let unfocusedScheme = resolvedBrowserChromeColorScheme(
for: testCase.surface,
ambientColorScheme: testCase.app
)
// A focus/hosting transition can expose the opposite inherited
// scheme even though the browser surface did not change.
let focusedAmbient = testCase.app == .dark ? ColorScheme.light : .dark
let focusedScheme = resolvedBrowserChromeColorScheme(
for: testCase.surface,
ambientColorScheme: focusedAmbient
)

#expect(
unfocusedScheme == testCase.surface,
"Browser toolbar authority changed while unfocused for surface=\(testCase.surface) app=\(testCase.app)"
)
#expect(
focusedScheme == testCase.surface,
"Browser toolbar authority changed while focused for surface=\(testCase.surface) app=\(testCase.app)"
)
#expect(
focusedScheme == unfocusedScheme,
"Browser toolbar scheme flipped across focus for surface=\(testCase.surface) app=\(testCase.app)"
)

let dockText = SidebarAppearanceColorResolver().activeForegroundColor(
opacity: 1,
for: testCase.surface
)
let expectedTextIsWhite = testCase.surface == .dark
let resolvedDockText = dockText.usingColorSpace(.sRGB) ?? dockText
var red: CGFloat = 0
var green: CGFloat = 0
var blue: CGFloat = 0
var alpha: CGFloat = 0
resolvedDockText.getRed(&red, green: &green, blue: &blue, alpha: &alpha)

#expect((red > 0.99) == expectedTextIsWhite)
#expect((green > 0.99) == expectedTextIsWhite)
#expect((blue > 0.99) == expectedTextIsWhite)
#expect(alpha == 1)
}
}

@Test
func inheritedAppearanceOnlyChangeKeepsBrowserChromeStableForWebKitRefresh() {
let surfaceScheme: ColorScheme = .light
let beforeRefresh = resolvedBrowserChromeColorScheme(
for: surfaceScheme,
ambientColorScheme: .dark
)
let afterRefresh = resolvedBrowserChromeColorScheme(
for: surfaceScheme,
ambientColorScheme: .light
)

// `BrowserPanelView` refreshes WebKit when this inherited value
// changes, while the toolbar authority must remain unchanged.
#expect(beforeRefresh == surfaceScheme)
#expect(afterRefresh == surfaceScheme)
#expect(afterRefresh == beforeRefresh)
}
}
9 changes: 3 additions & 6 deletions cmuxTests/BrowserPanelTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -455,16 +455,13 @@ final class BrowserPanelChromeBackgroundColorTests: XCTestCase {
XCTAssertEqual(themeBackground.alphaComponent, 1.0, accuracy: 0.001)
}

func testBrowserChromeColorSchemeAccountsForTranslucentBackground() {
let darkTranslucentBackground = NSColor(srgbRed: 0.02, green: 0.03, blue: 0.04, alpha: 0.05)

func testBrowserChromeColorSchemeUsesExplicitSurfaceAuthority() {
XCTAssertEqual(
resolvedBrowserChromeColorScheme(
for: .dark,
themeBackgroundColor: darkTranslucentBackground,
windowBackgroundColor: .white
ambientColorScheme: .light
),
.light
.dark
)
}

Expand Down
Loading