From 4e493a92011d3caf1a96296ea1f2eac138854469 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Thu, 26 Feb 2026 10:45:35 +0100 Subject: [PATCH] macos: add TerminalViewContainerTests --- .../Terminal/TerminalViewContainer.swift | 37 +++++-- .../Extensions/NSWindow+Extension.swift | 12 -- .../Terminal/TerminalViewContainerTests.swift | 103 ++++++++++++++++++ 3 files changed, 128 insertions(+), 24 deletions(-) create mode 100644 macos/Tests/Terminal/TerminalViewContainerTests.swift diff --git a/macos/Sources/Features/Terminal/TerminalViewContainer.swift b/macos/Sources/Features/Terminal/TerminalViewContainer.swift index bbe20ba53..eb176f296 100644 --- a/macos/Sources/Features/Terminal/TerminalViewContainer.swift +++ b/macos/Sources/Features/Terminal/TerminalViewContainer.swift @@ -7,9 +7,21 @@ class TerminalViewContainer: NSView { private let terminalView: NSView /// Combined glass effect and inactive tint overlay view - private var glassEffectView: NSView? + private(set) var glassEffectView: NSView? private var derivedConfig: DerivedConfig? + var windowThemeFrameView: NSView? { + window?.contentView?.superview + } + + var windowCornerRadius: CGFloat? { + guard let window, window.responds(to: Selector(("_cornerRadius"))) else { + return nil + } + + return window.value(forKey: "_cornerRadius") as? CGFloat + } + init(@ViewBuilder rootView: () -> Root) { self.terminalView = NSHostingView(rootView: rootView()) super.init(frame: .zero) @@ -79,7 +91,11 @@ class TerminalViewContainer: NSView { guard let config = notification.userInfo?[ Notification.Name.GhosttyConfigChangeKey ] as? Ghostty.Config else { return } - let newValue = DerivedConfig(config: config, preferredBackgroundColor: (window as? TerminalWindow)?.preferredBackgroundColor, cornerRadius: window?.defaultCornerRadius) + ghosttyConfigDidChange(config, preferredBackgroundColor: (window as? TerminalWindow)?.preferredBackgroundColor) + } + + func ghosttyConfigDidChange(_ config: Ghostty.Config, preferredBackgroundColor: NSColor?) { + let newValue = DerivedConfig(config: config, preferredBackgroundColor: preferredBackgroundColor, cornerRadius: windowCornerRadius) guard newValue != derivedConfig else { return } derivedConfig = newValue @@ -167,9 +183,7 @@ private class TerminalGlassView: NSView { ) { glassEffectView.style = style glassEffectView.tintColor = backgroundColor.withAlphaComponent(backgroundOpacity) - if let cornerRadius { - glassEffectView.cornerRadius = cornerRadius - } + glassEffectView.cornerRadius = cornerRadius ?? 0 updateKeyStatus(isKeyWindow, backgroundColor: backgroundColor) } @@ -196,15 +210,15 @@ private class TerminalGlassView: NSView { } #endif // compiler(>=6.2) -private extension TerminalViewContainer { +extension TerminalViewContainer { #if compiler(>=6.2) @available(macOS 26.0, *) - func addGlassEffectViewIfNeeded() -> TerminalGlassView? { + private func addGlassEffectViewIfNeeded() -> TerminalGlassView? { if let existed = glassEffectView as? TerminalGlassView { updateGlassEffectTopInsetIfNeeded() return existed } - guard let themeFrameView = window?.contentView?.superview else { + guard let themeFrameView = windowThemeFrameView else { return nil } let effectView = TerminalGlassView(topOffset: -themeFrameView.safeAreaInsets.top) @@ -220,7 +234,7 @@ private extension TerminalViewContainer { } #endif // compiler(>=6.2) - func updateGlassEffectIfNeeded() { + private func updateGlassEffectIfNeeded() { #if compiler(>=6.2) guard #available(macOS 26.0, *), let derivedConfig else { glassEffectView?.removeFromSuperview() @@ -241,13 +255,12 @@ private extension TerminalViewContainer { #endif // compiler(>=6.2) } - func updateGlassEffectTopInsetIfNeeded() { + private func updateGlassEffectTopInsetIfNeeded() { #if compiler(>=6.2) guard #available(macOS 26.0, *), let effectView = glassEffectView as? TerminalGlassView, - let themeFrameView = window?.contentView?.superview, - let derivedConfig + let themeFrameView = windowThemeFrameView else { return } diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index e57bba1be..3c5cbd23a 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -105,15 +105,3 @@ extension NSWindow { tabButtonHit(atScreenPoint: screenPoint)?.index } } - -// MARK: - Internal Properties - -extension NSWindow { - var defaultCornerRadius: CGFloat? { - guard responds(to: Selector(("_cornerRadius"))) else { - return nil - } - - return value(forKey: "_cornerRadius") as? CGFloat - } -} diff --git a/macos/Tests/Terminal/TerminalViewContainerTests.swift b/macos/Tests/Terminal/TerminalViewContainerTests.swift new file mode 100644 index 000000000..e3df8483e --- /dev/null +++ b/macos/Tests/Terminal/TerminalViewContainerTests.swift @@ -0,0 +1,103 @@ +// +// TerminalViewContainerTests.swift +// Ghostty +// +// Created by Lukas on 26.02.2026. +// + +import SwiftUI +import Testing +@testable import Ghostty + +class MockTerminalViewContainer: TerminalViewContainer { + var _windowCornerRadius: CGFloat? + override var windowThemeFrameView: NSView? { + NSView() + } + + override var windowCornerRadius: CGFloat? { + _windowCornerRadius + } +} + +class MockConfig: Ghostty.Config { + internal init(backgroundBlur: Ghostty.Config.BackgroundBlur, backgroundColor: Color, backgroundOpacity: Double) { + self._backgroundBlur = backgroundBlur + self._backgroundColor = backgroundColor + self._backgroundOpacity = backgroundOpacity + super.init(config: nil) + } + + var _backgroundBlur: Ghostty.Config.BackgroundBlur + var _backgroundColor: Color + var _backgroundOpacity: Double + + override var backgroundBlur: Ghostty.Config.BackgroundBlur { + _backgroundBlur + } + + override var backgroundColor: Color { + _backgroundColor + } + + override var backgroundOpacity: Double { + _backgroundOpacity + } +} + +struct TerminalViewContainerTests { + @Test func glassAvailability() async throws { + let view = await MockTerminalViewContainer { + EmptyView() + } + + let config = MockConfig(backgroundBlur: .macosGlassRegular, backgroundColor: .clear, backgroundOpacity: 1) + await view.ghosttyConfigDidChange(config, preferredBackgroundColor: nil) + try await Task.sleep(nanoseconds: UInt64(1e8)) // wait for the view to be setup if needed + if #available(macOS 26.0, *) { + #expect(view.glassEffectView != nil) + } else { + #expect(view.glassEffectView == nil) + } + } + +#if compiler(>=6.2) + @Test func configChangeUpdatesGlass() async throws { + guard #available(macOS 26.0, *) else { return } + let view = await MockTerminalViewContainer { + EmptyView() + } + let config1 = MockConfig(backgroundBlur: .macosGlassRegular, backgroundColor: .clear, backgroundOpacity: 1) + await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: nil) + let glassEffectView = await view.descendants(withClassName: "NSGlassEffectView").first as? NSGlassEffectView + let effectView = try #require(glassEffectView) + try await Task.sleep(nanoseconds: UInt64(1e8)) // wait for the view to be setup if needed + #expect(effectView.tintColor?.hexString == NSColor.clear.hexString) + + // Test with same config but with different preferredBackgroundColor + await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red) + #expect(effectView.tintColor?.hexString == NSColor.red.hexString) + + // MARK: - Corner Radius + + #expect(effectView.cornerRadius == 0) + await MainActor.run { view._windowCornerRadius = 10 } + + // This won't change, unless ghosttyConfigDidChange is called + #expect(effectView.cornerRadius == 0) + + await view.ghosttyConfigDidChange(config1, preferredBackgroundColor: .red) + #expect(effectView.cornerRadius == 10) + + // MARK: - Glass Style + + #expect(effectView.style == .regular) + + let config2 = MockConfig(backgroundBlur: .macosGlassClear, backgroundColor: .clear, backgroundOpacity: 1) + await view.ghosttyConfigDidChange(config2, preferredBackgroundColor: .red) + + #expect(effectView.style == .clear) + + } +#endif // compiler(>=6.2) +}