Deliver mouse release after a drag

Releasing a mouse button was delivered as a plain mouse-move (hover)
event: the release processing resets dragState to NOT_DRAGGING, after
which the event fell into the NOT_DRAGGING branch. Views therefore had
no way of telling that a drag gesture ended, which the upcoming
drag-based features (range selection, commit reordering) need.

Deliver the release as a real mouse event with the MouseRelease key
and normalize its modifiers to ModNone, so release bindings also match
modified drags. Make recordClickInfo ignore it: a release is the end of
a click, not a click of its own, and must not break double-click
detection.
This commit is contained in:
Stefan Haller 2026-07-24 08:13:03 +02:00
parent 38d2293a10
commit 44a2bbeb7c
3 changed files with 23 additions and 9 deletions

View file

@ -1773,6 +1773,12 @@ func (g *Gui) recordClickInfo(x, y int, key KeyName, v *View) bool {
g.lastClick = nil
return false
}
// A release ends a gesture but is not a click of its own; it must leave
// the click info of the press that started it alone, or no double click
// could ever be detected.
if key == MouseRelease {
return false
}
clickInfo := &clickInfo{
x: x,

View file

@ -202,7 +202,6 @@ const (
var (
lastMouseKey tcell.ButtonMask = tcell.ButtonNone
lastMouseMod tcell.ModMask = tcell.ModNone
dragState = NOT_DRAGGING
lastX = 0
lastY = 0
@ -367,10 +366,10 @@ func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent {
// process button events (not wheel events)
button &= tcell.ButtonMask(0xff)
newButtonPress := false
buttonReleased := false
if button != tcell.ButtonNone && lastMouseKey == tcell.ButtonNone {
newButtonPress = true
lastMouseKey = button
lastMouseMod = tev.Modifiers()
switch button {
case tcell.ButtonPrimary:
mouseKey = MouseLeft
@ -388,6 +387,7 @@ func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent {
switch tev.Buttons() {
case tcell.ButtonNone:
if lastMouseKey != tcell.ButtonNone {
buttonReleased = true
switch lastMouseKey {
case tcell.ButtonPrimary:
dragState = NOT_DRAGGING
@ -395,14 +395,13 @@ func gocuiEventFromTcellEvent(tev tcell.Event) GocuiEvent {
case tcell.ButtonMiddle:
default:
}
mouseMod = Modifier(lastMouseMod)
lastMouseMod = tcell.ModNone
mouseMod = ModNone
lastMouseKey = tcell.ButtonNone
}
default:
}
if !wheeling {
if !wheeling && !buttonReleased {
switch dragState {
case NOT_DRAGGING:
return GocuiEvent{

View file

@ -32,16 +32,25 @@ func TestMouseReleaseAfterDragIsMouseEvent(t *testing.T) {
gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonPrimary, tcell.ModNone))
releaseEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonNone, tcell.ModNone))
/* EXPECTED:
assert.Equal(t, eventMouse, releaseEvent.Type)
assert.Equal(t, MouseRelease, releaseEvent.Key.KeyName())
ACTUAL: */
assert.Equal(t, eventMouseMove, releaseEvent.Type)
}
func TestMouseReleaseDoesNotKeepPressModifiers(t *testing.T) {
t.Cleanup(resetMouseState)
resetMouseState()
gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 2, tcell.ButtonPrimary, tcell.ModAlt))
gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonPrimary, tcell.ModAlt))
releaseEvent := gocuiEventFromTcellEvent(tcell.NewEventMouse(1, 3, tcell.ButtonNone, tcell.ModAlt))
assert.Equal(t, eventMouse, releaseEvent.Type)
assert.Equal(t, MouseRelease, releaseEvent.Key.KeyName())
assert.Equal(t, ModNone, releaseEvent.Key.Mod())
}
func resetMouseState() {
lastMouseKey = tcell.ButtonNone
lastMouseMod = tcell.ModNone
dragState = NOT_DRAGGING
lastX = 0
lastY = 0