From 06307313cd649a13f854553791c80aa467a3fc0f Mon Sep 17 00:00:00 2001 From: qiannian Date: Tue, 16 Jun 2026 18:12:07 +0800 Subject: [PATCH] io/input: support direct pointer leave events Allow platform backends to send pointer.Leave directly. The router delivers it to entered handlers so hover state is cleared normally. Signed-off-by: Elias Naur --- io/input/pointer.go | 4 +++- io/input/pointer_test.go | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 1 deletion(-) diff --git a/io/input/pointer.go b/io/input/pointer.go index 7ba9aa36..cc68db4d 100644 --- a/io/input/pointer.go +++ b/io/input/pointer.go @@ -760,6 +760,8 @@ func (q *pointerQueue) Push(handlers map[event.Tag]*handler, state pointerState, if p.pressed { p, evts = q.deliverDragEvent(handlers, p, evts) } + case pointer.Leave: + p, evts, state.cursor, _ = q.deliverEnterLeaveEvents(handlers, state.cursor, p, evts, e) case pointer.Release: evts = q.deliverEvent(handlers, p, evts, e) p.pressed = false @@ -823,7 +825,7 @@ func (q *pointerQueue) deliverEvent(handlers map[event.Tag]*handler, p pointerIn func (q *pointerQueue) deliverEnterLeaveEvents(handlers map[event.Tag]*handler, cursor pointer.Cursor, p pointerInfo, evts []taggedEvent, e pointer.Event) (pointerInfo, []taggedEvent, pointer.Cursor, bool) { changed := false var hits []event.Tag - if e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { + if e.Kind == pointer.Leave || e.Source != pointer.Mouse && !p.pressed && e.Kind != pointer.Press { // Consider non-mouse pointers leaving when they're released. } else { var transSrc *pointerFilter diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index a5e3b4a9..de2ee0bf 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -255,6 +255,45 @@ func TestPointerMove(t *testing.T) { assertEventPointerTypeSequence(t, events(&r, -1, filter(handler2)), pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel) } +func TestPointerLeave(t *testing.T) { + handler := new(int) + var ops op.Ops + + filter := pointer.Filter{ + Target: handler, + Kinds: pointer.Move | pointer.Enter | pointer.Leave | pointer.Cancel, + } + defer clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop() + event.Op(&ops, handler) + + var r Router + events(&r, -1, filter) + r.Frame(&ops) + r.Queue( + pointer.Event{ + Kind: pointer.Move, + Source: pointer.Mouse, + PointerID: 1, + Position: f32.Pt(50, 50), + }, + pointer.Event{ + Kind: pointer.Leave, + Source: pointer.Mouse, + PointerID: 1, + Position: f32.Pt(50, 50), + }, + ) + assertEventPointerTypeSequence(t, events(&r, -1, filter), pointer.Enter, pointer.Move, pointer.Leave) + + r.Queue(pointer.Event{ + Kind: pointer.Move, + Source: pointer.Mouse, + PointerID: 1, + Position: f32.Pt(50, 50), + }) + assertEventPointerTypeSequence(t, events(&r, -1, filter), pointer.Enter, pointer.Move) +} + func TestPointerTypes(t *testing.T) { handler := new(int) var ops op.Ops