From aa4ff0089802bfc352679ad3cee22d524bf94cde Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Thu, 11 Jun 2026 15:44:00 +0200 Subject: [PATCH] Let a mouse binding opt into firing while a popup panel is focused Mouse clicks on a view other than the focused popup panel are normally swallowed by the ShouldHandleMouseEvent gate, so a registered click handler can't run while a modal is up. Hyperlink clicks already dodge this by being handled in an earlier phase; generalize that to ordinary mouse bindings via a HandleWhenPopupPanelFocused flag, dispatched before the gate. No binding sets it yet, so behavior is unchanged. This is what lets a click on the main view stay live behind a popup (e.g. opening a diff line in the editor while the commit-message panel is in front), the way the wheel already scrolls it. Co-Authored-By: Claude Opus 4.8 (1M context) --- pkg/gocui/gui.go | 33 ++++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/pkg/gocui/gui.go b/pkg/gocui/gui.go index af741732a..7e8f8f00f 100644 --- a/pkg/gocui/gui.go +++ b/pkg/gocui/gui.go @@ -86,6 +86,14 @@ type ViewMouseBinding struct { // must be a mouse key Key KeyName + + // If true, this binding is dispatched before ShouldHandleMouseEvent is + // consulted, so it fires even when a popup panel is focused and the click + // lands on a view other than that panel (which is normally swallowed). This + // is the same early phase that hyperlink clicks are handled in; use it for + // clicks that must stay live behind a popup, e.g. opening a diff line in the + // editor from the main view behind the commit-message panel. + HandleWhenPopupPanelFocused bool } type ViewMouseBindingOpts struct { @@ -1740,6 +1748,22 @@ func (g *Gui) onKey(ev *GocuiEvent) error { } } + var mouseOpts ViewMouseBindingOpts + if IsMouseKey(ev.Key) { + isDoubleClick := g.recordClickInfo(newX, newY, ev.Key.KeyName(), v) + mouseOpts = ViewMouseBindingOpts{X: newX, Y: newY, Key: ev.Key.KeyName(), IsDoubleClick: isDoubleClick} + + // Dispatch bindings that opt into firing while a popup panel is focused + // before the gate below gets a chance to reject the click. + matched, err := g.execMouseKeybindings(v, ev, mouseOpts, true) + if err != nil { + return err + } + if matched { + return nil + } + } + if g.ShouldHandleMouseEvent != nil { if !g.ShouldHandleMouseEvent(v, ev.Key.KeyName()) { // Give clients a chance to reject clicks, for example clicks in inactive views @@ -1789,9 +1813,7 @@ func (g *Gui) onKey(ev *GocuiEvent) error { } if IsMouseKey(ev.Key) { - isDoubleClick := g.recordClickInfo(newX, newY, ev.Key.KeyName(), v) - opts := ViewMouseBindingOpts{X: newX, Y: newY, Key: ev.Key.KeyName(), IsDoubleClick: isDoubleClick} - matched, err := g.execMouseKeybindings(v, ev, opts) + matched, err := g.execMouseKeybindings(v, ev, mouseOpts, false) if err != nil { return err } @@ -1855,11 +1877,12 @@ func (g *Gui) recordClickInfo(x, y int, key KeyName, v *View) bool { return isDoubleClick } -func (g *Gui) execMouseKeybindings(view *View, ev *GocuiEvent, opts ViewMouseBindingOpts) (bool, error) { +func (g *Gui) execMouseKeybindings(view *View, ev *GocuiEvent, opts ViewMouseBindingOpts, handleWhenPopupPanelFocused bool) (bool, error) { isMatch := func(binding *ViewMouseBinding) bool { return binding.ViewName == view.Name() && ev.Key.KeyName() == binding.Key && - ev.Key.Mod() == binding.Modifier + ev.Key.Mod() == binding.Modifier && + binding.HandleWhenPopupPanelFocused == handleWhenPopupPanelFocused } // first pass looks for ones that match the focused view