From 205c05d59d016222b350b63dd10f8745b1a5d831 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:00:51 +0100 Subject: [PATCH 1/4] macos: passthrough mouse down event to TabTitleEditor if needed --- .../Window Styles/TerminalWindow.swift | 2 +- macos/Sources/Helpers/TabTitleEditor.swift | 26 +++++++++++++++---- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 33ca7e3d8..519218c04 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -181,7 +181,7 @@ class TerminalWindow: NSWindow { override var canBecomeMain: Bool { return true } override func sendEvent(_ event: NSEvent) { - if tabTitleEditor.handleDoubleClick(event) { + if tabTitleEditor.handleMouseDown(event) { return } diff --git a/macos/Sources/Helpers/TabTitleEditor.swift b/macos/Sources/Helpers/TabTitleEditor.swift index 667834a3b..a36df54ae 100644 --- a/macos/Sources/Helpers/TabTitleEditor.swift +++ b/macos/Sources/Helpers/TabTitleEditor.swift @@ -52,11 +52,10 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { self.delegate = delegate } - /// Handles double-click events from the host window and begins inline edit if possible. If this - /// returns true then the double click was handled by the coordinator. - func handleDoubleClick(_ event: NSEvent) -> Bool { - // We only want double-clicks - guard event.type == .leftMouseDown, event.clickCount == 2 else { return false } + /// Handles leftMouseDown events from the host window and begins inline edit if possible. If this + /// returns true then the event was handled by the coordinator. + func handleMouseDown(_ event: NSEvent) -> Bool { + guard event.type == .leftMouseDown else { return false } // If we don't have a host window to look up the click, we do nothing. guard let hostWindow else { return false } @@ -68,6 +67,14 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { delegate?.tabTitleEditor(self, canRenameTabFor: targetWindow) == true else { return false } + guard !isMouseEventWithinEditor(event) else { + // If the click lies within the editor, + // we should forward the event to the editor + inlineTitleEditor?.mouseDown(with: event) + return true + } + // We only want double-clicks to enable editing + guard event.clickCount == 2 else { return false } // We need to start editing in a separate event loop tick, so set that up. pendingEditWorkItem?.cancel() let workItem = DispatchWorkItem { [weak self, weak targetWindow] in @@ -336,3 +343,12 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { finishEditing(commit: true) } } + +private extension TabTitleEditor { + func isMouseEventWithinEditor(_ event: NSEvent) -> Bool { + guard let editor = inlineTitleEditor?.currentEditor() else { + return false + } + return editor.convert(editor.bounds, to: nil).contains(event.locationInWindow) + } +} From 661470897e878b766254e59f30531192d7ae2771 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:24:40 +0100 Subject: [PATCH 2/4] macos: passthrough right mouse down event to TabTitleEditor if needed --- .../Terminal/Window Styles/TerminalWindow.swift | 2 +- .../TitlebarTabsTahoeTerminalWindow.swift | 4 ++++ macos/Sources/Helpers/TabTitleEditor.swift | 12 ++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift index 519218c04..dc744180d 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TerminalWindow.swift @@ -38,7 +38,7 @@ class TerminalWindow: NSWindow { private var tabMenuObserver: NSObjectProtocol? /// Handles inline tab title editing for this host window. - private lazy var tabTitleEditor = TabTitleEditor( + private(set) lazy var tabTitleEditor = TabTitleEditor( hostWindow: self, delegate: self ) diff --git a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift index 184614831..6df1b14bc 100644 --- a/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift +++ b/macos/Sources/Features/Terminal/Window Styles/TitlebarTabsTahoeTerminalWindow.swift @@ -90,6 +90,10 @@ class TitlebarTabsTahoeTerminalWindow: TransparentTitlebarTerminalWindow, NSTool return } + guard !tabTitleEditor.handleRightMouseDown(event) else { + return + } + let locationInTabBar = tabBarView.convert(event.locationInWindow, from: nil) guard tabBarView.bounds.contains(locationInTabBar) else { super.sendEvent(event) diff --git a/macos/Sources/Helpers/TabTitleEditor.swift b/macos/Sources/Helpers/TabTitleEditor.swift index a36df54ae..b38e8ac4c 100644 --- a/macos/Sources/Helpers/TabTitleEditor.swift +++ b/macos/Sources/Helpers/TabTitleEditor.swift @@ -92,6 +92,18 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { return true } + /// Handles rightMouseDown events from the host window. + /// + /// If this returns true then the event was handled by the coordinator. + func handleRightMouseDown(_ event: NSEvent) -> Bool { + if isMouseEventWithinEditor(event) { + inlineTitleEditor?.rightMouseDown(with: event) + return true + } else { + return false + } + } + /// Begins editing the given target tab window title. Returns true if we're able to start the /// inline edit. @discardableResult From 78fdff34a969d3864ae5a471f673a19ab5e064cf Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:21:34 +0100 Subject: [PATCH 3/4] macos: hide close button when editing tab title --- macos/Sources/Helpers/TabTitleEditor.swift | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/macos/Sources/Helpers/TabTitleEditor.swift b/macos/Sources/Helpers/TabTitleEditor.swift index b38e8ac4c..6ce8d1c1a 100644 --- a/macos/Sources/Helpers/TabTitleEditor.swift +++ b/macos/Sources/Helpers/TabTitleEditor.swift @@ -41,6 +41,8 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { private weak var inlineTitleTargetWindow: NSWindow? /// Original hidden state for title labels that are temporarily hidden while editing. private var hiddenLabels: [(label: NSTextField, wasHidden: Bool)] = [] + /// Original hidden state for buttons that are temporarily hidden while editing. + private var hiddenButtons: [(button: NSButton, wasHidden: Bool)] = [] /// Original button title state restored once editing finishes. private var buttonState: (button: NSButton, title: String, attributedTitle: NSAttributedString?)? /// Deferred begin-editing work used to avoid visual flicker on double-click. @@ -170,6 +172,16 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { } else { buttonState = nil } + + hiddenButtons = tabButton + .descendants(withClassName: "NSButton") + .compactMap { $0 as? NSButton } + .map { ($0, $0.isHidden) } + + for (btn, _) in hiddenButtons { + btn.isHidden = true + } + tabButton.layoutSubtreeIfNeeded() tabButton.displayIfNeeded() tabButton.addSubview(editor) @@ -232,6 +244,11 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { } self.buttonState = nil + for (btn, wasHidden) in hiddenButtons { + btn.isHidden = wasHidden + } + hiddenButtons.removeAll() + // Delegate owns title persistence semantics (including empty-title handling). guard commit, let targetWindow else { return } delegate?.tabTitleEditor(self, didCommitTitle: editedTitle, for: targetWindow) From 44377071323b629480a367abf80862a1d7b084b0 Mon Sep 17 00:00:00 2001 From: Lukas <134181853+bo2themax@users.noreply.github.com> Date: Tue, 3 Mar 2026 17:42:17 +0100 Subject: [PATCH 4/4] macos: use a separated struct to hide and restore tab states --- macos/Sources/Helpers/TabTitleEditor.swift | 109 ++++++++++++--------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/macos/Sources/Helpers/TabTitleEditor.swift b/macos/Sources/Helpers/TabTitleEditor.swift index 6ce8d1c1a..0a1efae32 100644 --- a/macos/Sources/Helpers/TabTitleEditor.swift +++ b/macos/Sources/Helpers/TabTitleEditor.swift @@ -39,12 +39,8 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { private weak var inlineTitleEditor: NSTextField? /// Tab window currently being edited. private weak var inlineTitleTargetWindow: NSWindow? - /// Original hidden state for title labels that are temporarily hidden while editing. - private var hiddenLabels: [(label: NSTextField, wasHidden: Bool)] = [] - /// Original hidden state for buttons that are temporarily hidden while editing. - private var hiddenButtons: [(button: NSButton, wasHidden: Bool)] = [] - /// Original button title state restored once editing finishes. - private var buttonState: (button: NSButton, title: String, attributedTitle: NSAttributedString?)? + /// Original state of the tab bar. + private var previousTabState: TabUIState? /// Deferred begin-editing work used to avoid visual flicker on double-click. private var pendingEditWorkItem: DispatchWorkItem? @@ -125,12 +121,11 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { pendingEditWorkItem = nil finishEditing(commit: true) + let tabState = TabUIState(tabButton: tabButton) + // Build the editor using title text and style derived from the tab's existing label. - let titleLabels = tabButton - .descendants(withClassName: "NSTextField") - .compactMap { $0 as? NSTextField } let editedTitle = delegate?.tabTitleEditor(self, titleFor: targetWindow) ?? targetWindow.title - let sourceLabel = sourceTabTitleLabel(from: titleLabels, matching: editedTitle) + let sourceLabel = sourceTabTitleLabel(from: tabState.labels.map(\.label), matching: editedTitle) let editorFrame = tabTitleEditorFrame(for: tabButton, sourceLabel: sourceLabel) guard editorFrame.width >= 20, editorFrame.height >= 14 else { return false } @@ -157,30 +152,11 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { inlineTitleEditor = editor inlineTitleTargetWindow = targetWindow - + previousTabState = tabState // Temporarily hide native title label views while editing so only the text field is visible. CATransaction.begin() CATransaction.setDisableActions(true) - hiddenLabels = titleLabels.map { ($0, $0.isHidden) } - for label in titleLabels { - label.isHidden = true - } - if let tabButton = tabButton as? NSButton { - buttonState = (tabButton, tabButton.title, tabButton.attributedTitle) - tabButton.title = "" - tabButton.attributedTitle = NSAttributedString(string: "") - } else { - buttonState = nil - } - - hiddenButtons = tabButton - .descendants(withClassName: "NSButton") - .compactMap { $0 as? NSButton } - .map { ($0, $0.isHidden) } - - for (btn, _) in hiddenButtons { - btn.isHidden = true - } + tabState.hide() tabButton.layoutSubtreeIfNeeded() tabButton.displayIfNeeded() @@ -232,22 +208,8 @@ final class TabTitleEditor: NSObject, NSTextFieldDelegate { editor.removeFromSuperview() - // Restore original tab title presentation. - for (label, wasHidden) in hiddenLabels { - label.isHidden = wasHidden - } - hiddenLabels.removeAll() - - if let buttonState { - buttonState.button.title = buttonState.title - buttonState.button.attributedTitle = buttonState.attributedTitle ?? NSAttributedString(string: buttonState.title) - } - self.buttonState = nil - - for (btn, wasHidden) in hiddenButtons { - btn.isHidden = wasHidden - } - hiddenButtons.removeAll() + previousTabState?.restore() + previousTabState = nil // Delegate owns title persistence semantics (including empty-title handling). guard commit, let targetWindow else { return } @@ -381,3 +343,56 @@ private extension TabTitleEditor { return editor.convert(editor.bounds, to: nil).contains(event.locationInWindow) } } + +private extension TabTitleEditor { + struct TabUIState { + /// Original hidden state for title labels that are temporarily hidden while editing. + let labels: [(label: NSTextField, wasHidden: Bool)] + /// Original hidden state for buttons that are temporarily hidden while editing. + let buttons: [(button: NSButton, wasHidden: Bool)] + /// Original button title state restored once editing finishes. + let titleButton: (button: NSButton, title: String, attributedTitle: NSAttributedString?)? + + init(tabButton: NSView) { + labels = tabButton + .descendants(withClassName: "NSTextField") + .compactMap { $0 as? NSTextField } + .map { ($0, $0.isHidden) } + buttons = tabButton + .descendants(withClassName: "NSButton") + .compactMap { $0 as? NSButton } + .map { ($0, $0.isHidden) } + if let button = tabButton as? NSButton { + titleButton = (button, button.title, button.attributedTitle) + } else { + titleButton = nil + } + } + + func hide() { + for (label, _) in labels { + label.isHidden = true + } + for (btn, _) in buttons { + btn.isHidden = true + } + titleButton?.button.title = "" + titleButton?.button.attributedTitle = NSAttributedString(string: "") + } + + func restore() { + for (label, wasHidden) in labels { + label.isHidden = wasHidden + } + for (btn, wasHidden) in buttons { + btn.isHidden = wasHidden + } + if let titleButton { + titleButton.button.title = titleButton.title + if let attributedTitle = titleButton.attributedTitle { + titleButton.button.attributedTitle = attributedTitle + } + } + } + } +}