macos: setup colorspace in base terminal controller

Fixes #2519

This sets up the colorspace for terminal windows in the base controller.

This also modifies some of our logic so its easier for subclasses of
base controllers to specify custom logic when the configuration reloads,
since that's likely to be a common thing.
This commit is contained in:
Mitchell Hashimoto
2024-10-30 20:25:40 -04:00
parent 569d887de8
commit e64b231248
5 changed files with 111 additions and 105 deletions

View File

@ -496,11 +496,6 @@ class AppDelegate: NSObject,
// AppKit mutex on the appearance.
DispatchQueue.main.async { self.syncAppearance() }
// Update all of our windows
terminalManager.windows.forEach { window in
window.controller.configDidReload()
}
// If we have configuration errors, we need to show them.
let c = ConfigurationErrorsController.sharedInstance
c.errors = state.config.errors

View File

@ -33,11 +33,6 @@ class QuickTerminalController: BaseTerminalController {
selector: #selector(onToggleFullscreen),
name: Ghostty.Notification.ghosttyToggleFullscreen,
object: nil)
center.addObserver(
self,
selector: #selector(ghosttyDidReloadConfig),
name: Ghostty.Notification.ghosttyDidReloadConfig,
object: nil)
}
required init?(coder: NSCoder) {
@ -53,6 +48,8 @@ class QuickTerminalController: BaseTerminalController {
// MARK: NSWindowController
override func windowDidLoad() {
super.windowDidLoad()
guard let window = self.window else { return }
// The controller is the window delegate so we can detect events such as
@ -63,9 +60,6 @@ class QuickTerminalController: BaseTerminalController {
// make this restorable, but it isn't currently implemented.
window.isRestorable = false
// Setup our configured appearance that we support.
syncAppearance()
// Setup our initial size based on our configured position
position.setLoaded(window)
@ -297,35 +291,6 @@ class QuickTerminalController: BaseTerminalController {
})
}
private func syncAppearance() {
guard let window else { return }
// If our window is not visible, then delay this. This is possible specifically
// during state restoration but probably in other scenarios as well. To delay,
// we just loop directly on the dispatch queue. We have to delay because some
// APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else {
// Weak window so that if the window changes or is destroyed we aren't holding a ref
DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
return
}
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.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
}
}
// MARK: First Responder
@IBAction override func closeWindow(_ sender: Any) {
@ -357,10 +322,6 @@ class QuickTerminalController: BaseTerminalController {
// We ignore the requested mode and always use non-native for the quick terminal
toggleFullscreen(mode: .nonNative)
}
@objc private func ghosttyDidReloadConfig(notification: SwiftUI.Notification) {
syncAppearance()
}
}
extension Notification.Name {

View File

@ -93,6 +93,11 @@ class BaseTerminalController: NSWindowController,
selector: #selector(didChangeScreenParametersNotification),
name: NSApplication.didChangeScreenParametersNotification,
object: nil)
center.addObserver(
self,
selector: #selector(ghosttyDidReloadConfigNotification),
name: Ghostty.Notification.ghosttyDidReloadConfig,
object: nil)
// Listen for local events that we need to know of outside of
// single surface handlers.
@ -123,7 +128,7 @@ class BaseTerminalController: NSWindowController,
/// Update all surfaces with the focus state. This ensures that libghostty has an accurate view about
/// what surface is focused. This must be called whenever a surface OR window changes focus.
func syncFocusToSurfaceTree() {
private func syncFocusToSurfaceTree() {
guard let tree = self.surfaceTree else { return }
for leaf in tree {
@ -136,6 +141,48 @@ class BaseTerminalController: NSWindowController,
}
}
// Call this whenever we want to setup our appearance parameters based on
// configuration changes.
private func syncAppearance() {
guard let window else { return }
// If our window is not visible, then delay this. This is possible specifically
// during state restoration but probably in other scenarios as well. To delay,
// we just loop directly on the dispatch queue. We have to delay because some
// APIs such as window blur have no effect unless the window is visible.
guard window.isVisible else {
// Weak window so that if the window changes or is destroyed we aren't holding a ref
DispatchQueue.main.async { [weak self] in self?.syncAppearance() }
return
}
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.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
}
// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (ghostty.config.windowColorspace) {
case .displayP3:
window.colorSpace = .displayP3
case .srgb:
window.colorSpace = .sRGB
}
}
// Call this whenever the frame changes
private func windowFrameDidChange() {
// We need to update our saved frame information in case of monitor
@ -145,6 +192,14 @@ class BaseTerminalController: NSWindowController,
savedFrame = .init(window: window.frame, screen: screen.visibleFrame)
}
// MARK: Overridable Callbacks
/// Called whenever Ghostty reloads the configuration. Callers should call super.
open func ghosttyDidReloadConfig() {
// Whenever the config changes we setup our appearance.
syncAppearance()
}
// MARK: Notifications
@objc private func didChangeScreenParametersNotification(_ notification: Notification) {
@ -191,6 +246,10 @@ class BaseTerminalController: NSWindowController,
window.setFrame(newFrame, display: true)
}
@objc private func ghosttyDidReloadConfigNotification(notification: SwiftUI.Notification) {
ghosttyDidReloadConfig()
}
// MARK: Local Events
private func localEventHandler(_ event: NSEvent) -> NSEvent? {
@ -381,7 +440,16 @@ class BaseTerminalController: NSWindowController,
}
}
//MARK: - NSWindowDelegate
// MARK: NSWindowController
override func windowDidLoad() {
super.windowDidLoad()
// Setup our configured appearance that we support.
syncAppearance()
}
// MARK: NSWindowDelegate
// This is called when performClose is called on a window (NOT when close()
// is called directly). performClose is called primarily when UI elements such

View File

@ -78,14 +78,16 @@ class TerminalController: BaseTerminalController {
}
}
//MARK: - Methods
override func ghosttyDidReloadConfig() {
super.ghosttyDidReloadConfig()
func configDidReload() {
guard let window = window as? TerminalWindow else { return }
window.focusFollowsMouse = ghostty.config.focusFollowsMouse
syncAppearance()
}
//MARK: - Methods
/// Update the accessory view of each tab according to the keyboard
/// shortcut that activates it (if any). This is called when the key window
/// changes, when a window is closed, and when tabs are reordered
@ -164,21 +166,6 @@ class TerminalController: BaseTerminalController {
window.titlebarFont = nil
}
// If we have window transparency then set it transparent. Otherwise set it opaque.
if (ghostty.config.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 = ghostty.config.macosWindowShadow
guard window.hasStyledTabs else { return }
@ -208,31 +195,20 @@ class TerminalController: BaseTerminalController {
}
override func windowDidLoad() {
super.windowDidLoad()
guard let window = window as? TerminalWindow else { return }
// Setting all three of these is required for restoration to work.
window.isRestorable = restorable
if (restorable) {
window.restorationClass = TerminalWindowRestoration.self
window.identifier = .init(String(describing: TerminalWindowRestoration.self))
}
// If window decorations are disabled, remove our title
if (!ghostty.config.windowDecorations) { window.styleMask.remove(.titled) }
// Terminals typically operate in sRGB color space and macOS defaults
// to "native" which is typically P3. There is a lot more resources
// covered in this GitHub issue: https://github.com/mitchellh/ghostty/pull/376
// Ghostty defaults to sRGB but this can be overridden.
switch (ghostty.config.windowColorspace) {
case "display-p3":
window.colorSpace = .displayP3
case "srgb":
fallthrough
default:
window.colorSpace = .sRGB
}
// If we have only a single surface (no splits) and that surface requested
// an initial size then we set it here now.
if case let .leaf(leaf) = surfaceTree {
@ -245,21 +221,21 @@ class TerminalController: BaseTerminalController {
frame.size.height -= leaf.surface.frame.size.height
frame.size.width += min(initialSize.width, screen.frame.width)
frame.size.height += min(initialSize.height, screen.frame.height)
// We have no tabs and we are not a split, so set the initial size of the window.
window.setFrame(frame, display: true)
}
}
// Center the window to start, we'll move the window frame automatically
// when cascading.
window.center()
// Make sure our theme is set on the window so styling is correct.
if let windowTheme = ghostty.config.windowTheme {
window.windowTheme = .init(rawValue: windowTheme)
}
// Handle titlebar tabs config option. Something about what we do while setting up the
// titlebar tabs interferes with the window restore process unless window.tabbingMode
// is set to .preferred, so we set it, and switch back to automatic as soon as we can.
@ -272,50 +248,50 @@ class TerminalController: BaseTerminalController {
} else if (ghostty.config.macosTitlebarStyle == "transparent") {
window.transparentTabs = true
}
if window.hasStyledTabs {
// Set the background color of the window
let backgroundColor = NSColor(ghostty.config.backgroundColor)
window.backgroundColor = backgroundColor
// This makes sure our titlebar renders correctly when there is a transparent background
window.titlebarColor = backgroundColor.withAlphaComponent(ghostty.config.backgroundOpacity)
}
// Initialize our content view to the SwiftUI root
window.contentView = NSHostingView(rootView: TerminalView(
ghostty: self.ghostty,
viewModel: self,
delegate: self
))
// If our titlebar style is "hidden" we adjust the style appropriately
if (ghostty.config.macosTitlebarStyle == "hidden") {
window.styleMask = [
// We need `titled` in the mask to get the normal window frame
.titled,
// Full size content view so we can extend
// content in to the hidden titlebar's area
.fullSizeContentView,
.resizable,
.fullSizeContentView,
.resizable,
.closable,
.miniaturizable,
]
// Hide the title
window.titleVisibility = .hidden
window.titlebarAppearsTransparent = true
// Hide the traffic lights (window control buttons)
window.standardWindowButton(.closeButton)?.isHidden = true
window.standardWindowButton(.miniaturizeButton)?.isHidden = true
window.standardWindowButton(.zoomButton)?.isHidden = true
// Disallow tabbing if the titlebar is hidden, since that will (should) also hide the tab bar.
window.tabbingMode = .disallowed
// Nuke it from orbit -- hide the titlebar container entirely, just in case. There are
// some operations that appear to bring back the titlebar visibility so this ensures
// it is gone forever.
@ -324,7 +300,7 @@ class TerminalController: BaseTerminalController {
titleBarContainer.isHidden = true
}
}
// In various situations, macOS automatically tabs new windows. Ghostty handles
// its own tabbing so we DONT want this behavior. This detects this scenario and undoes
// it.
@ -344,9 +320,9 @@ class TerminalController: BaseTerminalController {
window.tabGroup?.removeWindow(window)
}
}
window.focusFollowsMouse = ghostty.config.focusFollowsMouse
// Apply any additional appearance-related properties to the new window.
syncAppearance()
}

View File

@ -128,13 +128,14 @@ extension Ghostty {
return v
}
var windowColorspace: String {
guard let config = self.config else { return "" }
var windowColorspace: WindowColorspace {
guard let config = self.config else { return .srgb }
var v: UnsafePointer<Int8>? = nil
let key = "window-colorspace"
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return "" }
guard let ptr = v else { return "" }
return String(cString: ptr)
guard ghostty_config_get(config, &v, key, UInt(key.count)) else { return .srgb }
guard let ptr = v else { return .srgb }
let str = String(cString: ptr)
return WindowColorspace(rawValue: str) ?? .srgb
}
var windowSaveState: String {
@ -474,4 +475,9 @@ extension Ghostty.Config {
}
}
}
enum WindowColorspace : String {
case srgb
case displayP3 = "display-p3"
}
}