io/router: fix system action routing logic

When running ActionAt, the router used to only consider the topmost clip area, even
if that clip area had no input handlers attached whatsoever. This change updates the
logic for that test to use the same traversal as normal event handling, ensuring that
action inputs behave intuitively like any other pointer input area. Included is a test
catching the problematic behavior that prompted this change.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
This commit is contained in:
Chris Waldon
2023-08-18 10:57:55 -04:00
committed by Elias Naur
parent 0e77a2b521
commit e9cb0b326d
2 changed files with 59 additions and 13 deletions
+23 -13
View File
@@ -432,17 +432,15 @@ func (q *pointerQueue) semanticIDFor(content semanticContent) SemanticID {
return id.id return id.id
} }
func (q *pointerQueue) ActionAt(pos f32.Point) (system.Action, bool) { func (q *pointerQueue) ActionAt(pos f32.Point) (action system.Action, hasAction bool) {
for i := len(q.hitTree) - 1; i >= 0; i-- { q.hitTest(pos, func(n *hitNode) {
n := &q.hitTree[i]
hit, _ := q.hit(n.area, pos)
if !hit {
continue
}
area := q.areas[n.area] area := q.areas[n.area]
return area.action, area.action != 0 if area.action != 0 {
} action = area.action
return 0, false hasAction = true
}
})
return action, hasAction
} }
func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) { func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
@@ -461,10 +459,14 @@ func (q *pointerQueue) SemanticAt(pos f32.Point) (SemanticID, bool) {
return 0, false return 0, false
} }
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) { // hitTest searches the hit tree for nodes matching pos. Any node matching pos will
// have the onNode func invoked on it to allow the caller to extract whatever information
// is necessary for further processing. Providing this algorithm in this generic way
// allows normal event routing and system action event routing to share the same traversal
// logic even though they are interested in different aspects of hit nodes.
func (q *pointerQueue) hitTest(pos f32.Point, onNode func(*hitNode)) pointer.Cursor {
// Track whether we're passing through hits. // Track whether we're passing through hits.
pass := true pass := true
hits := q.scratch[:0]
idx := len(q.hitTree) - 1 idx := len(q.hitTree) - 1
cursor := pointer.CursorDefault cursor := pointer.CursorDefault
for idx >= 0 { for idx >= 0 {
@@ -483,12 +485,20 @@ func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
} else { } else {
idx = n.next idx = n.next
} }
onNode(n)
}
return cursor
}
func (q *pointerQueue) opHit(pos f32.Point) ([]event.Tag, pointer.Cursor) {
hits := q.scratch[:0]
cursor := q.hitTest(pos, func(n *hitNode) {
if n.tag != nil { if n.tag != nil {
if _, exists := q.handlers[n.tag]; exists { if _, exists := q.handlers[n.tag]; exists {
hits = addHandler(hits, n.tag) hits = addHandler(hits, n.tag)
} }
} }
} })
q.scratch = hits[:0] q.scratch = hits[:0]
return hits, cursor return hits, cursor
} }
+36
View File
@@ -14,6 +14,7 @@ import (
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/system"
"gioui.org/io/transfer" "gioui.org/io/transfer"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
@@ -221,6 +222,30 @@ func TestPointerTypes(t *testing.T) {
assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release) assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release)
} }
func TestPointerSystemAction(t *testing.T) {
t.Run("simple", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
})
t.Run("covered by another clip", func(t *testing.T) {
var ops op.Ops
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
system.ActionInputOp(system.ActionMove).Add(&ops)
clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop()
r1.Pop()
var r Router
r.Frame(&ops)
assertActionAt(t, r, f32.Pt(50, 50), system.ActionMove)
})
}
func TestPointerPriority(t *testing.T) { func TestPointerPriority(t *testing.T) {
handler1 := new(int) handler1 := new(int)
handler2 := new(int) handler2 := new(int)
@@ -1231,6 +1256,17 @@ func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
} }
} }
// assertActionAt checks that the router has a system action of the expected type at point.
func assertActionAt(t *testing.T, q Router, point f32.Point, expected system.Action) {
t.Helper()
action, ok := q.ActionAt(point)
if !ok {
t.Errorf("expected action %v at %v, got no action", expected, point)
} else if action != expected {
t.Errorf("expected action %v at %v, got %v", expected, point, action)
}
}
func BenchmarkRouterAdd(b *testing.B) { func BenchmarkRouterAdd(b *testing.B) {
// Set this to the number of overlapping handlers that you want to // Set this to the number of overlapping handlers that you want to
// evaluate performance for. Typical values for the example applications // evaluate performance for. Typical values for the example applications