macos: transparent titlebar handles transparent background
parent
6ce7f612a6
commit
3595b2a847
|
|
@ -12,6 +12,7 @@
|
|||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
||||
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = A50297342DFA0F3300B4E924 /* Double+Extension.swift */; };
|
||||
A514C8D62B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D72B54A16400493A16 /* Ghostty.Config.swift in Sources */ = {isa = PBXBuildFile; fileRef = A514C8D52B54A16400493A16 /* Ghostty.Config.swift */; };
|
||||
A514C8D82B54DC6800493A16 /* Ghostty.App.swift in Sources */ = {isa = PBXBuildFile; fileRef = A53D0C992B543F3B00305CE6 /* Ghostty.App.swift */; };
|
||||
|
|
@ -134,6 +135,7 @@
|
|||
552964E52B34A9B400030505 /* vim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = vim; path = "../zig-out/share/vim"; sourceTree = "<group>"; };
|
||||
857F63802A5E64F200CA4815 /* MainMenu.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = MainMenu.xib; sourceTree = "<group>"; };
|
||||
9351BE8E2D22937F003B3499 /* nvim */ = {isa = PBXFileReference; lastKnownFileType = folder; name = nvim; path = "../zig-out/share/nvim"; sourceTree = "<group>"; };
|
||||
A50297342DFA0F3300B4E924 /* Double+Extension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Double+Extension.swift"; sourceTree = "<group>"; };
|
||||
A514C8D52B54A16400493A16 /* Ghostty.Config.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Ghostty.Config.swift; sourceTree = "<group>"; };
|
||||
A51B78462AF4B58B00F3EDB9 /* LegacyTerminalWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LegacyTerminalWindow.swift; sourceTree = "<group>"; };
|
||||
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
|
||||
|
|
@ -464,6 +466,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
|
||||
A50297342DFA0F3300B4E924 /* Double+Extension.swift */,
|
||||
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
|
||||
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
|
||||
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
|
||||
|
|
@ -737,6 +740,7 @@
|
|||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||
A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */,
|
||||
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */,
|
||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
|
||||
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */,
|
||||
|
|
|
|||
|
|
@ -449,57 +449,34 @@ class TerminalController: BaseTerminalController {
|
|||
}
|
||||
|
||||
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||
// Let our window handle its own appearance
|
||||
if let window = window as? TerminalWindow {
|
||||
window.syncAppearance(surfaceConfig)
|
||||
}
|
||||
|
||||
guard let window = self.window as? LegacyTerminalWindow else { return }
|
||||
|
||||
// Set our explicit appearance if we need to based on the configuration.
|
||||
window.appearance = surfaceConfig.windowAppearance
|
||||
guard let window else { return }
|
||||
|
||||
if let window = window as? LegacyTerminalWindow {
|
||||
// Update our window light/darkness based on our updated background color
|
||||
window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||
|
||||
// Sync our zoom state for splits
|
||||
window.surfaceIsZoomed = surfaceTree.zoomed != nil
|
||||
|
||||
// If our window is not visible, then we do nothing. Some things such as blurring
|
||||
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||
// at some point when a surface becomes focused.
|
||||
guard window.isVisible else { return }
|
||||
|
||||
// Set the font for the window and tab titles.
|
||||
if let titleFontName = surfaceConfig.windowTitleFontFamily {
|
||||
window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize)
|
||||
} else {
|
||||
window.titlebarFont = nil
|
||||
}
|
||||
|
||||
// If we have window transparency then set it transparent. Otherwise set it opaque.
|
||||
|
||||
// Window transparency only takes effect if our window is not native fullscreen.
|
||||
// In native fullscreen we disable transparency/opacity because the background
|
||||
// becomes gray and widgets show through.
|
||||
if (!window.styleMask.contains(.fullScreen) &&
|
||||
surfaceConfig.backgroundOpacity < 1
|
||||
) {
|
||||
window.isOpaque = false
|
||||
|
||||
// This is weird, but we don't use ".clear" because this creates a look that
|
||||
// matches Terminal.app much more closer. This lets users transition from
|
||||
// Terminal.app more easily.
|
||||
window.backgroundColor = .white.withAlphaComponent(0.001)
|
||||
|
||||
ghostty_set_window_background_blur(ghostty.app, Unmanaged.passUnretained(window).toOpaque())
|
||||
} else {
|
||||
window.isOpaque = true
|
||||
window.backgroundColor = .windowBackgroundColor
|
||||
}
|
||||
|
||||
window.hasShadow = surfaceConfig.macosWindowShadow
|
||||
// If our window is not visible, then we do nothing. Some things such as blurring
|
||||
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||
// at some point when a surface becomes focused.
|
||||
guard window.isVisible else { return }
|
||||
|
||||
guard window.hasStyledTabs else { return }
|
||||
guard let window = window as? LegacyTerminalWindow, window.hasStyledTabs else { return }
|
||||
|
||||
// Our background color depends on if our focused surface borders the top or not.
|
||||
// If it does, we match the focused surface. If it doesn't, we use the app
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import AppKit
|
||||
import GhosttyKit
|
||||
|
||||
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||
/// style and configuration of the window based on the app configuration.
|
||||
|
|
@ -7,6 +8,14 @@ class TerminalWindow: NSWindow {
|
|||
/// used by the manual float on top menu item feature.
|
||||
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
||||
|
||||
/// The configuration derived from the Ghostty config so we don't need to rely on references.
|
||||
private var derivedConfig: DerivedConfig?
|
||||
|
||||
/// Gets the terminal controller from the window controller.
|
||||
var terminalController: TerminalController? {
|
||||
windowController as? TerminalController
|
||||
}
|
||||
|
||||
// MARK: NSWindow Overrides
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
|
@ -15,6 +24,9 @@ class TerminalWindow: NSWindow {
|
|||
// All new windows are based on the app config at the time of creation.
|
||||
let config = appDelegate.ghostty.config
|
||||
|
||||
// Setup our initial config
|
||||
derivedConfig = .init(config)
|
||||
|
||||
// If window decorations are disabled, remove our title
|
||||
if (!config.windowDecorations) { styleMask.remove(.titled) }
|
||||
|
||||
|
|
@ -42,7 +54,71 @@ class TerminalWindow: NSWindow {
|
|||
// MARK: Positioning And Styling
|
||||
|
||||
/// This is called by the controller when there is a need to reset the window apperance.
|
||||
func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {}
|
||||
func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||
// If our window is not visible, then we do nothing. Some things such as blurring
|
||||
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||
// at some point when a surface becomes focused.
|
||||
guard isVisible else { return }
|
||||
|
||||
// Basic properties
|
||||
appearance = surfaceConfig.windowAppearance
|
||||
hasShadow = surfaceConfig.macosWindowShadow
|
||||
|
||||
// Window transparency only takes effect if our window is not native fullscreen.
|
||||
// In native fullscreen we disable transparency/opacity because the background
|
||||
// becomes gray and widgets show through.
|
||||
if !styleMask.contains(.fullScreen) &&
|
||||
surfaceConfig.backgroundOpacity < 1
|
||||
{
|
||||
isOpaque = false
|
||||
|
||||
// This is weird, but we don't use ".clear" because this creates a look that
|
||||
// matches Terminal.app much more closer. This lets users transition from
|
||||
// Terminal.app more easily.
|
||||
backgroundColor = .white.withAlphaComponent(0.001)
|
||||
|
||||
if let appDelegate = NSApp.delegate as? AppDelegate {
|
||||
ghostty_set_window_background_blur(
|
||||
appDelegate.ghostty.app,
|
||||
Unmanaged.passUnretained(self).toOpaque())
|
||||
}
|
||||
} else {
|
||||
isOpaque = true
|
||||
|
||||
let backgroundColor = preferredBackgroundColor ?? NSColor(surfaceConfig.backgroundColor)
|
||||
self.backgroundColor = backgroundColor.withAlphaComponent(1)
|
||||
}
|
||||
}
|
||||
|
||||
/// The preferred window background color. The current window background color may not be set
|
||||
/// to this, since this is dynamic based on the state of the surface tree.
|
||||
///
|
||||
/// This background color will include alpha transparency if set. If the caller doesn't want that,
|
||||
/// change the alpha channel again manually.
|
||||
var preferredBackgroundColor: NSColor? {
|
||||
if let terminalController, !terminalController.surfaceTree.isEmpty {
|
||||
// If our focused surface borders the top then we prefer its background color
|
||||
if let focusedSurface = terminalController.focusedSurface,
|
||||
let treeRoot = terminalController.surfaceTree.root,
|
||||
let focusedNode = treeRoot.node(view: focusedSurface),
|
||||
treeRoot.spatial().doesBorder(side: .up, from: focusedNode),
|
||||
let backgroundcolor = focusedSurface.backgroundColor {
|
||||
let alpha = focusedSurface.derivedConfig.backgroundOpacity.clamped(to: 0.001...1)
|
||||
return NSColor(backgroundcolor).withAlphaComponent(alpha)
|
||||
}
|
||||
|
||||
// Doesn't border the top or we don't have a focused surface, so
|
||||
// we try to match the top-left surface.
|
||||
let topLeftSurface = terminalController.surfaceTree.root?.leftmostLeaf()
|
||||
if let topLeftBgColor = topLeftSurface?.backgroundColor {
|
||||
let alpha = topLeftSurface?.derivedConfig.backgroundOpacity.clamped(to: 0.001...1) ?? 1
|
||||
return NSColor(topLeftBgColor).withAlphaComponent(alpha)
|
||||
}
|
||||
}
|
||||
|
||||
let alpha = derivedConfig?.backgroundOpacity.clamped(to: 0.001...1) ?? 1
|
||||
return derivedConfig?.backgroundColor.withAlphaComponent(alpha)
|
||||
}
|
||||
|
||||
private func setInitialWindowPosition(x: Int16?, y: Int16?, windowDecorations: Bool) {
|
||||
// If we don't have an X/Y then we try to use the previously saved window pos.
|
||||
|
|
@ -72,4 +148,21 @@ class TerminalWindow: NSWindow {
|
|||
standardWindowButton(.miniaturizeButton)?.isHidden = true
|
||||
standardWindowButton(.zoomButton)?.isHidden = true
|
||||
}
|
||||
|
||||
// MARK: Config
|
||||
|
||||
struct DerivedConfig {
|
||||
let backgroundColor: NSColor
|
||||
let backgroundOpacity: Double
|
||||
|
||||
init() {
|
||||
self.backgroundColor = NSColor.windowBackgroundColor
|
||||
self.backgroundOpacity = 1
|
||||
}
|
||||
|
||||
init(_ config: Ghostty.Config) {
|
||||
self.backgroundColor = NSColor(config.backgroundColor)
|
||||
self.backgroundOpacity = config.backgroundOpacity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
|||
// MARK: Appearance
|
||||
|
||||
override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||
super.syncAppearance(surfaceConfig)
|
||||
|
||||
lastSurfaceConfig = surfaceConfig
|
||||
if #available(macOS 26.0, *) {
|
||||
syncAppearanceTahoe(surfaceConfig)
|
||||
|
|
@ -43,9 +45,23 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
|||
|
||||
@available(macOS 26.0, *)
|
||||
private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||
guard let titlebarBackgroundView else { return }
|
||||
titlebarBackgroundView.isHidden = true
|
||||
backgroundColor = NSColor(surfaceConfig.backgroundColor)
|
||||
// When we have transparency, we need to set the titlebar background to match the
|
||||
// window background but with opacity. The window background is set using the
|
||||
// "preferred background color" property.
|
||||
//
|
||||
// As an inverse, if we don't have transparency, we don't bother with this because
|
||||
// the window background will be set to the correct color so we can just hide the
|
||||
// titlebar completely and we're good to go.
|
||||
if !isOpaque {
|
||||
if let titlebarView = titlebarContainer?.firstDescendant(withClassName: "NSTitlebarView") {
|
||||
titlebarView.wantsLayer = true
|
||||
titlebarView.layer?.backgroundColor = preferredBackgroundColor?.cgColor
|
||||
}
|
||||
}
|
||||
|
||||
// In all cases, we have to hide the background view since this has multiple subviews
|
||||
// that force a background color.
|
||||
titlebarBackgroundView?.isHidden = true
|
||||
}
|
||||
|
||||
@available(macOS 13.0, *)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,5 @@
|
|||
extension Double {
|
||||
func clamped(to range: ClosedRange<Double>) -> Double {
|
||||
return Swift.min(Swift.max(self, range.lowerBound), range.upperBound)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue