macos: fix tab title rename hit testing and focus handling in fullscreen mode

This commit is contained in:
ydah
2026-03-11 21:33:16 +09:00
committed by Mitchell Hashimoto
parent 87e496b30f
commit c2206542d3
2 changed files with 43 additions and 12 deletions

View File

@@ -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)
}

View File

@@ -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)
}
}