diff --git a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift index 3c5cbd23a..46758a42d 100644 --- a/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift +++ b/macos/Sources/Helpers/Extensions/NSWindow+Extension.swift @@ -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) } diff --git a/macos/Sources/Helpers/TabTitleEditor.swift b/macos/Sources/Helpers/TabTitleEditor.swift index 570be1bf4..4be2c5306 100644 --- a/macos/Sources/Helpers/TabTitleEditor.swift +++ b/macos/Sources/Helpers/TabTitleEditor.swift @@ -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) } }