macos: keep glass titlebar inset in sync when tab bar appears (#10105)

Discussion: https://github.com/ghostty-org/ghostty/discussions/10104

Summary:
When background blur uses macOS glass styles, the titlebar becomes fully
transparent after opening a new tab. This updates the glass effect
view’s top inset during layout so
the glass layer continues to cover the titlebar area.

Root Cause:
The glass effect view’s top constraint is computed once using the theme
frame’s safe-area top inset. On macOS 26, opening a new tab changes the
titlebar height/safe-area,
but the constraint is never refreshed. That leaves an uncovered strip at
the top, which appears fully transparent.

Fix:
Track the glass view’s top constraint and update its constant on layout.
This keeps the glass layer aligned with the current safe-area inset
while avoiding unnecessary
reconfiguration.

Tests:
- Manual: `zig build run`, open a new tab with `background-opacity < 1`
and `background-blur = macos-glass-regular` and confirm titlebar is no
longer fully transparent.
- Note: automated tests not added; UI behavior is hard to exercise in
existing test suite.

Window position question:
Opening a new tab seems to reset the window position / trigger a
maximize-like behavior on my system. Is this intended (feature) or a
bug? I did not change this behavior in this PR.

AI Assistance:
This change was implemented with AI assistance (Codex) and reviewed by
me.
This commit is contained in:
Mitchell Hashimoto
2025-12-30 07:21:32 -08:00
committed by GitHub

View File

@@ -8,6 +8,7 @@ class TerminalViewContainer<ViewModel: TerminalViewModel>: NSView {
/// Glass effect view for liquid glass background when transparency is enabled
private var glassEffectView: NSView?
private var glassTopConstraint: NSLayoutConstraint?
private var derivedConfig: DerivedConfig
init(ghostty: Ghostty.App, viewModel: ViewModel, delegate: (any TerminalViewDelegate)? = nil) {
@@ -54,6 +55,12 @@ class TerminalViewContainer<ViewModel: TerminalViewModel>: NSView {
override func viewDidMoveToWindow() {
super.viewDidMoveToWindow()
updateGlassEffectIfNeeded()
updateGlassEffectTopInsetIfNeeded()
}
override func layout() {
super.layout()
updateGlassEffectTopInsetIfNeeded()
}
@objc private func ghosttyConfigDidChange(_ notification: Notification) {
@@ -74,6 +81,7 @@ private extension TerminalViewContainer {
@available(macOS 26.0, *)
func addGlassEffectViewIfNeeded() -> NSGlassEffectView? {
if let existed = glassEffectView as? NSGlassEffectView {
updateGlassEffectTopInsetIfNeeded()
return existed
}
guard let themeFrameView = window?.contentView?.superview else {
@@ -82,12 +90,18 @@ private extension TerminalViewContainer {
let effectView = NSGlassEffectView()
addSubview(effectView, positioned: .below, relativeTo: terminalView)
effectView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
effectView.topAnchor.constraint(equalTo: topAnchor, constant: -themeFrameView.safeAreaInsets.top),
effectView.leadingAnchor.constraint(equalTo: leadingAnchor),
effectView.bottomAnchor.constraint(equalTo: bottomAnchor),
effectView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
glassTopConstraint = effectView.topAnchor.constraint(
equalTo: topAnchor,
constant: -themeFrameView.safeAreaInsets.top
)
if let glassTopConstraint {
NSLayoutConstraint.activate([
glassTopConstraint,
effectView.leadingAnchor.constraint(equalTo: leadingAnchor),
effectView.bottomAnchor.constraint(equalTo: bottomAnchor),
effectView.trailingAnchor.constraint(equalTo: trailingAnchor),
])
}
glassEffectView = effectView
return effectView
}
@@ -98,6 +112,7 @@ private extension TerminalViewContainer {
guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else {
glassEffectView?.removeFromSuperview()
glassEffectView = nil
glassTopConstraint = nil
return
}
guard let effectView = addGlassEffectViewIfNeeded() else {
@@ -120,6 +135,17 @@ private extension TerminalViewContainer {
#endif // compiler(>=6.2)
}
func updateGlassEffectTopInsetIfNeeded() {
#if compiler(>=6.2)
guard #available(macOS 26.0, *), derivedConfig.backgroundBlur.isGlassStyle else {
return
}
guard glassEffectView != nil else { return }
guard let themeFrameView = window?.contentView?.superview else { return }
glassTopConstraint?.constant = -themeFrameView.safeAreaInsets.top
#endif // compiler(>=6.2)
}
struct DerivedConfig: Equatable {
var backgroundOpacity: Double = 0
var backgroundBlur: Ghostty.Config.BackgroundBlur