macos: transparent titlebar handles transparent background
parent
6ce7f612a6
commit
3595b2a847
|
|
@ -12,6 +12,7 @@
|
||||||
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
552964E62B34A9B400030505 /* vim in Resources */ = {isa = PBXBuildFile; fileRef = 552964E52B34A9B400030505 /* vim */; };
|
||||||
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
857F63812A5E64F200CA4815 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 857F63802A5E64F200CA4815 /* MainMenu.xib */; };
|
||||||
9351BE8E3D22937F003B3499 /* nvim in Resources */ = {isa = PBXBuildFile; fileRef = 9351BE8E2D22937F003B3499 /* nvim */; };
|
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 */; };
|
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 */; };
|
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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
A51BFC1D2B2FB5CE00E92F16 /* About.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = About.xib; sourceTree = "<group>"; };
|
||||||
|
|
@ -464,6 +466,7 @@
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
|
A586366A2DF0A98900E04A10 /* Array+Extension.swift */,
|
||||||
|
A50297342DFA0F3300B4E924 /* Double+Extension.swift */,
|
||||||
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
|
A586366E2DF25D8300E04A10 /* Duration+Extension.swift */,
|
||||||
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
|
A53A29802DB44A5E00B6E02C /* KeyboardShortcut+Extension.swift */,
|
||||||
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
|
A53A297E2DB4480A00B6E02C /* EventModifiers+Extension.swift */,
|
||||||
|
|
@ -737,6 +740,7 @@
|
||||||
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
A51BFC222B2FB6B400E92F16 /* AboutView.swift in Sources */,
|
||||||
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
A5278A9B2AA05B2600CD3039 /* Ghostty.Input.swift in Sources */,
|
||||||
A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */,
|
A53A29812DB44A6100B6E02C /* KeyboardShortcut+Extension.swift in Sources */,
|
||||||
|
A50297352DFA0F3400B4E924 /* Double+Extension.swift in Sources */,
|
||||||
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
A5CBD0562C9E65B80017A1AE /* DraggableWindowView.swift in Sources */,
|
||||||
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
|
C1F26EE92B76CBFC00404083 /* VibrantLayer.m in Sources */,
|
||||||
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */,
|
A5593FDF2DF8D57C00B47B10 /* TerminalWindow.swift in Sources */,
|
||||||
|
|
|
||||||
|
|
@ -449,57 +449,34 @@ class TerminalController: BaseTerminalController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||||
|
// Let our window handle its own appearance
|
||||||
if let window = window as? TerminalWindow {
|
if let window = window as? TerminalWindow {
|
||||||
window.syncAppearance(surfaceConfig)
|
window.syncAppearance(surfaceConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let window = self.window as? LegacyTerminalWindow else { return }
|
guard let window else { return }
|
||||||
|
|
||||||
// Set our explicit appearance if we need to based on the configuration.
|
if let window = window as? LegacyTerminalWindow {
|
||||||
window.appearance = surfaceConfig.windowAppearance
|
// Update our window light/darkness based on our updated background color
|
||||||
|
window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
||||||
|
|
||||||
// Update our window light/darkness based on our updated background color
|
// Sync our zoom state for splits
|
||||||
window.isLightTheme = OSColor(surfaceConfig.backgroundColor).isLightColor
|
window.surfaceIsZoomed = surfaceTree.zoomed != nil
|
||||||
|
|
||||||
// Sync our zoom state for splits
|
// Set the font for the window and tab titles.
|
||||||
window.surfaceIsZoomed = surfaceTree.zoomed != nil
|
if let titleFontName = surfaceConfig.windowTitleFontFamily {
|
||||||
|
window.titlebarFont = NSFont(name: titleFontName, size: NSFont.systemFontSize)
|
||||||
|
} else {
|
||||||
|
window.titlebarFont = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// If our window is not visible, then we do nothing. Some things such as blurring
|
// 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
|
// have no effect if the window is not visible. Ultimately, we'll have this called
|
||||||
// at some point when a surface becomes focused.
|
// at some point when a surface becomes focused.
|
||||||
guard window.isVisible else { return }
|
guard window.isVisible else { return }
|
||||||
|
|
||||||
// Set the font for the window and tab titles.
|
guard let window = window as? LegacyTerminalWindow, window.hasStyledTabs else { return }
|
||||||
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
|
|
||||||
|
|
||||||
guard window.hasStyledTabs else { return }
|
|
||||||
|
|
||||||
// Our background color depends on if our focused surface borders the top or not.
|
// 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
|
// If it does, we match the focused surface. If it doesn't, we use the app
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import AppKit
|
import AppKit
|
||||||
|
import GhosttyKit
|
||||||
|
|
||||||
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
/// The base class for all standalone, "normal" terminal windows. This sets the basic
|
||||||
/// style and configuration of the window based on the app configuration.
|
/// 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.
|
/// used by the manual float on top menu item feature.
|
||||||
static let defaultLevelKey: String = "TerminalDefaultLevel"
|
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
|
// MARK: NSWindow Overrides
|
||||||
|
|
||||||
override func awakeFromNib() {
|
override func awakeFromNib() {
|
||||||
|
|
@ -15,6 +24,9 @@ class TerminalWindow: NSWindow {
|
||||||
// All new windows are based on the app config at the time of creation.
|
// All new windows are based on the app config at the time of creation.
|
||||||
let config = appDelegate.ghostty.config
|
let config = appDelegate.ghostty.config
|
||||||
|
|
||||||
|
// Setup our initial config
|
||||||
|
derivedConfig = .init(config)
|
||||||
|
|
||||||
// If window decorations are disabled, remove our title
|
// If window decorations are disabled, remove our title
|
||||||
if (!config.windowDecorations) { styleMask.remove(.titled) }
|
if (!config.windowDecorations) { styleMask.remove(.titled) }
|
||||||
|
|
||||||
|
|
@ -42,7 +54,71 @@ class TerminalWindow: NSWindow {
|
||||||
// MARK: Positioning And Styling
|
// MARK: Positioning And Styling
|
||||||
|
|
||||||
/// This is called by the controller when there is a need to reset the window apperance.
|
/// 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) {
|
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.
|
// 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(.miniaturizeButton)?.isHidden = true
|
||||||
standardWindowButton(.zoomButton)?.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
|
// MARK: Appearance
|
||||||
|
|
||||||
override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
override func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||||
|
super.syncAppearance(surfaceConfig)
|
||||||
|
|
||||||
lastSurfaceConfig = surfaceConfig
|
lastSurfaceConfig = surfaceConfig
|
||||||
if #available(macOS 26.0, *) {
|
if #available(macOS 26.0, *) {
|
||||||
syncAppearanceTahoe(surfaceConfig)
|
syncAppearanceTahoe(surfaceConfig)
|
||||||
|
|
@ -43,9 +45,23 @@ class TransparentTitlebarTerminalWindow: TerminalWindow {
|
||||||
|
|
||||||
@available(macOS 26.0, *)
|
@available(macOS 26.0, *)
|
||||||
private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
private func syncAppearanceTahoe(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) {
|
||||||
guard let titlebarBackgroundView else { return }
|
// When we have transparency, we need to set the titlebar background to match the
|
||||||
titlebarBackgroundView.isHidden = true
|
// window background but with opacity. The window background is set using the
|
||||||
backgroundColor = NSColor(surfaceConfig.backgroundColor)
|
// "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, *)
|
@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