Fix split close focus and window background sync
This commit is contained in:
parent
b715f0cebe
commit
b40f3dec35
4 changed files with 216 additions and 16 deletions
|
|
@ -58,12 +58,14 @@ struct ContentView: View {
|
|||
}
|
||||
}
|
||||
.frame(minWidth: 800, minHeight: 600)
|
||||
.background(Color(nsColor: .windowBackgroundColor))
|
||||
.background(Color.clear)
|
||||
.onAppear {
|
||||
focusedTabId = tabManager.selectedTabId
|
||||
tabManager.applyWindowBackgroundForSelectedTab()
|
||||
}
|
||||
.onChange(of: tabManager.selectedTabId) { newValue in
|
||||
focusedTabId = newValue
|
||||
tabManager.applyWindowBackgroundForSelectedTab()
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: .ghosttyDidFocusTab)) { _ in
|
||||
sidebarSelection = .tabs
|
||||
|
|
|
|||
|
|
@ -66,6 +66,15 @@ class GhosttyApp {
|
|||
|
||||
private(set) var app: ghostty_app_t?
|
||||
private(set) var config: ghostty_config_t?
|
||||
private(set) var defaultBackgroundColor: NSColor = .windowBackgroundColor
|
||||
private(set) var defaultBackgroundOpacity: Double = 1.0
|
||||
let backgroundLogEnabled = {
|
||||
if ProcessInfo.processInfo.environment["GHOSTTYTABS_DEBUG_BG"] == "1" {
|
||||
return true
|
||||
}
|
||||
return UserDefaults.standard.bool(forKey: "GhosttyTabsDebugBG")
|
||||
}()
|
||||
private let backgroundLogURL = URL(fileURLWithPath: "/tmp/ghosttytabs-bg.log")
|
||||
private var appObservers: [NSObjectProtocol] = []
|
||||
|
||||
private init() {
|
||||
|
|
@ -90,6 +99,7 @@ class GhosttyApp {
|
|||
// Load default config
|
||||
ghostty_config_load_default_files(config)
|
||||
ghostty_config_finalize(config)
|
||||
updateDefaultBackground(from: config)
|
||||
|
||||
// Create runtime config with callbacks
|
||||
var runtimeConfig = ghostty_runtime_config_s()
|
||||
|
|
@ -203,6 +213,29 @@ class GhosttyApp {
|
|||
ghostty_app_tick(app)
|
||||
}
|
||||
|
||||
private func updateDefaultBackground(from config: ghostty_config_t?) {
|
||||
guard let config else { return }
|
||||
|
||||
var color = ghostty_config_color_s()
|
||||
let bgKey = "background"
|
||||
if ghostty_config_get(config, &color, bgKey, UInt(bgKey.lengthOfBytes(using: .utf8))) {
|
||||
defaultBackgroundColor = NSColor(
|
||||
red: CGFloat(color.r) / 255,
|
||||
green: CGFloat(color.g) / 255,
|
||||
blue: CGFloat(color.b) / 255,
|
||||
alpha: 1.0
|
||||
)
|
||||
}
|
||||
|
||||
var opacity: Double = 1.0
|
||||
let opacityKey = "background-opacity"
|
||||
_ = ghostty_config_get(config, &opacity, opacityKey, UInt(opacityKey.lengthOfBytes(using: .utf8)))
|
||||
defaultBackgroundOpacity = opacity
|
||||
if backgroundLogEnabled {
|
||||
logBackground("default background updated color=\(defaultBackgroundColor) opacity=\(String(format: "%.3f", defaultBackgroundOpacity))")
|
||||
}
|
||||
}
|
||||
|
||||
private func performOnMain<T>(_ work: () -> T) -> T {
|
||||
if Thread.isMainThread {
|
||||
return work()
|
||||
|
|
@ -265,6 +298,32 @@ class GhosttyApp {
|
|||
return true
|
||||
}
|
||||
|
||||
if action.tag == GHOSTTY_ACTION_COLOR_CHANGE,
|
||||
action.action.color_change.kind == GHOSTTY_ACTION_COLOR_KIND_BACKGROUND {
|
||||
let change = action.action.color_change
|
||||
defaultBackgroundColor = NSColor(
|
||||
red: CGFloat(change.r) / 255,
|
||||
green: CGFloat(change.g) / 255,
|
||||
blue: CGFloat(change.b) / 255,
|
||||
alpha: 1.0
|
||||
)
|
||||
if backgroundLogEnabled {
|
||||
logBackground("OSC background change (app target) color=\(defaultBackgroundColor)")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
GhosttyApp.shared.applyBackgroundToKeyWindow()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
if action.tag == GHOSTTY_ACTION_CONFIG_CHANGE {
|
||||
updateDefaultBackground(from: action.action.config_change.config)
|
||||
DispatchQueue.main.async {
|
||||
GhosttyApp.shared.applyBackgroundToKeyWindow()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
guard let userdata = ghostty_surface_userdata(target.target.surface) else { return false }
|
||||
|
|
@ -379,10 +438,57 @@ class GhosttyApp {
|
|||
)
|
||||
}
|
||||
return true
|
||||
case GHOSTTY_ACTION_COLOR_CHANGE:
|
||||
if action.action.color_change.kind == GHOSTTY_ACTION_COLOR_KIND_BACKGROUND {
|
||||
let change = action.action.color_change
|
||||
surfaceView.backgroundColor = NSColor(
|
||||
red: CGFloat(change.r) / 255,
|
||||
green: CGFloat(change.g) / 255,
|
||||
blue: CGFloat(change.b) / 255,
|
||||
alpha: 1.0
|
||||
)
|
||||
if backgroundLogEnabled {
|
||||
logBackground("OSC background change tab=\(surfaceView.tabId?.uuidString ?? "unknown") color=\(surfaceView.backgroundColor?.description ?? "nil")")
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.applyWindowBackgroundIfActive()
|
||||
}
|
||||
}
|
||||
return true
|
||||
case GHOSTTY_ACTION_CONFIG_CHANGE:
|
||||
updateDefaultBackground(from: action.action.config_change.config)
|
||||
DispatchQueue.main.async {
|
||||
surfaceView.applyWindowBackgroundIfActive()
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
private func applyBackgroundToKeyWindow() {
|
||||
guard let window = NSApp.keyWindow ?? NSApp.windows.first else { return }
|
||||
let color = defaultBackgroundColor.withAlphaComponent(defaultBackgroundOpacity)
|
||||
window.backgroundColor = color
|
||||
window.isOpaque = color.alphaComponent >= 1.0
|
||||
if backgroundLogEnabled {
|
||||
logBackground("applied default window background color=\(color) opacity=\(String(format: "%.3f", color.alphaComponent))")
|
||||
}
|
||||
}
|
||||
|
||||
func logBackground(_ message: String) {
|
||||
let line = "GhosttyTabs bg: \(message)\n"
|
||||
if let data = line.data(using: .utf8) {
|
||||
if FileManager.default.fileExists(atPath: backgroundLogURL.path) == false {
|
||||
FileManager.default.createFile(atPath: backgroundLogURL.path, contents: nil)
|
||||
}
|
||||
if let handle = try? FileHandle(forWritingTo: backgroundLogURL) {
|
||||
defer { try? handle.close() }
|
||||
try? handle.seekToEnd()
|
||||
try? handle.write(contentsOf: data)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Terminal Surface (owns the ghostty_surface_t lifecycle)
|
||||
|
|
@ -505,6 +611,10 @@ class TerminalSurface: Identifiable {
|
|||
}
|
||||
}
|
||||
|
||||
func applyWindowBackgroundIfActive() {
|
||||
surfaceView.applyWindowBackgroundIfActive()
|
||||
}
|
||||
|
||||
func setFocus(_ focused: Bool) {
|
||||
guard let surface = surface else { return }
|
||||
ghostty_surface_set_focus(surface, focused)
|
||||
|
|
@ -530,6 +640,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
var desiredFocus: Bool = false
|
||||
var tabId: UUID?
|
||||
var onFocus: (() -> Void)?
|
||||
var backgroundColor: NSColor?
|
||||
private var eventMonitor: Any?
|
||||
private var trackingArea: NSTrackingArea?
|
||||
|
||||
|
|
@ -538,7 +649,8 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
metalLayer.device = MTLCreateSystemDefaultDevice()
|
||||
metalLayer.pixelFormat = .bgra8Unorm
|
||||
metalLayer.framebufferOnly = true
|
||||
metalLayer.isOpaque = true
|
||||
metalLayer.isOpaque = false
|
||||
metalLayer.backgroundColor = NSColor.clear.cgColor
|
||||
metalLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
|
||||
return metalLayer
|
||||
}
|
||||
|
|
@ -560,6 +672,25 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
updateTrackingAreas()
|
||||
}
|
||||
|
||||
private func effectiveBackgroundColor() -> NSColor {
|
||||
let base = backgroundColor ?? GhosttyApp.shared.defaultBackgroundColor
|
||||
let opacity = GhosttyApp.shared.defaultBackgroundOpacity
|
||||
return base.withAlphaComponent(opacity)
|
||||
}
|
||||
|
||||
func applyWindowBackgroundIfActive() {
|
||||
guard let window else { return }
|
||||
if let tabId, let selectedId = AppDelegate.shared?.tabManager?.selectedTabId, tabId != selectedId {
|
||||
return
|
||||
}
|
||||
let color = effectiveBackgroundColor()
|
||||
window.backgroundColor = color
|
||||
window.isOpaque = color.alphaComponent >= 1.0
|
||||
if GhosttyApp.shared.backgroundLogEnabled {
|
||||
GhosttyApp.shared.logBackground("applied window background tab=\(tabId?.uuidString ?? "unknown") color=\(color) opacity=\(String(format: "%.3f", color.alphaComponent))")
|
||||
}
|
||||
}
|
||||
|
||||
private func installEventMonitor() {
|
||||
guard eventMonitor == nil else { return }
|
||||
eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.scrollWheel]) { [weak self] event in
|
||||
|
|
@ -614,6 +745,7 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
if window != nil {
|
||||
attachSurfaceIfNeeded()
|
||||
updateSurfaceSize()
|
||||
applyWindowBackgroundIfActive()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -628,6 +760,8 @@ class GhosttyNSView: NSView, NSUserInterfaceValidations {
|
|||
attachSurfaceIfNeeded()
|
||||
}
|
||||
|
||||
override var isOpaque: Bool { false }
|
||||
|
||||
private func updateSurfaceSize() {
|
||||
guard let terminalSurface = terminalSurface else { return }
|
||||
let scale = window?.screen?.backingScaleFactor ?? 2.0
|
||||
|
|
@ -1078,10 +1212,15 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
scrollView.usesPredominantAxisScrolling = true
|
||||
scrollView.scrollerStyle = .overlay
|
||||
scrollView.drawsBackground = false
|
||||
scrollView.backgroundColor = .clear
|
||||
scrollView.contentView.clipsToBounds = false
|
||||
scrollView.contentView.drawsBackground = false
|
||||
scrollView.contentView.backgroundColor = .clear
|
||||
scrollView.surfaceView = surfaceView
|
||||
|
||||
documentView = NSView(frame: .zero)
|
||||
documentView.wantsLayer = true
|
||||
documentView.layer?.backgroundColor = NSColor.clear.cgColor
|
||||
scrollView.documentView = documentView
|
||||
documentView.addSubview(surfaceView)
|
||||
|
||||
|
|
@ -1206,6 +1345,38 @@ final class GhosttySurfaceScrollView: NSView {
|
|||
}
|
||||
}
|
||||
|
||||
func moveFocus(from previous: GhosttySurfaceScrollView? = nil, delay: TimeInterval? = nil) {
|
||||
let maxDelay: TimeInterval = 0.5
|
||||
guard (delay ?? 0) < maxDelay else { return }
|
||||
|
||||
let nextDelay: TimeInterval = if let delay {
|
||||
delay * 2
|
||||
} else {
|
||||
0.05
|
||||
}
|
||||
|
||||
let work = DispatchWorkItem { [weak self] in
|
||||
guard let self else { return }
|
||||
guard let window = self.window else {
|
||||
self.moveFocus(from: previous, delay: nextDelay)
|
||||
return
|
||||
}
|
||||
|
||||
if let previous, previous !== self {
|
||||
_ = previous.surfaceView.resignFirstResponder()
|
||||
}
|
||||
|
||||
window.makeFirstResponder(self.surfaceView)
|
||||
}
|
||||
|
||||
let queue = DispatchQueue.main
|
||||
if let delay {
|
||||
queue.asyncAfter(deadline: .now() + delay, execute: work)
|
||||
} else {
|
||||
queue.async(execute: work)
|
||||
}
|
||||
}
|
||||
|
||||
private func updateFocusForWindow() {
|
||||
let shouldFocus = isActive && (window?.isKeyWindow ?? false)
|
||||
surfaceView.desiredFocus = shouldFocus
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ fileprivate struct TerminalSplitSubtreeView: View {
|
|||
isActive: isFocused,
|
||||
onFocus: { _ in onFocus(surface.id) }
|
||||
)
|
||||
.background(Color(nsColor: .windowBackgroundColor))
|
||||
.background(Color.clear)
|
||||
|
||||
if isSplit && !isFocused && appearance.unfocusedOverlayOpacity > 0 {
|
||||
Rectangle()
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ class Tab: Identifiable, ObservableObject {
|
|||
func focusSurface(_ id: UUID) {
|
||||
guard focusedSurfaceId != id else { return }
|
||||
focusedSurfaceId = id
|
||||
if let selectedId = AppDelegate.shared?.tabManager?.selectedTabId, selectedId == self.id {
|
||||
focusedSurface?.applyWindowBackgroundIfActive()
|
||||
}
|
||||
}
|
||||
|
||||
func updateSplitViewSize(_ size: CGSize) {
|
||||
|
|
@ -125,22 +128,33 @@ class Tab: Identifiable, ObservableObject {
|
|||
return true
|
||||
}
|
||||
|
||||
private func findNextFocusTargetAfterClosing(
|
||||
node: SplitTree<TerminalSurface>.Node
|
||||
) -> TerminalSurface? {
|
||||
guard let root = splitTree.root else { return nil }
|
||||
|
||||
if root.leftmostLeaf() === node.leftmostLeaf() {
|
||||
return splitTree.focusTarget(for: .next, from: node)
|
||||
}
|
||||
|
||||
return splitTree.focusTarget(for: .previous, from: node)
|
||||
}
|
||||
|
||||
func closeSurface(_ surfaceId: UUID) -> Bool {
|
||||
guard let root = splitTree.root,
|
||||
let targetNode = root.find(id: surfaceId) else {
|
||||
return false
|
||||
}
|
||||
|
||||
let shouldMoveFocus = focusedSurfaceId == surfaceId
|
||||
let nextFocus: TerminalSurface? = if shouldMoveFocus {
|
||||
if root.leftmostLeaf() === targetNode.leftmostLeaf() {
|
||||
splitTree.focusTarget(for: .next, from: targetNode)
|
||||
} else {
|
||||
splitTree.focusTarget(for: .previous, from: targetNode)
|
||||
}
|
||||
let oldFocusedSurface = focusedSurface
|
||||
let shouldMoveFocus = if let focusedSurfaceId {
|
||||
targetNode.find(id: focusedSurfaceId) != nil
|
||||
} else {
|
||||
nil
|
||||
false
|
||||
}
|
||||
let nextFocus: TerminalSurface? = shouldMoveFocus
|
||||
? findNextFocusTargetAfterClosing(node: targetNode)
|
||||
: nil
|
||||
|
||||
splitTree = splitTree.removing(targetNode)
|
||||
|
||||
|
|
@ -150,17 +164,23 @@ class Tab: Identifiable, ObservableObject {
|
|||
}
|
||||
|
||||
if shouldMoveFocus {
|
||||
if let nextFocus {
|
||||
focusedSurfaceId = nextFocus.id
|
||||
} else {
|
||||
focusedSurfaceId = splitTree.root?.leftmostLeaf().id
|
||||
}
|
||||
focusedSurfaceId = nextFocus?.id
|
||||
}
|
||||
|
||||
if focusedSurfaceId == nil {
|
||||
focusedSurfaceId = splitTree.root?.leftmostLeaf().id
|
||||
}
|
||||
|
||||
if !splitTree.isSplit {
|
||||
splitTree = SplitTree(root: splitTree.root, zoomed: nil)
|
||||
}
|
||||
|
||||
if shouldMoveFocus, let newFocusedSurface = focusedSurface {
|
||||
DispatchQueue.main.async {
|
||||
newFocusedSurface.hostedView.moveFocus(from: oldFocusedSurface?.hostedView)
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
@ -229,6 +249,13 @@ class TabManager: ObservableObject {
|
|||
tabs.first(where: { $0.id == tabId })?.focusedSurfaceId
|
||||
}
|
||||
|
||||
func applyWindowBackgroundForSelectedTab() {
|
||||
guard let selectedTabId,
|
||||
let tab = tabs.first(where: { $0.id == selectedTabId }),
|
||||
let surface = tab.focusedSurface else { return }
|
||||
surface.applyWindowBackgroundIfActive()
|
||||
}
|
||||
|
||||
private func updateTabTitle(tabId: UUID, title: String) {
|
||||
guard !title.isEmpty else { return }
|
||||
guard let index = tabs.firstIndex(where: { $0.id == tabId }) else { return }
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue