Follow up PR 242: refresh browser under-page background on theme updates (#272)

* Address PR 242 follow-ups for titlebar and browser background

* Restore titlebar border per follow-up scope

* Refresh browser under-page color with Ghostty opacity

* Browser: theme blank page fallback for about:blank

* Browser: keep new tabs webview-less until first nav
This commit is contained in:
Lawrence Chen 2026-02-21 05:30:21 -08:00 committed by GitHub
parent 356a20e97a
commit 8ac554fb06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 105 additions and 21 deletions

View file

@ -1005,6 +1005,27 @@ final class BrowserPanel: Panel, ObservableObject {
/// Shared process pool for cookie sharing across all browser panels
private static let sharedProcessPool = WKProcessPool()
private static func clampedGhosttyBackgroundOpacity(_ opacity: Double) -> CGFloat {
CGFloat(max(0.0, min(1.0, opacity)))
}
private static func resolvedGhosttyBackgroundColor(from notification: Notification? = nil) -> NSColor {
let userInfo = notification?.userInfo
let baseColor = (userInfo?[GhosttyNotificationKey.backgroundColor] as? NSColor)
?? GhosttyApp.shared.defaultBackgroundColor
let opacity: Double
if let value = userInfo?[GhosttyNotificationKey.backgroundOpacity] as? Double {
opacity = value
} else if let value = userInfo?[GhosttyNotificationKey.backgroundOpacity] as? NSNumber {
opacity = value.doubleValue
} else {
opacity = GhosttyApp.shared.defaultBackgroundOpacity
}
return baseColor.withAlphaComponent(clampedGhosttyBackgroundOpacity(opacity))
}
let id: UUID
let panelType: PanelType = .browser
@ -1027,6 +1048,10 @@ final class BrowserPanel: Panel, ObservableObject {
/// Published URL being displayed
@Published private(set) var currentURL: URL?
/// Whether the browser panel should render its WKWebView in the content area.
/// New browser tabs stay in an empty "new tab" state until first navigation.
@Published private(set) var shouldRenderWebView: Bool = false
/// Published page title
@Published private(set) var pageTitle: String = ""
@ -1090,7 +1115,7 @@ final class BrowserPanel: Panel, ObservableObject {
if let url = currentURL {
return url.host ?? url.absoluteString
}
return "Browser"
return "New tab"
}
var displayIcon: String? {
@ -1130,7 +1155,7 @@ final class BrowserPanel: Panel, ObservableObject {
// Match the empty-page background to the terminal theme so newly-created browsers
// don't flash white before content loads.
webView.underPageBackgroundColor = GhosttyApp.shared.defaultBackgroundColor
webView.underPageBackgroundColor = Self.resolvedGhosttyBackgroundColor()
// Always present as Safari.
webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent
@ -1206,6 +1231,7 @@ final class BrowserPanel: Panel, ObservableObject {
// Navigate to initial URL if provided
if let url = initialURL {
shouldRenderWebView = true
navigate(to: url)
}
}
@ -1295,6 +1321,13 @@ final class BrowserPanel: Panel, ObservableObject {
}
}
webViewObservers.append(progressObserver)
NotificationCenter.default.publisher(for: .ghosttyDefaultBackgroundDidChange)
.sink { [weak self] notification in
guard let self else { return }
self.webView.underPageBackgroundColor = Self.resolvedGhosttyBackgroundColor(from: notification)
}
.store(in: &cancellables)
}
// MARK: - Panel Protocol
@ -1337,6 +1370,7 @@ final class BrowserPanel: Panel, ObservableObject {
navigationDelegate = nil
uiDelegate = nil
webViewObservers.removeAll()
cancellables.removeAll()
faviconTask?.cancel()
faviconTask = nil
}
@ -1547,6 +1581,7 @@ final class BrowserPanel: Panel, ObservableObject {
guard let url = request.url else { return }
// Some installs can end up with a legacy Chrome UA override; keep this pinned.
webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent
shouldRenderWebView = true
if recordTypedNavigation {
BrowserHistoryStore.shared.recordTypedNavigation(url: url)
}
@ -1649,6 +1684,7 @@ final class BrowserPanel: Panel, ObservableObject {
BrowserWindowPortalRegistry.detach(webView: webView)
}
webViewObservers.removeAll()
cancellables.removeAll()
}
}

View file

@ -548,25 +548,38 @@ struct BrowserPanelView: View {
}
private var webView: some View {
WebViewRepresentable(
panel: panel,
shouldAttachWebView: isVisibleInUI,
shouldFocusWebView: isFocused && !addressBarFocused,
isPanelFocused: isFocused,
portalZPriority: portalPriority
)
// Keep the representable identity stable across bonsplit structural updates.
// This reduces WKWebView reparenting churn (and the associated WebKit crashes).
.id(panel.id)
.contentShape(Rectangle())
.simultaneousGesture(TapGesture().onEnded {
// Chrome-like behavior: clicking web content while editing the
// omnibar should commit blur and revert transient edits.
if addressBarFocused {
addressBarFocused = false
}
})
.zIndex(0)
Group {
if panel.shouldRenderWebView {
WebViewRepresentable(
panel: panel,
shouldAttachWebView: isVisibleInUI,
shouldFocusWebView: isFocused && !addressBarFocused,
isPanelFocused: isFocused,
portalZPriority: portalPriority
)
// Keep the representable identity stable across bonsplit structural updates.
// This reduces WKWebView reparenting churn (and the associated WebKit crashes).
.id(panel.id)
.contentShape(Rectangle())
.simultaneousGesture(TapGesture().onEnded {
// Chrome-like behavior: clicking web content while editing the
// omnibar should commit blur and revert transient edits.
if addressBarFocused {
addressBarFocused = false
}
})
} else {
Color(nsColor: GhosttyApp.shared.defaultBackgroundColor)
.contentShape(Rectangle())
.onTapGesture {
onRequestPanelFocus()
if addressBarFocused {
addressBarFocused = false
}
}
}
}
.zIndex(0)
}
private func triggerFocusFlashAnimation() {

View file

@ -214,6 +214,41 @@ final class BrowserDeveloperToolsConfigurationTests: XCTestCase {
XCTAssertTrue(panel.webView.isInspectable)
}
}
func testBrowserPanelRefreshesUnderPageBackgroundColorWhenGhosttyBackgroundChanges() {
let panel = BrowserPanel(workspaceId: UUID())
let updatedColor = NSColor(srgbRed: 0.18, green: 0.29, blue: 0.44, alpha: 1.0)
let updatedOpacity = 0.57
NotificationCenter.default.post(
name: .ghosttyDefaultBackgroundDidChange,
object: nil,
userInfo: [
GhosttyNotificationKey.backgroundColor: updatedColor,
GhosttyNotificationKey.backgroundOpacity: updatedOpacity
]
)
guard let actual = panel.webView.underPageBackgroundColor?.usingColorSpace(.sRGB),
let expected = updatedColor.withAlphaComponent(updatedOpacity).usingColorSpace(.sRGB) else {
XCTFail("Expected sRGB-convertible under-page background colors")
return
}
XCTAssertEqual(actual.redComponent, expected.redComponent, accuracy: 0.005)
XCTAssertEqual(actual.greenComponent, expected.greenComponent, accuracy: 0.005)
XCTAssertEqual(actual.blueComponent, expected.blueComponent, accuracy: 0.005)
XCTAssertEqual(actual.alphaComponent, expected.alphaComponent, accuracy: 0.005)
}
func testBrowserPanelStartsAsNewTabWithoutLoadingAboutBlank() {
let panel = BrowserPanel(workspaceId: UUID())
XCTAssertEqual(panel.displayTitle, "New tab")
XCTAssertFalse(panel.shouldRenderWebView)
XCTAssertNil(panel.webView.url)
XCTAssertNil(panel.currentURL)
}
}
@MainActor