io: [API] introduce event filters; convert pointer input to use them

Instead of having to supply the predicates for event filtering at the
time of layout, the new Filter type allows widgets to filter at the time
of calling Source.Events. There is then only the need for a single input
op type, in package event.

Filters most importantly allow the use of one tag for several event types,
and we can define that a widget w has &w as its primary tag, by convention.
This allows the replacement of per-widget Focus methods with direct uses
of FocusCmd{&w}, and the later addition of Source.Focused(&w) queries.

Note that the TestCursor test needed restructuring to avoid its use of
InputOps.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2023-10-16 17:57:27 -05:00
parent d2085ab7c5
commit ef8171b971
19 changed files with 521 additions and 504 deletions
+3 -3
View File
@@ -29,8 +29,8 @@ func TestClipboardDuplicateEvent(t *testing.T) {
}
router.Queue(event)
assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), true)
assertClipboardEvent(t, router.Events(&handler[1]), true)
assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true)
assertClipboardEvent(t, router.Events(&handler[1], transfer.TargetFilter{Type: "application/text"}), true)
ops.Reset()
// No ReadCmd
@@ -81,7 +81,7 @@ func TestQueueProcessReadClipboard(t *testing.T) {
}
router.Queue(event)
assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), true)
assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true)
ops.Reset()
// No ReadCmd
+15 -14
View File
@@ -269,25 +269,24 @@ func TestFocusScroll(t *testing.T) {
r := new(Router)
h := new(int)
f := pointer.Filter{
Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
}
r.Events(h, f)
parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{
Tag: h,
Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
}.Add(ops)
event.InputOp(ops, h)
// Test that h is scrolled even if behind another handler.
pointer.InputOp{
Tag: new(int),
}.Add(ops)
event.InputOp(ops, new(int))
cl.Pop()
parent.Pop()
r.Frame(ops)
r.MoveFocus(key.FocusLeft)
r.RevealFocus(image.Rect(0, 0, 15, 40))
evts := r.Events(h)
evts := r.Events(h, f)
assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
}
@@ -296,18 +295,20 @@ func TestFocusClick(t *testing.T) {
r := new(Router)
h := new(int)
f := pointer.Filter{
Kinds: pointer.Press | pointer.Release,
}
assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Cancel)
cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{
Tag: h,
Kinds: pointer.Press | pointer.Release,
}.Add(ops)
event.InputOp(ops, h)
cl.Pop()
r.Frame(ops)
r.MoveFocus(key.FocusLeft)
r.ClickFocus()
assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press, pointer.Release)
assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Press, pointer.Release)
}
func TestNoFocus(t *testing.T) {
+37 -26
View File
@@ -244,11 +244,20 @@ func (q *pointerQueue) handlerFor(tag event.Tag, events *handlerEvents) *pointer
h = &pointerHandler{
area: -1,
}
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
q.handlers[tag] = h
// Cancel handlers on (each) first appearance, but don't
// trigger redraw.
events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel})
}
if !h.active {
h.types = 0
h.scrollRange = image.Rectangle{}
h.sourceMimes = h.sourceMimes[:0]
h.targetMimes = h.targetMimes[:0]
}
h.active = true
return h
}
@@ -288,20 +297,17 @@ func (q *pointerQueue) grab(req pointer.GrabCmd, events *handlerEvents) {
}
}
func (c *pointerCollector) inputOp(op pointer.InputOp, events *handlerEvents) {
func (c *pointerCollector) inputOp(tag event.Tag, events *handlerEvents) {
areaID := c.currentArea()
area := &c.q.areas[areaID]
area.semantic.content.tag = op.Tag
if op.Kinds&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
}
if op.Kinds&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
area.semantic.valid = area.semantic.content.gestures != 0
h := c.newHandler(op.Tag, events)
h.types = h.types | op.Kinds
h.scrollRange = op.ScrollBounds
area.semantic.content.tag = tag
c.newHandler(tag, events)
}
func (q *pointerQueue) filterTag(tag event.Tag, f pointer.Filter, events *handlerEvents) {
h := q.handlerFor(tag, events)
h.types = h.types | f.Kinds
h.scrollRange = h.scrollRange.Union(f.ScrollBounds)
}
func (c *pointerCollector) semanticLabel(lbl string) {
@@ -345,14 +351,14 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
area.cursor = cursor
}
func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) {
h := c.newHandler(op.Tag, events)
h.sourceMimes = append(h.sourceMimes, op.Type)
func (q *pointerQueue) sourceFilter(tag event.Tag, f transfer.SourceFilter, events *handlerEvents) {
h := q.handlerFor(tag, events)
h.sourceMimes = append(h.sourceMimes, f.Type)
}
func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) {
h := c.newHandler(op.Tag, events)
h.targetMimes = append(h.targetMimes, op.Type)
func (q *pointerQueue) targetFilter(tag event.Tag, f transfer.TargetFilter, events *handlerEvents) {
h := q.handlerFor(tag, events)
h.targetMimes = append(h.targetMimes, f.Type)
}
func (q *pointerQueue) offerData(req transfer.OfferCmd, events *handlerEvents) {
@@ -571,16 +577,9 @@ func (q *pointerQueue) hit(areaIdx int, p f32.Point) (bool, pointer.Cursor) {
}
func (q *pointerQueue) reset() {
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
for _, h := range q.handlers {
// Reset handler.
h.active = false
h.area = -1
h.types = 0
h.sourceMimes = h.sourceMimes[:0]
h.targetMimes = h.targetMimes[:0]
}
q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0]
@@ -612,6 +611,18 @@ func (q *pointerQueue) Frame(events *handlerEvents) {
if !h.active {
q.dropHandler(nil, k)
delete(q.handlers, k)
continue
}
h.active = false
if h.area != -1 {
area := &q.areas[h.area]
if h.types&(pointer.Press|pointer.Release) != 0 {
area.semantic.content.gestures |= ClickGesture
}
if h.types&pointer.Scroll != 0 {
area.semantic.content.gestures |= ScrollGesture
}
area.semantic.valid = area.semantic.content.gestures != 0
}
}
for i := range q.pointers {
@@ -669,7 +680,7 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
continue
}
h := q.handlers[n.tag]
if e.Kind&h.types == 0 {
if h == nil || e.Kind&h.types == 0 {
continue
}
e := e
+245 -260
View File
File diff suppressed because it is too large Load Diff
+67 -38
View File
@@ -119,16 +119,27 @@ func (s Source) Enabled() bool {
return s.r != nil
}
// Events returns the available events for the handler tag.
func (s Source) Events(k event.Tag) []event.Event {
// Events returns the events for the handler tag that matches one
// or more of filters.
func (s Source) Events(k event.Tag, filters ...event.Filter) []event.Event {
if !s.Enabled() {
return nil
}
return s.r.Events(k)
return s.r.Events(k, filters...)
}
func (q *Router) Events(k event.Tag) []event.Event {
events := q.handlers.Events(k)
func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
for _, f := range filters {
switch f := f.(type) {
case pointer.Filter:
q.pointer.queue.filterTag(k, f, &q.handlers)
case transfer.SourceFilter:
q.pointer.queue.sourceFilter(k, f, &q.handlers)
case transfer.TargetFilter:
q.pointer.queue.targetFilter(k, f, &q.handlers)
}
}
events := q.handlers.Events(k, filters...)
return events
}
@@ -424,7 +435,6 @@ func (q *Router) collect() {
kq := &q.key.queue
q.key.queue.Reset()
var t f32.Affine2D
bo := binary.LittleEndian
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
switch ops.OpType(encOp.Data[0]) {
case ops.TypeInvalidate:
@@ -464,42 +474,18 @@ func (q *Router) collect() {
q.transStack = q.transStack[:n-1]
pc.setTrans(t)
case ops.TypeInput:
tag := encOp.Refs[0].(event.Tag)
pc.inputOp(tag, &q.handlers)
// Pointer ops.
case ops.TypePass:
pc.pass()
case ops.TypePopPass:
pc.popPass()
case ops.TypePointerInput:
op := pointer.InputOp{
Tag: encOp.Refs[0].(event.Tag),
Kinds: pointer.Kind(bo.Uint16(encOp.Data[1:])),
ScrollBounds: image.Rectangle{
Min: image.Point{
X: int(int32(bo.Uint32(encOp.Data[3:]))),
Y: int(int32(bo.Uint32(encOp.Data[7:]))),
},
Max: image.Point{
X: int(int32(bo.Uint32(encOp.Data[11:]))),
Y: int(int32(bo.Uint32(encOp.Data[15:]))),
},
},
}
pc.inputOp(op, &q.handlers)
case ops.TypeCursor:
name := pointer.Cursor(encOp.Data[1])
pc.cursor(name)
case ops.TypeSource:
op := transfer.SourceOp{
Tag: encOp.Refs[0].(event.Tag),
Type: encOp.Refs[1].(string),
}
pc.sourceOp(op, &q.handlers)
case ops.TypeTarget:
op := transfer.TargetOp{
Tag: encOp.Refs[0].(event.Tag),
Type: encOp.Refs[1].(string),
}
pc.targetOp(op, &q.handlers)
case ops.TypeActionInput:
act := system.Action(encOp.Data[1])
pc.actionInputOp(act)
@@ -570,12 +556,55 @@ func (h *handlerEvents) HadEvents() bool {
return u
}
func (h *handlerEvents) Events(k event.Tag) []event.Event {
func (h *handlerEvents) Events(k event.Tag, filters ...event.Filter) []event.Event {
var filtered []event.Event
if events, ok := h.handlers[k]; ok {
h.handlers[k] = h.handlers[k][:0]
return events
i := 0
for i < len(events) {
e := events[i]
if filtersMatches(filters, e) {
filtered = append(filtered, e)
events = append(events[:i], events[i+1:]...)
} else {
i++
}
}
h.handlers[k] = events
}
return nil
return filtered
}
func filtersMatches(filters []event.Filter, e event.Event) bool {
switch e := e.(type) {
case pointer.Event:
for _, f := range filters {
if f, ok := f.(pointer.Filter); ok && f.Kinds&e.Kind == e.Kind {
return true
}
}
case transfer.CancelEvent, transfer.InitiateEvent:
for _, f := range filters {
switch f.(type) {
case transfer.SourceFilter, transfer.TargetFilter:
return true
}
}
case transfer.RequestEvent:
for _, f := range filters {
if f, ok := f.(transfer.SourceFilter); ok && f.Type == e.Type {
return true
}
}
case transfer.DataEvent:
for _, f := range filters {
if f, ok := f.(transfer.TargetFilter); ok && f.Type == e.Type {
return true
}
}
default:
return true
}
return false
}
func (h *handlerEvents) Clear() {
+24
View File
@@ -0,0 +1,24 @@
// SPDX-License-Identifier: Unlicense OR MIT
package input
import (
"testing"
"gioui.org/io/pointer"
)
func TestNoFilterAllocs(t *testing.T) {
b := testing.Benchmark(func(b *testing.B) {
var r Router
s := r.Source()
b.ReportAllocs()
b.ResetTimer()
for i := 0; i < b.N; i++ {
s.Events(nil, pointer.Filter{})
}
})
if allocs := b.AllocsPerOp(); allocs != 0 {
t.Fatalf("expected 0 AllocsPerOp, got %d", allocs)
}
}
+7 -1
View File
@@ -9,6 +9,7 @@ import (
"testing"
"gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/pointer"
"gioui.org/io/semantic"
"gioui.org/op"
@@ -74,13 +75,18 @@ func TestSemanticTree(t *testing.T) {
func TestSemanticDescription(t *testing.T) {
var ops op.Ops
pointer.InputOp{Tag: new(int), Kinds: pointer.Press | pointer.Release}.Add(&ops)
h := new(int)
event.InputOp(&ops, h)
semantic.DescriptionOp("description").Add(&ops)
semantic.LabelOp("label").Add(&ops)
semantic.Button.Add(&ops)
semantic.EnabledOp(false).Add(&ops)
semantic.SelectedOp(true).Add(&ops)
var r Router
r.Events(h, pointer.Filter{
Kinds: pointer.Press | pointer.Release,
})
r.Frame(&ops)
tree := r.AppendSemantics(nil)
got := tree[0].Desc