mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
io/router: simplify pointer event routing
- Drop pointer.Event.Hit in favour of Enter/Leave events. - Track enter/leaves for each pointer.ID (updates #122). Add test. - Resolve grabs once. - Get rid of scratch slice. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+1
-1
@@ -145,7 +145,7 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
|
|||||||
case pointer.Cancel:
|
case pointer.Cancel:
|
||||||
c.state = StateNormal
|
c.state = StateNormal
|
||||||
case pointer.Press:
|
case pointer.Press:
|
||||||
if c.state == StatePressed || !e.Hit {
|
if c.state == StatePressed {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonLeft {
|
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonLeft {
|
||||||
|
|||||||
@@ -31,11 +31,6 @@ type Event struct {
|
|||||||
Time time.Duration
|
Time time.Duration
|
||||||
// Buttons are the set of pressed mouse buttons for this event.
|
// Buttons are the set of pressed mouse buttons for this event.
|
||||||
Buttons Buttons
|
Buttons Buttons
|
||||||
// Hit is set when the event was within the registered
|
|
||||||
// area for the handler. Hit can be false when a pointer
|
|
||||||
// was pressed within the hit area, and then dragged
|
|
||||||
// outside it.
|
|
||||||
Hit bool
|
|
||||||
// Position is the position of the event, relative to
|
// Position is the position of the event, relative to
|
||||||
// the current transformation, as set by op.TransformOp.
|
// the current transformation, as set by op.TransformOp.
|
||||||
Position f32.Point
|
Position f32.Point
|
||||||
|
|||||||
+85
-83
@@ -20,11 +20,6 @@ type pointerQueue struct {
|
|||||||
handlers map[event.Tag]*pointerHandler
|
handlers map[event.Tag]*pointerHandler
|
||||||
pointers []pointerInfo
|
pointers []pointerInfo
|
||||||
reader ops.Reader
|
reader ops.Reader
|
||||||
scratch []event.Tag
|
|
||||||
|
|
||||||
// prev and curr are two additional scratch slices that track active
|
|
||||||
// pointer event handlers from the previous and current frame
|
|
||||||
prev, curr []event.Tag
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type hitNode struct {
|
type hitNode struct {
|
||||||
@@ -34,13 +29,16 @@ type hitNode struct {
|
|||||||
pass bool
|
pass bool
|
||||||
|
|
||||||
// For handler nodes.
|
// For handler nodes.
|
||||||
key event.Tag
|
tag event.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
type pointerInfo struct {
|
type pointerInfo struct {
|
||||||
id pointer.ID
|
id pointer.ID
|
||||||
pressed bool
|
pressed bool
|
||||||
handlers []event.Tag
|
handlers []event.Tag
|
||||||
|
|
||||||
|
// entered tracks the tags that contain the pointer.
|
||||||
|
entered []event.Tag
|
||||||
}
|
}
|
||||||
|
|
||||||
type pointerHandler struct {
|
type pointerHandler struct {
|
||||||
@@ -98,7 +96,7 @@ func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents, t o
|
|||||||
next: node,
|
next: node,
|
||||||
area: area,
|
area: area,
|
||||||
pass: pass,
|
pass: pass,
|
||||||
key: op.Tag,
|
tag: op.Tag,
|
||||||
})
|
})
|
||||||
node = len(q.hitTree) - 1
|
node = len(q.hitTree) - 1
|
||||||
h, ok := q.handlers[op.Tag]
|
h, ok := q.handlers[op.Tag]
|
||||||
@@ -110,7 +108,7 @@ func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents, t o
|
|||||||
h.active = true
|
h.active = true
|
||||||
h.area = area
|
h.area = area
|
||||||
h.transform = t
|
h.transform = t
|
||||||
h.wantsGrab = h.wantsGrab || op.Grab
|
h.wantsGrab = op.Grab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -131,17 +129,14 @@ func (q *pointerQueue) opHit(handlers *[]event.Tag, pos f32.Point) {
|
|||||||
} else {
|
} else {
|
||||||
idx = n.next
|
idx = n.next
|
||||||
}
|
}
|
||||||
if n.key != nil {
|
if n.tag != nil {
|
||||||
if _, exists := q.handlers[n.key]; exists {
|
if _, exists := q.handlers[n.tag]; exists {
|
||||||
*handlers = append(*handlers, n.key)
|
*handlers = append(*handlers, n.tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(whereswaldon): This method fails to handle the case in which a child
|
|
||||||
// hit area extends outside of the boundaries of its parent. Such child hit
|
|
||||||
// areas will not recieve some events as a result.
|
|
||||||
func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool {
|
func (q *pointerQueue) hit(areaIdx int, p f32.Point) bool {
|
||||||
for areaIdx != -1 {
|
for areaIdx != -1 {
|
||||||
a := &q.areas[areaIdx]
|
a := &q.areas[areaIdx]
|
||||||
@@ -176,20 +171,41 @@ func (q *pointerQueue) Frame(root *op.Ops, events *handlerEvents) {
|
|||||||
q.collectHandlers(&q.reader, events, op.TransformOp{}, -1, -1, false)
|
q.collectHandlers(&q.reader, events, op.TransformOp{}, -1, -1, false)
|
||||||
for k, h := range q.handlers {
|
for k, h := range q.handlers {
|
||||||
if !h.active {
|
if !h.active {
|
||||||
q.dropHandler(k, events)
|
q.dropHandlers(events, k)
|
||||||
delete(q.handlers, k)
|
delete(q.handlers, k)
|
||||||
}
|
}
|
||||||
|
if h.wantsGrab {
|
||||||
|
for _, p := range q.pointers {
|
||||||
|
if !p.pressed {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for i, k2 := range p.handlers {
|
||||||
|
if k2 == k {
|
||||||
|
// Drop other handlers that lost their grab.
|
||||||
|
q.dropHandlers(events, p.handlers[i+1:]...)
|
||||||
|
q.dropHandlers(events, p.handlers[:i]...)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (q *pointerQueue) dropHandler(k event.Tag, events *handlerEvents) {
|
func (q *pointerQueue) dropHandlers(events *handlerEvents, tags ...event.Tag) {
|
||||||
events.Add(k, pointer.Event{Type: pointer.Cancel})
|
for _, k := range tags {
|
||||||
q.handlers[k].wantsGrab = false
|
events.Add(k, pointer.Event{Type: pointer.Cancel})
|
||||||
for i := range q.pointers {
|
for i := range q.pointers {
|
||||||
p := &q.pointers[i]
|
p := &q.pointers[i]
|
||||||
for i := len(p.handlers) - 1; i >= 0; i-- {
|
for i := len(p.handlers) - 1; i >= 0; i-- {
|
||||||
if p.handlers[i] == k {
|
if p.handlers[i] == k {
|
||||||
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
|
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := len(p.entered) - 1; i >= 0; i-- {
|
||||||
|
if p.entered[i] == k {
|
||||||
|
p.entered = append(p.entered[:i], p.entered[i+1:]...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -200,7 +216,7 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
|||||||
if e.Type == pointer.Cancel {
|
if e.Type == pointer.Cancel {
|
||||||
q.pointers = q.pointers[:0]
|
q.pointers = q.pointers[:0]
|
||||||
for k := range q.handlers {
|
for k := range q.handlers {
|
||||||
q.dropHandler(k, events)
|
q.dropHandlers(events, k)
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -216,86 +232,72 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
|||||||
pidx = len(q.pointers) - 1
|
pidx = len(q.pointers) - 1
|
||||||
}
|
}
|
||||||
p := &q.pointers[pidx]
|
p := &q.pointers[pidx]
|
||||||
if !p.pressed && (e.Type == pointer.Move || e.Type == pointer.Press) {
|
|
||||||
p.handlers, q.scratch = q.scratch[:0], p.handlers
|
q.deliverEnterLeaveEvents(p, events, e)
|
||||||
q.opHit(&p.handlers, e.Position)
|
if e.Type == pointer.Release {
|
||||||
|
q.deliverEvent(p, events, e)
|
||||||
|
p.pressed = false
|
||||||
|
}
|
||||||
|
if !p.pressed {
|
||||||
if e.Type == pointer.Press {
|
if e.Type == pointer.Press {
|
||||||
p.pressed = true
|
p.pressed = true
|
||||||
}
|
}
|
||||||
|
p.handlers = p.handlers[:0]
|
||||||
|
q.opHit(&p.handlers, e.Position)
|
||||||
|
q.deliverEnterLeaveEvents(p, events, e)
|
||||||
}
|
}
|
||||||
if p.pressed {
|
if e.Type != pointer.Release {
|
||||||
// Resolve grabs.
|
q.deliverEvent(p, events, e)
|
||||||
q.scratch = q.scratch[:0]
|
|
||||||
for i, k := range p.handlers {
|
|
||||||
h := q.handlers[k]
|
|
||||||
if h.wantsGrab {
|
|
||||||
q.scratch = append(q.scratch, p.handlers[:i]...)
|
|
||||||
q.scratch = append(q.scratch, p.handlers[i+1:]...)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Drop handlers that lost their grab.
|
|
||||||
for _, k := range q.scratch {
|
|
||||||
q.dropHandler(k, events)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if e.Type == pointer.Release {
|
if !p.pressed && len(p.entered) == 0 {
|
||||||
|
// No longer need to track pointer.
|
||||||
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
|
q.pointers = append(q.pointers[:pidx], q.pointers[pidx+1:]...)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Deliver enter and leave events for pointers that entered or left a hit area.
|
func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
||||||
q.curr, q.prev = q.prev[:0], q.curr
|
|
||||||
q.opHit(&q.curr, e.Position)
|
|
||||||
q.deliverEventsToMissingHandlers(q.prev, q.curr, pointer.Enter, e, events)
|
|
||||||
q.deliverEventsToMissingHandlers(q.curr, q.prev, pointer.Leave, e, events)
|
|
||||||
|
|
||||||
for _, k := range p.handlers {
|
for _, k := range p.handlers {
|
||||||
h := q.handlers[k]
|
h := q.handlers[k]
|
||||||
e := e
|
e := e
|
||||||
if p.pressed && len(p.handlers) == 1 {
|
if p.pressed && len(p.handlers) == 1 {
|
||||||
e.Priority = pointer.Grabbed
|
e.Priority = pointer.Grabbed
|
||||||
}
|
}
|
||||||
e.Hit = q.hit(h.area, e.Position)
|
|
||||||
e.Position = h.transform.Invert().Transform(e.Position)
|
e.Position = h.transform.Invert().Transform(e.Position)
|
||||||
|
|
||||||
events.Add(k, e)
|
events.Add(k, e)
|
||||||
if e.Type == pointer.Release {
|
|
||||||
// Release grab when the number of grabs reaches zero.
|
|
||||||
grabs := 0
|
|
||||||
for _, p := range q.pointers {
|
|
||||||
if p.pressed && len(p.handlers) == 1 && p.handlers[0] == k {
|
|
||||||
grabs++
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if grabs == 0 {
|
|
||||||
h.wantsGrab = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// deliverEventsToMissingHandlers compares the a and b handler lists to find all
|
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
||||||
// handlers in b that are missing from a. It then sends an event templated off of
|
for _, k := range p.handlers {
|
||||||
// evTemplate but with the type specified by evType.
|
h := q.handlers[k]
|
||||||
//
|
e := e
|
||||||
// This is useful for delivering pointer.Enter and pointer.Leave events.
|
if p.pressed && len(p.handlers) == 1 {
|
||||||
func (q *pointerQueue) deliverEventsToMissingHandlers(a, b []event.Tag, evType pointer.Type, evTemplate pointer.Event, events *handlerEvents) {
|
e.Priority = pointer.Grabbed
|
||||||
for _, newH := range b {
|
}
|
||||||
found := false
|
|
||||||
for _, oldH := range a {
|
// Hit-test to deliver Enter/Leave events. Consider non-mouse
|
||||||
if newH == oldH {
|
// events leaving when they're Released.
|
||||||
found = true
|
hit := (e.Source == pointer.Mouse || p.pressed) && q.hit(h.area, e.Position)
|
||||||
|
entered := -1
|
||||||
|
for i, k2 := range p.entered {
|
||||||
|
if k2 == k {
|
||||||
|
entered = i
|
||||||
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !found {
|
|
||||||
h, ok := q.handlers[newH]
|
e.Position = h.transform.Invert().Transform(e.Position)
|
||||||
if !ok {
|
|
||||||
continue
|
switch {
|
||||||
}
|
case !hit && entered != -1:
|
||||||
ev := evTemplate
|
p.entered = append(p.entered[:entered], p.entered[entered+1:]...)
|
||||||
ev.Hit = q.hit(h.area, evTemplate.Position)
|
e.Type = pointer.Leave
|
||||||
ev.Position = h.transform.Invert().Transform(evTemplate.Position)
|
events.Add(k, e)
|
||||||
ev.Type = evType
|
case hit && entered == -1:
|
||||||
events.Add(newH, ev)
|
p.entered = append(p.entered, k)
|
||||||
|
e.Type = pointer.Enter
|
||||||
|
events.Add(k, e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+53
-24
@@ -5,6 +5,7 @@ package router
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
@@ -344,6 +345,44 @@ func TestPointerActiveInputDisappears(t *testing.T) {
|
|||||||
assertEventSequence(t, r.Events(handler1), pointer.Cancel)
|
assertEventSequence(t, r.Events(handler1), pointer.Cancel)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMultitouch(t *testing.T) {
|
||||||
|
var ops op.Ops
|
||||||
|
|
||||||
|
// Add two separate handlers.
|
||||||
|
h1, h2 := new(int), new(int)
|
||||||
|
addPointerHandler(&ops, h1, image.Rect(0, 0, 100, 100))
|
||||||
|
addPointerHandler(&ops, h2, image.Rect(0, 100, 100, 200))
|
||||||
|
|
||||||
|
h1pt, h2pt := f32.Pt(0, 0), f32.Pt(0, 100)
|
||||||
|
var p1, p2 pointer.ID = 0, 1
|
||||||
|
|
||||||
|
var r Router
|
||||||
|
r.Frame(&ops)
|
||||||
|
r.Add(
|
||||||
|
pointer.Event{
|
||||||
|
Type: pointer.Press,
|
||||||
|
Position: h1pt,
|
||||||
|
PointerID: p1,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.Add(
|
||||||
|
pointer.Event{
|
||||||
|
Type: pointer.Press,
|
||||||
|
Position: h2pt,
|
||||||
|
PointerID: p2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
r.Add(
|
||||||
|
pointer.Event{
|
||||||
|
Type: pointer.Release,
|
||||||
|
Position: h2pt,
|
||||||
|
PointerID: p2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
assertEventSequence(t, r.Events(h1), pointer.Cancel, pointer.Enter, pointer.Press)
|
||||||
|
assertEventSequence(t, r.Events(h2), pointer.Cancel, pointer.Enter, pointer.Press, pointer.Release)
|
||||||
|
}
|
||||||
|
|
||||||
// addPointerHandler adds a pointer.InputOp for the tag in a
|
// addPointerHandler adds a pointer.InputOp for the tag in a
|
||||||
// rectangular area.
|
// rectangular area.
|
||||||
func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) {
|
func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) {
|
||||||
@@ -354,36 +393,26 @@ func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) {
|
|||||||
pointer.InputOp{Tag: tag}.Add(ops)
|
pointer.InputOp{Tag: tag}.Add(ops)
|
||||||
}
|
}
|
||||||
|
|
||||||
// toTypes converts a sequence of event.Event to their pointer.Types. It assumes
|
// pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes
|
||||||
// that all input events are of underlying type pointer.Event, and thus will
|
// that all input events are of underlying type pointer.Event, and thus will
|
||||||
// panic if some are not.
|
// panic if some are not.
|
||||||
func toTypes(events []event.Event) []pointer.Type {
|
func pointerTypes(events []event.Event) []pointer.Type {
|
||||||
out := make([]pointer.Type, len(events))
|
var types []pointer.Type
|
||||||
for i, event := range events {
|
for _, e := range events {
|
||||||
out[i] = event.(pointer.Event).Type
|
if e, ok := e.(pointer.Event); ok {
|
||||||
|
types = append(types, e.Type)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return out
|
return types
|
||||||
}
|
}
|
||||||
|
|
||||||
// assertEventSequence ensures that the provided actualEvents match the expected event types
|
// assertEventSequence ensures that the provided actualEvents match the expected event types
|
||||||
// in the provided order
|
// in the provided order.
|
||||||
func assertEventSequence(t *testing.T, actualEvents []event.Event, expected ...pointer.Type) {
|
func assertEventSequence(t *testing.T, events []event.Event, expected ...pointer.Type) {
|
||||||
if len(actualEvents) != len(expected) {
|
t.Helper()
|
||||||
t.Errorf("expected %v events, got %v", expected, toTypes(actualEvents))
|
got := pointerTypes(events)
|
||||||
}
|
if !reflect.DeepEqual(got, expected) {
|
||||||
for i, event := range actualEvents {
|
t.Errorf("expected %v events, got %v", expected, got)
|
||||||
pointerEvent, ok := event.(pointer.Event)
|
|
||||||
if !ok {
|
|
||||||
t.Errorf("actualEvents[%d] is not a pointer event, type %T", i, event)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if len(expected) <= i {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if pointerEvent.Type != expected[i] {
|
|
||||||
t.Errorf("actualEvents[%d] has type %s, expected %s", i, pointerEvent.Type.String(), expected[i].String())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user