mirror of
https://github.com/ghostty-org/ghostty.git
synced 2026-04-06 07:38:21 +00:00
macos: fix tab title rename hit testing and focus handling in fullscreen mode
This commit is contained in:
@@ -85,13 +85,17 @@ extension NSWindow {
|
||||
|
||||
/// Returns the visual tab index and matching tab button at the given screen point.
|
||||
func tabButtonHit(atScreenPoint screenPoint: NSPoint) -> (index: Int, tabButton: NSView)? {
|
||||
guard let tabBarView else { return nil }
|
||||
let locationInWindow = convertPoint(fromScreen: screenPoint)
|
||||
let locationInTabBar = tabBarView.convert(locationInWindow, from: nil)
|
||||
guard let tabBarView, let tabBarWindow = tabBarView.window else { return nil }
|
||||
|
||||
// In fullscreen, AppKit can host the titlebar and tab bar in a separate
|
||||
// NSToolbarFullScreenWindow. Hit testing has to use that window's base
|
||||
// coordinate space or content clicks can be misinterpreted as tab clicks.
|
||||
let locationInTabBarWindow = tabBarWindow.convertPoint(fromScreen: screenPoint)
|
||||
let locationInTabBar = tabBarView.convert(locationInTabBarWindow, from: nil)
|
||||
guard tabBarView.bounds.contains(locationInTabBar) else { return nil }
|
||||
|
||||
for (index, tabButton) in tabButtonsInVisualOrder().enumerated() {
|
||||
let locationInTabButton = tabButton.convert(locationInWindow, from: nil)
|
||||
let locationInTabButton = tabButton.convert(locationInTabBarWindow, from: nil)
|
||||
if tabButton.bounds.contains(locationInTabButton) {
|
||||
return (index, tabButton)
|
||||
}
|
||||
|
||||
@@ -40,6 +40,8 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate {
|
||||
private weak var hostWindow: NSWindow?
|
||||
/// Delegate that provides and commits title data for target tab windows.
|
||||
private weak var delegate: TabTitleEditorDelegate?
|
||||
/// Local event monitor so fullscreen titlebar-window clicks can also trigger rename.
|
||||
private var eventMonitor: Any?
|
||||
|
||||
/// Active inline editor view, if editing is in progress.
|
||||
private weak var inlineTitleEditor: NSTextField?
|
||||
@@ -52,8 +54,24 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate {
|
||||
|
||||
/// Creates a coordinator bound to a host window and rename delegate.
|
||||
init(hostWindow: NSWindow, delegate: TabTitleEditorDelegate) {
|
||||
super.init()
|
||||
|
||||
self.hostWindow = hostWindow
|
||||
self.delegate = delegate
|
||||
|
||||
// This is needed so that fullscreen clicks can register since they won't
|
||||
// event on the NSWindow. We may want to tighten this up in the future by
|
||||
// only doing this if we're fullscreen.
|
||||
self.eventMonitor = NSEvent.addLocalMonitorForEvents(matching: [.leftMouseDown]) { [weak self] event in
|
||||
guard let self else { return event }
|
||||
return handleMouseDown(event) ? nil : event
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
if let eventMonitor {
|
||||
NSEvent.removeMonitor(eventMonitor)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handles leftMouseDown events from the host window and begins inline edit if possible. If this
|
||||
@@ -64,8 +82,15 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate {
|
||||
// If we don't have a host window to look up the click, we do nothing.
|
||||
guard let hostWindow else { return false }
|
||||
|
||||
// In native fullscreen, AppKit can route titlebar clicks through a detached
|
||||
// NSToolbarFullScreenWindow. Only allow clicks from the host window or its
|
||||
// fullscreen tab bar window so rename handling stays scoped to this tab strip.
|
||||
let sourceWindow = event.window ?? hostWindow
|
||||
guard sourceWindow === hostWindow || sourceWindow === hostWindow.tabBarView?.window
|
||||
else { return false }
|
||||
|
||||
// Find the tab window that is being clicked.
|
||||
let locationInScreen = hostWindow.convertPoint(toScreen: event.locationInWindow)
|
||||
let locationInScreen = sourceWindow.convertPoint(toScreen: event.locationInWindow)
|
||||
guard let tabIndex = hostWindow.tabIndex(atScreenPoint: locationInScreen),
|
||||
let targetWindow = hostWindow.tabbedWindows?[safe: tabIndex],
|
||||
delegate?.tabTitleEditor(self, canRenameTabFor: targetWindow) == true
|
||||
@@ -171,9 +196,11 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate {
|
||||
|
||||
// Focus after insertion so AppKit has created the field editor for this text field.
|
||||
DispatchQueue.main.async { [weak hostWindow, weak editor] in
|
||||
guard let hostWindow, let editor else { return }
|
||||
guard let editor else { return }
|
||||
let responderWindow = editor.window ?? hostWindow
|
||||
guard let responderWindow else { return }
|
||||
editor.isHidden = false
|
||||
hostWindow.makeFirstResponder(editor)
|
||||
responderWindow.makeFirstResponder(editor)
|
||||
if let fieldEditor = editor.currentEditor() as? NSTextView,
|
||||
let editorFont = editor.font {
|
||||
fieldEditor.font = editorFont
|
||||
@@ -204,11 +231,11 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate {
|
||||
inlineTitleTargetWindow = nil
|
||||
|
||||
// Make sure the window grabs focus again
|
||||
if let hostWindow {
|
||||
if let currentEditor = editor.currentEditor(), hostWindow.firstResponder === currentEditor {
|
||||
hostWindow.makeFirstResponder(nil)
|
||||
} else if hostWindow.firstResponder === editor {
|
||||
hostWindow.makeFirstResponder(nil)
|
||||
if let responderWindow = editor.window ?? hostWindow {
|
||||
if let currentEditor = editor.currentEditor(), responderWindow.firstResponder === currentEditor {
|
||||
responderWindow.makeFirstResponder(nil)
|
||||
} else if responderWindow.firstResponder === editor {
|
||||
responderWindow.makeFirstResponder(nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user