diff --git a/Sources/Panels/BrowserPanel.swift b/Sources/Panels/BrowserPanel.swift index 4db10c98..dedcdb85 100644 --- a/Sources/Panels/BrowserPanel.swift +++ b/Sources/Panels/BrowserPanel.swift @@ -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() } } diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index a4212818..aaa39f83 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -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() { diff --git a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift index 376f1eca..87ee1b28 100644 --- a/cmuxTests/CmuxWebViewKeyEquivalentTests.swift +++ b/cmuxTests/CmuxWebViewKeyEquivalentTests.swift @@ -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