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:
|
||||
c.state = StateNormal
|
||||
case pointer.Press:
|
||||
if c.state == StatePressed || !e.Hit {
|
||||
if c.state == StatePressed {
|
||||
break
|
||||
}
|
||||
if e.Source == pointer.Mouse && e.Buttons != pointer.ButtonLeft {
|
||||
|
||||
@@ -31,11 +31,6 @@ type Event struct {
|
||||
Time time.Duration
|
||||
// Buttons are the set of pressed mouse buttons for this event.
|
||||
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
|
||||
// the current transformation, as set by op.TransformOp.
|
||||
Position f32.Point
|
||||
|
||||
+85
-83
@@ -20,11 +20,6 @@ type pointerQueue struct {
|
||||
handlers map[event.Tag]*pointerHandler
|
||||
pointers []pointerInfo
|
||||
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 {
|
||||
@@ -34,13 +29,16 @@ type hitNode struct {
|
||||
pass bool
|
||||
|
||||
// For handler nodes.
|
||||
key event.Tag
|
||||
tag event.Tag
|
||||
}
|
||||
|
||||
type pointerInfo struct {
|
||||
id pointer.ID
|
||||
pressed bool
|
||||
handlers []event.Tag
|
||||
|
||||
// entered tracks the tags that contain the pointer.
|
||||
entered []event.Tag
|
||||
}
|
||||
|
||||
type pointerHandler struct {
|
||||
@@ -98,7 +96,7 @@ func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents, t o
|
||||
next: node,
|
||||
area: area,
|
||||
pass: pass,
|
||||
key: op.Tag,
|
||||
tag: op.Tag,
|
||||
})
|
||||
node = len(q.hitTree) - 1
|
||||
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.area = area
|
||||
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 {
|
||||
idx = n.next
|
||||
}
|
||||
if n.key != nil {
|
||||
if _, exists := q.handlers[n.key]; exists {
|
||||
*handlers = append(*handlers, n.key)
|
||||
if n.tag != nil {
|
||||
if _, exists := q.handlers[n.tag]; exists {
|
||||
*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 {
|
||||
for areaIdx != -1 {
|
||||
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)
|
||||
for k, h := range q.handlers {
|
||||
if !h.active {
|
||||
q.dropHandler(k, events)
|
||||
q.dropHandlers(events, 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) {
|
||||
events.Add(k, pointer.Event{Type: pointer.Cancel})
|
||||
q.handlers[k].wantsGrab = false
|
||||
for i := range q.pointers {
|
||||
p := &q.pointers[i]
|
||||
for i := len(p.handlers) - 1; i >= 0; i-- {
|
||||
if p.handlers[i] == k {
|
||||
p.handlers = append(p.handlers[:i], p.handlers[i+1:]...)
|
||||
func (q *pointerQueue) dropHandlers(events *handlerEvents, tags ...event.Tag) {
|
||||
for _, k := range tags {
|
||||
events.Add(k, pointer.Event{Type: pointer.Cancel})
|
||||
for i := range q.pointers {
|
||||
p := &q.pointers[i]
|
||||
for i := len(p.handlers) - 1; i >= 0; i-- {
|
||||
if p.handlers[i] == k {
|
||||
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 {
|
||||
q.pointers = q.pointers[:0]
|
||||
for k := range q.handlers {
|
||||
q.dropHandler(k, events)
|
||||
q.dropHandlers(events, k)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -216,86 +232,72 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
|
||||
pidx = len(q.pointers) - 1
|
||||
}
|
||||
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.opHit(&p.handlers, e.Position)
|
||||
|
||||
q.deliverEnterLeaveEvents(p, events, e)
|
||||
if e.Type == pointer.Release {
|
||||
q.deliverEvent(p, events, e)
|
||||
p.pressed = false
|
||||
}
|
||||
if !p.pressed {
|
||||
if e.Type == pointer.Press {
|
||||
p.pressed = true
|
||||
}
|
||||
p.handlers = p.handlers[:0]
|
||||
q.opHit(&p.handlers, e.Position)
|
||||
q.deliverEnterLeaveEvents(p, events, e)
|
||||
}
|
||||
if p.pressed {
|
||||
// Resolve grabs.
|
||||
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 {
|
||||
q.deliverEvent(p, events, e)
|
||||
}
|
||||
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:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// Deliver enter and leave events for pointers that entered or left a hit area.
|
||||
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)
|
||||
|
||||
func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
||||
for _, k := range p.handlers {
|
||||
h := q.handlers[k]
|
||||
e := e
|
||||
if p.pressed && len(p.handlers) == 1 {
|
||||
e.Priority = pointer.Grabbed
|
||||
}
|
||||
e.Hit = q.hit(h.area, e.Position)
|
||||
e.Position = h.transform.Invert().Transform(e.Position)
|
||||
|
||||
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
|
||||
// handlers in b that are missing from a. It then sends an event templated off of
|
||||
// evTemplate but with the type specified by evType.
|
||||
//
|
||||
// This is useful for delivering pointer.Enter and pointer.Leave events.
|
||||
func (q *pointerQueue) deliverEventsToMissingHandlers(a, b []event.Tag, evType pointer.Type, evTemplate pointer.Event, events *handlerEvents) {
|
||||
for _, newH := range b {
|
||||
found := false
|
||||
for _, oldH := range a {
|
||||
if newH == oldH {
|
||||
found = true
|
||||
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
|
||||
for _, k := range p.handlers {
|
||||
h := q.handlers[k]
|
||||
e := e
|
||||
if p.pressed && len(p.handlers) == 1 {
|
||||
e.Priority = pointer.Grabbed
|
||||
}
|
||||
|
||||
// Hit-test to deliver Enter/Leave events. Consider non-mouse
|
||||
// events leaving when they're Released.
|
||||
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]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
ev := evTemplate
|
||||
ev.Hit = q.hit(h.area, evTemplate.Position)
|
||||
ev.Position = h.transform.Invert().Transform(evTemplate.Position)
|
||||
ev.Type = evType
|
||||
events.Add(newH, ev)
|
||||
|
||||
e.Position = h.transform.Invert().Transform(e.Position)
|
||||
|
||||
switch {
|
||||
case !hit && entered != -1:
|
||||
p.entered = append(p.entered[:entered], p.entered[entered+1:]...)
|
||||
e.Type = pointer.Leave
|
||||
events.Add(k, e)
|
||||
case hit && entered == -1:
|
||||
p.entered = append(p.entered, k)
|
||||
e.Type = pointer.Enter
|
||||
events.Add(k, e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+53
-24
@@ -5,6 +5,7 @@ package router
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"gioui.org/f32"
|
||||
@@ -344,6 +345,44 @@ func TestPointerActiveInputDisappears(t *testing.T) {
|
||||
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
|
||||
// rectangular area.
|
||||
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)
|
||||
}
|
||||
|
||||
// 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
|
||||
// panic if some are not.
|
||||
func toTypes(events []event.Event) []pointer.Type {
|
||||
out := make([]pointer.Type, len(events))
|
||||
for i, event := range events {
|
||||
out[i] = event.(pointer.Event).Type
|
||||
func pointerTypes(events []event.Event) []pointer.Type {
|
||||
var types []pointer.Type
|
||||
for _, e := range events {
|
||||
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
|
||||
// in the provided order
|
||||
func assertEventSequence(t *testing.T, actualEvents []event.Event, expected ...pointer.Type) {
|
||||
if len(actualEvents) != len(expected) {
|
||||
t.Errorf("expected %v events, got %v", expected, toTypes(actualEvents))
|
||||
}
|
||||
for i, event := range actualEvents {
|
||||
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
|
||||
}
|
||||
// in the provided order.
|
||||
func assertEventSequence(t *testing.T, events []event.Event, expected ...pointer.Type) {
|
||||
t.Helper()
|
||||
got := pointerTypes(events)
|
||||
if !reflect.DeepEqual(got, expected) {
|
||||
t.Errorf("expected %v events, got %v", expected, got)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user