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) <noreply@anthropic.com>
This commit is contained in:
Stefan Haller 2026-06-11 15:44:00 +02:00
parent a31755de22
commit aa4ff00898

View file

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