diff --git a/macos/Sources/Features/Terminal/TerminalController.swift b/macos/Sources/Features/Terminal/TerminalController.swift index 09b758a1e..e997868f7 100644 --- a/macos/Sources/Features/Terminal/TerminalController.swift +++ b/macos/Sources/Features/Terminal/TerminalController.swift @@ -93,21 +93,37 @@ class TerminalController: BaseTerminalController { //MARK: - Methods @objc private func ghosttyConfigDidChange(_ notification: Notification) { - // We only care if the configuration is a global configuration, not a - // surface-specific one. - guard notification.object == nil else { return } - // Get our managed configuration object out guard let config = notification.userInfo?[ Notification.Name.GhosttyConfigChangeKey ] as? Ghostty.Config else { return } - // Update our derived config - self.derivedConfig = DerivedConfig(config) + // If this is an app-level config update then we update some things. + if (notification.object == nil) { + // Update our derived config + self.derivedConfig = DerivedConfig(config) - guard let window = window as? TerminalWindow else { return } - window.focusFollowsMouse = config.focusFollowsMouse - syncAppearance(config) + guard let window = window as? TerminalWindow else { return } + window.focusFollowsMouse = config.focusFollowsMouse + + // If we have no surfaces in our window (is that possible?) then we update + // our window appearance based on the root config. If we have surfaces, we + // don't call this because the TODO + if surfaceTree == nil { + syncAppearance(.init(config)) + } + + return + } + + // This is a surface-level config update. If we have the surface, we + // update our appearance based on it. + guard let surfaceView = notification.object as? Ghostty.SurfaceView else { return } + guard surfaceTree?.contains(view: surfaceView) ?? false else { return } + + // We can't use surfaceView.derivedConfig because it may not be updated + // yet since it also responds to notifications. + syncAppearance(.init(config)) } /// Update the accessory view of each tab according to the keyboard @@ -168,7 +184,7 @@ class TerminalController: BaseTerminalController { self.relabelTabs() } - private func syncAppearance(_ config: Ghostty.Config) { + private func syncAppearance(_ surfaceConfig: Ghostty.SurfaceView.DerivedConfig) { guard let window = self.window as? TerminalWindow else { return } // If our window is not visible, then delay this. This is possible specifically @@ -177,19 +193,19 @@ class TerminalController: BaseTerminalController { // 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(config) } + DispatchQueue.main.async { [weak self] in self?.syncAppearance(surfaceConfig) } return } // Set the font for the window and tab titles. - if let titleFontName = config.windowTitleFontFamily { + 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. - if (config.backgroundOpacity < 1) { + if (surfaceConfig.backgroundOpacity < 1) { window.isOpaque = false // This is weird, but we don't use ".clear" because this creates a look that @@ -203,14 +219,14 @@ class TerminalController: BaseTerminalController { window.backgroundColor = .windowBackgroundColor } - window.hasShadow = config.macosWindowShadow + window.hasShadow = surfaceConfig.macosWindowShadow guard window.hasStyledTabs else { return } // The titlebar is always updated. We don't need to worry about opacity // because we handle it here. - let backgroundColor = OSColor(config.backgroundColor) - window.titlebarColor = backgroundColor.withAlphaComponent(config.backgroundOpacity) + let backgroundColor = OSColor(surfaceConfig.backgroundColor) + window.titlebarColor = backgroundColor.withAlphaComponent(surfaceConfig.backgroundOpacity) if (window.isOpaque) { // Bg color is only synced if we have no transparency. This is because @@ -377,8 +393,10 @@ class TerminalController: BaseTerminalController { window.focusFollowsMouse = config.focusFollowsMouse - // Apply any additional appearance-related properties to the new window. - syncAppearance(config) + // Apply any additional appearance-related properties to the new window. We + // apply this based on the root config but change it later based on surface + // config (see focused surface change callback). + syncAppearance(.init(config)) } // Shows the "+" button in the tab bar, responds to that click. @@ -515,6 +533,15 @@ class TerminalController: BaseTerminalController { window.surfaceIsZoomed = to } + override func focusedSurfaceDidChange(to: Ghostty.SurfaceView?) { + super.focusedSurfaceDidChange(to: to) + + // When our focus changes, we update our window appearance based on the + // currently focused surface. + guard let focusedSurface else { return } + syncAppearance(focusedSurface.derivedConfig) + } + //MARK: - Notifications @objc private func onMoveTab(notification: SwiftUI.Notification) { diff --git a/macos/Sources/Ghostty/SurfaceView_AppKit.swift b/macos/Sources/Ghostty/SurfaceView_AppKit.swift index c678ca79d..8f281df54 100644 --- a/macos/Sources/Ghostty/SurfaceView_AppKit.swift +++ b/macos/Sources/Ghostty/SurfaceView_AppKit.swift @@ -48,6 +48,9 @@ extension Ghostty { // Whether the pointer should be visible or not @Published private(set) var pointerStyle: BackportPointerStyle = .default + /// The configuration derived from the Ghostty config so we don't need to rely on references. + @Published private(set) var derivedConfig: DerivedConfig + // An initial size to request for a window. This will only affect // then the view is moved to a new window. var initialSize: NSSize? = nil @@ -114,6 +117,13 @@ extension Ghostty { self.markedText = NSMutableAttributedString() self.uuid = uuid ?? .init() + // Our initial config always is our application wide config. + if let appDelegate = NSApplication.shared.delegate as? AppDelegate { + self.derivedConfig = DerivedConfig(appDelegate.ghostty.config) + } else { + self.derivedConfig = DerivedConfig() + } + // Initialize with some default frame size. The important thing is that this // is non-zero so that our layer bounds are non-zero so that our renderer // can do SOMETHING. @@ -137,6 +147,11 @@ extension Ghostty { selector: #selector(ghosttyDidEndKeySequence), name: Ghostty.Notification.didEndKeySequence, object: self) + center.addObserver( + self, + selector: #selector(ghosttyConfigDidChange(_:)), + name: .ghosttyConfigDidChange, + object: self) center.addObserver( self, selector: #selector(windowDidChangeScreen), @@ -333,6 +348,16 @@ extension Ghostty { keySequence = [] } + @objc private func ghosttyConfigDidChange(_ notification: SwiftUI.Notification) { + // Get our managed configuration object out + guard let config = notification.userInfo?[ + SwiftUI.Notification.Name.GhosttyConfigChangeKey + ] as? Ghostty.Config else { return } + + // Update our derived config + self.derivedConfig = DerivedConfig(config) + } + @objc private func windowDidChangeScreen(notification: SwiftUI.Notification) { guard let window = self.window else { return } guard let object = notification.object as? NSWindow, window == object else { return } @@ -1025,6 +1050,27 @@ extension Ghostty { Ghostty.moveFocus(to: self) } } + + struct DerivedConfig { + let backgroundColor: Color + let backgroundOpacity: Double + let macosWindowShadow: Bool + let windowTitleFontFamily: String? + + init() { + self.backgroundColor = Color(NSColor.windowBackgroundColor) + self.backgroundOpacity = 1 + self.macosWindowShadow = true + self.windowTitleFontFamily = nil + } + + init(_ config: Ghostty.Config) { + self.backgroundColor = config.backgroundColor + self.backgroundOpacity = config.backgroundOpacity + self.macosWindowShadow = config.macosWindowShadow + self.windowTitleFontFamily = config.windowTitleFontFamily + } + } } } diff --git a/src/config/Config.zig b/src/config/Config.zig index e68ad3da8..a22779ac1 100644 --- a/src/config/Config.zig +++ b/src/config/Config.zig @@ -365,7 +365,6 @@ const c = @cImport({ /// be fixed in a future update: /// /// - macOS: titlebar tabs style is not updated when switching themes. -/// - macOS: native titlebar style is not supported. /// theme: ?Theme = null, @@ -2756,12 +2755,6 @@ pub fn finalize(self: *Config) !void { // This setting doesn't make sense with different light/dark themes // because it'll force the theme based on the Ghostty theme. if (self.@"window-theme" == .auto) self.@"window-theme" = .system; - - // This is buggy with different light/dark themes and is noted - // in the documentation. - if (self.@"macos-titlebar-style" == .transparent) { - self.@"macos-titlebar-style" = .native; - } } }