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 /// Shared process pool for cookie sharing across all browser panels
private static let sharedProcessPool = WKProcessPool() 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 id: UUID
let panelType: PanelType = .browser let panelType: PanelType = .browser
@ -1027,6 +1048,10 @@ final class BrowserPanel: Panel, ObservableObject {
/// Published URL being displayed /// Published URL being displayed
@Published private(set) var currentURL: URL? @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 page title
@Published private(set) var pageTitle: String = "" @Published private(set) var pageTitle: String = ""
@ -1090,7 +1115,7 @@ final class BrowserPanel: Panel, ObservableObject {
if let url = currentURL { if let url = currentURL {
return url.host ?? url.absoluteString return url.host ?? url.absoluteString
} }
return "Browser" return "New tab"
} }
var displayIcon: String? { 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 // Match the empty-page background to the terminal theme so newly-created browsers
// don't flash white before content loads. // don't flash white before content loads.
webView.underPageBackgroundColor = GhosttyApp.shared.defaultBackgroundColor webView.underPageBackgroundColor = Self.resolvedGhosttyBackgroundColor()
// Always present as Safari. // Always present as Safari.
webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent
@ -1206,6 +1231,7 @@ final class BrowserPanel: Panel, ObservableObject {
// Navigate to initial URL if provided // Navigate to initial URL if provided
if let url = initialURL { if let url = initialURL {
shouldRenderWebView = true
navigate(to: url) navigate(to: url)
} }
} }
@ -1295,6 +1321,13 @@ final class BrowserPanel: Panel, ObservableObject {
} }
} }
webViewObservers.append(progressObserver) 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 // MARK: - Panel Protocol
@ -1337,6 +1370,7 @@ final class BrowserPanel: Panel, ObservableObject {
navigationDelegate = nil navigationDelegate = nil
uiDelegate = nil uiDelegate = nil
webViewObservers.removeAll() webViewObservers.removeAll()
cancellables.removeAll()
faviconTask?.cancel() faviconTask?.cancel()
faviconTask = nil faviconTask = nil
} }
@ -1547,6 +1581,7 @@ final class BrowserPanel: Panel, ObservableObject {
guard let url = request.url else { return } guard let url = request.url else { return }
// Some installs can end up with a legacy Chrome UA override; keep this pinned. // Some installs can end up with a legacy Chrome UA override; keep this pinned.
webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent webView.customUserAgent = BrowserUserAgentSettings.safariUserAgent
shouldRenderWebView = true
if recordTypedNavigation { if recordTypedNavigation {
BrowserHistoryStore.shared.recordTypedNavigation(url: url) BrowserHistoryStore.shared.recordTypedNavigation(url: url)
} }
@ -1649,6 +1684,7 @@ final class BrowserPanel: Panel, ObservableObject {
BrowserWindowPortalRegistry.detach(webView: webView) BrowserWindowPortalRegistry.detach(webView: webView)
} }
webViewObservers.removeAll() webViewObservers.removeAll()
cancellables.removeAll()
} }
} }

View file

@ -548,6 +548,8 @@ struct BrowserPanelView: View {
} }
private var webView: some View { private var webView: some View {
Group {
if panel.shouldRenderWebView {
WebViewRepresentable( WebViewRepresentable(
panel: panel, panel: panel,
shouldAttachWebView: isVisibleInUI, shouldAttachWebView: isVisibleInUI,
@ -566,6 +568,17 @@ struct BrowserPanelView: View {
addressBarFocused = false addressBarFocused = false
} }
}) })
} else {
Color(nsColor: GhosttyApp.shared.defaultBackgroundColor)
.contentShape(Rectangle())
.onTapGesture {
onRequestPanelFocus()
if addressBarFocused {
addressBarFocused = false
}
}
}
}
.zIndex(0) .zIndex(0)
} }

View file

@ -214,6 +214,41 @@ final class BrowserDeveloperToolsConfigurationTests: XCTestCase {
XCTAssertTrue(panel.webView.isInspectable) 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 @MainActor