From 5b2be45f3abe5ef7d37032876fb7d04f321b469a Mon Sep 17 00:00:00 2001 From: Lawrence Chen <54008264+lawrencecchen@users.noreply.github.com> Date: Thu, 19 Feb 2026 23:38:21 -0800 Subject: [PATCH] Fix browser panel mouse back/forward buttons and middle-click (#131) (#139) Handle multi-button mouse events in the browser panel's WKWebView: - Mouse back button (button 3) triggers goBack(), forward button (button 4) triggers goForward(), enabling side-button navigation on mice like Logitech - Middle-click (button 2) on a link opens it in a new browser tab by hit-testing the click position via JavaScript and routing through the existing openLinkInNewTab mechanism --- Sources/Panels/BrowserPanelView.swift | 8 ++++ Sources/Panels/CmuxWebView.swift | 59 +++++++++++++++++++++++++++ Sources/TabManager.swift | 1 + 3 files changed, 68 insertions(+) diff --git a/Sources/Panels/BrowserPanelView.swift b/Sources/Panels/BrowserPanelView.swift index 76e5e9a1..89a64487 100644 --- a/Sources/Panels/BrowserPanelView.swift +++ b/Sources/Panels/BrowserPanelView.swift @@ -107,6 +107,14 @@ struct BrowserPanelView: View { }) { _ in onRequestPanelFocus() } + .onReceive(NotificationCenter.default.publisher(for: .webViewMiddleClickedLink).filter { [weak panel] note in + guard let webView = note.object as? CmuxWebView else { return false } + return webView === panel?.webView + }) { note in + if let url = note.userInfo?["url"] as? URL { + panel.openLinkInNewTab(url: url) + } + } .onAppear { UserDefaults.standard.register(defaults: [ BrowserSearchSettings.searchEngineKey: BrowserSearchSettings.defaultSearchEngine.rawValue, diff --git a/Sources/Panels/CmuxWebView.swift b/Sources/Panels/CmuxWebView.swift index f4ac545d..ed27c263 100644 --- a/Sources/Panels/CmuxWebView.swift +++ b/Sources/Panels/CmuxWebView.swift @@ -43,6 +43,65 @@ final class CmuxWebView: WKWebView { super.mouseDown(with: event) } + // MARK: - Mouse back/forward buttons & middle-click + + override func otherMouseDown(with event: NSEvent) { + // Button 3 = back, button 4 = forward (multi-button mice like Logitech). + // Consume the event so WebKit doesn't handle it. + switch event.buttonNumber { + case 3: + goBack() + return + case 4: + goForward() + return + default: + break + } + super.otherMouseDown(with: event) + } + + override func otherMouseUp(with event: NSEvent) { + // Middle-click (button 2) on a link opens it in a new tab. + if event.buttonNumber == 2 { + let point = convert(event.locationInWindow, from: nil) + findLinkAtPoint(point) { [weak self] url in + guard let self, let url else { return } + NotificationCenter.default.post( + name: .webViewMiddleClickedLink, + object: self, + userInfo: ["url": url] + ) + } + return + } + super.otherMouseUp(with: event) + } + + /// Use JavaScript to find the nearest anchor element at the given view-local point. + private func findLinkAtPoint(_ point: NSPoint, completion: @escaping (URL?) -> Void) { + // WKWebView's coordinate system is flipped (origin top-left for web content). + let flippedY = bounds.height - point.y + let js = """ + (() => { + let el = document.elementFromPoint(\(point.x), \(flippedY)); + while (el) { + if (el.tagName === 'A' && el.href) return el.href; + el = el.parentElement; + } + return ''; + })(); + """ + evaluateJavaScript(js) { result, _ in + guard let href = result as? String, !href.isEmpty, + let url = URL(string: href) else { + completion(nil) + return + } + completion(url) + } + } + // MARK: - Drag-and-drop passthrough // WKWebView inherently calls registerForDraggedTypes with public.text (and others). diff --git a/Sources/TabManager.swift b/Sources/TabManager.swift index f3c43522..df72c4d0 100644 --- a/Sources/TabManager.swift +++ b/Sources/TabManager.swift @@ -2489,4 +2489,5 @@ extension Notification.Name { static let browserDidFocusAddressBar = Notification.Name("browserDidFocusAddressBar") static let browserDidBlurAddressBar = Notification.Name("browserDidBlurAddressBar") static let webViewDidReceiveClick = Notification.Name("webViewDidReceiveClick") + static let webViewMiddleClickedLink = Notification.Name("webViewMiddleClickedLink") }