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
+20 -34
View File
@@ -17,6 +17,7 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/fling" "gioui.org/internal/fling"
"gioui.org/io/event"
"gioui.org/io/input" "gioui.org/io/input"
"gioui.org/io/key" "gioui.org/io/key"
"gioui.org/io/pointer" "gioui.org/io/pointer"
@@ -37,15 +38,12 @@ type Hover struct {
// Add the gesture to detect hovering over the current pointer area. // Add the gesture to detect hovering over the current pointer area.
func (h *Hover) Add(ops *op.Ops) { func (h *Hover) Add(ops *op.Ops) {
pointer.InputOp{ event.InputOp(ops, h)
Tag: h,
Kinds: pointer.Enter | pointer.Leave,
}.Add(ops)
} }
// Update state and report whether a pointer is inside the area. // Update state and report whether a pointer is inside the area.
func (h *Hover) Update(q input.Source) bool { func (h *Hover) Update(q input.Source) bool {
for _, ev := range q.Events(h) { for _, ev := range q.Events(h, pointer.Filter{Kinds: pointer.Enter | pointer.Leave}) {
e, ok := ev.(pointer.Event) e, ok := ev.(pointer.Event)
if !ok { if !ok {
continue continue
@@ -114,7 +112,6 @@ type Drag struct {
// movements as well as drag and fling touch gestures. // movements as well as drag and fling touch gestures.
type Scroll struct { type Scroll struct {
dragging bool dragging bool
axis Axis
estimator fling.Extrapolation estimator fling.Extrapolation
flinger fling.Animation flinger fling.Animation
pid pointer.ID pid pointer.ID
@@ -159,10 +156,7 @@ const touchSlop = unit.Dp(3)
// Add the handler to the operation list to receive click events. // Add the handler to the operation list to receive click events.
func (c *Click) Add(ops *op.Ops) { func (c *Click) Add(ops *op.Ops) {
pointer.InputOp{ event.InputOp(ops, c)
Tag: c,
Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave,
}.Add(ops)
} }
// Hovered returns whether a pointer is inside the area. // Hovered returns whether a pointer is inside the area.
@@ -178,7 +172,7 @@ func (c *Click) Pressed() bool {
// Update state and return the click events. // Update state and return the click events.
func (c *Click) Update(q input.Source) []ClickEvent { func (c *Click) Update(q input.Source) []ClickEvent {
var events []ClickEvent var events []ClickEvent
for _, evt := range q.Events(c) { for _, evt := range q.Events(c, pointer.Filter{Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave}) {
e, ok := evt.(pointer.Event) e, ok := evt.(pointer.Event)
if !ok { if !ok {
continue continue
@@ -248,13 +242,8 @@ func (ClickEvent) ImplementsEvent() {}
// Add the handler to the operation list to receive scroll events. // Add the handler to the operation list to receive scroll events.
// The bounds variable refers to the scrolling boundaries // The bounds variable refers to the scrolling boundaries
// as defined in io/pointer.InputOp. // as defined in io/pointer.InputOp.
func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) { func (s *Scroll) Add(ops *op.Ops) {
oph := pointer.InputOp{ event.InputOp(ops, s)
Tag: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
ScrollBounds: bounds,
}
oph.Add(ops)
if s.flinger.Active() { if s.flinger.Active() {
op.InvalidateOp{}.Add(ops) op.InvalidateOp{}.Add(ops)
} }
@@ -266,13 +255,13 @@ func (s *Scroll) Stop() {
} }
// Update state and report the scroll distance along axis. // Update state and report the scroll distance along axis.
func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis) int { func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int {
if s.axis != axis {
s.axis = axis
return 0
}
total := 0 total := 0
for _, evt := range q.Events(s) { f := pointer.Filter{
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
ScrollBounds: bounds,
}
for _, evt := range q.Events(s, f) {
e, ok := evt.(pointer.Event) e, ok := evt.(pointer.Event)
if !ok { if !ok {
continue continue
@@ -289,7 +278,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis)
} }
s.Stop() s.Stop()
s.estimator = fling.Extrapolation{} s.estimator = fling.Extrapolation{}
v := s.val(e.Position) v := s.val(axis, e.Position)
s.last = int(math.Round(float64(v))) s.last = int(math.Round(float64(v)))
s.estimator.Sample(e.Time, v) s.estimator.Sample(e.Time, v)
s.dragging = true s.dragging = true
@@ -306,7 +295,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis)
case pointer.Cancel: case pointer.Cancel:
s.dragging = false s.dragging = false
case pointer.Scroll: case pointer.Scroll:
switch s.axis { switch axis {
case Horizontal: case Horizontal:
s.scroll += e.Scroll.X s.scroll += e.Scroll.X
case Vertical: case Vertical:
@@ -319,7 +308,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis)
if !s.dragging || s.pid != e.PointerID { if !s.dragging || s.pid != e.PointerID {
continue continue
} }
val := s.val(e.Position) val := s.val(axis, e.Position)
s.estimator.Sample(e.Time, val) s.estimator.Sample(e.Time, val)
v := int(math.Round(float64(val))) v := int(math.Round(float64(val)))
dist := s.last - v dist := s.last - v
@@ -338,8 +327,8 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis)
return total return total
} }
func (s *Scroll) val(p f32.Point) float32 { func (s *Scroll) val(axis Axis, p f32.Point) float32 {
if s.axis == Horizontal { if axis == Horizontal {
return p.X return p.X
} else { } else {
return p.Y return p.Y
@@ -360,16 +349,13 @@ func (s *Scroll) State() ScrollState {
// Add the handler to the operation list to receive drag events. // Add the handler to the operation list to receive drag events.
func (d *Drag) Add(ops *op.Ops) { func (d *Drag) Add(ops *op.Ops) {
pointer.InputOp{ event.InputOp(ops, d)
Tag: d,
Kinds: pointer.Press | pointer.Drag | pointer.Release,
}.Add(ops)
} }
// Update state and return the drag events. // Update state and return the drag events.
func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) []pointer.Event { func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) []pointer.Event {
var events []pointer.Event var events []pointer.Event
for _, e := range q.Events(d) { for _, e := range q.Events(d, pointer.Filter{Kinds: pointer.Press | pointer.Drag | pointer.Release}) {
e, ok := e.(pointer.Event) e, ok := e.(pointer.Event)
if !ok { if !ok {
continue continue
+2
View File
@@ -23,6 +23,7 @@ func TestHover(t *testing.T) {
h.Add(ops) h.Add(ops)
stack.Pop() stack.Pop()
r := new(input.Router) r := new(input.Router)
h.Update(r.Source())
r.Frame(ops) r.Frame(ops)
r.Queue( r.Queue(
@@ -72,6 +73,7 @@ func TestMouseClicks(t *testing.T) {
click.Add(&ops) click.Add(&ops)
var r input.Router var r input.Router
click.Update(r.Source())
r.Frame(&ops) r.Frame(&ops)
r.Queue(tc.events...) r.Queue(tc.events...)
+5 -15
View File
@@ -62,9 +62,7 @@ const (
TypeLinearGradient TypeLinearGradient
TypePass TypePass
TypePopPass TypePopPass
TypePointerInput TypeInput
TypeSource
TypeTarget
TypeKeyInput TypeKeyInput
TypeSave TypeSave
TypeLoad TypeLoad
@@ -140,9 +138,7 @@ const (
TypeLinearGradientLen = 1 + 8*2 + 4*2 TypeLinearGradientLen = 1 + 8*2 + 4*2
TypePassLen = 1 TypePassLen = 1
TypePopPassLen = 1 TypePopPassLen = 1
TypePointerInputLen = 1 + 1*2 + 2*4 + 2*4 TypeInputLen = 1
TypeSourceLen = 1
TypeTargetLen = 1
TypeKeyInputLen = 1 + 1 TypeKeyInputLen = 1 + 1
TypeSaveLen = 1 + 4 TypeSaveLen = 1 + 4
TypeLoadLen = 1 + 4 TypeLoadLen = 1 + 4
@@ -416,9 +412,7 @@ var opProps = [0x100]opProp{
TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0}, TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0},
TypePass: {Size: TypePassLen, NumRefs: 0}, TypePass: {Size: TypePassLen, NumRefs: 0},
TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, TypePopPass: {Size: TypePopPassLen, NumRefs: 0},
TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1}, TypeInput: {Size: TypeInputLen, NumRefs: 1},
TypeSource: {Size: TypeSourceLen, NumRefs: 2},
TypeTarget: {Size: TypeTargetLen, NumRefs: 2},
TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2},
TypeSave: {Size: TypeSaveLen, NumRefs: 0}, TypeSave: {Size: TypeSaveLen, NumRefs: 0},
TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0},
@@ -479,12 +473,8 @@ func (t OpType) String() string {
return "Pass" return "Pass"
case TypePopPass: case TypePopPass:
return "PopPass" return "PopPass"
case TypePointerInput: case TypeInput:
return "PointerInput" return "Input"
case TypeSource:
return "Source"
case TypeTarget:
return "Target"
case TypeKeyInput: case TypeKeyInput:
return "KeyInput" return "KeyInput"
case TypeSave: case TypeSave:
+20
View File
@@ -3,6 +3,11 @@
// Package event contains types for event handling. // Package event contains types for event handling.
package event package event
import (
"gioui.org/internal/ops"
"gioui.org/op"
)
// Tag is the stable identifier for an event handler. // Tag is the stable identifier for an event handler.
// For a handler h, the tag is typically &h. // For a handler h, the tag is typically &h.
type Tag interface{} type Tag interface{}
@@ -11,3 +16,18 @@ type Tag interface{}
type Event interface { type Event interface {
ImplementsEvent() ImplementsEvent()
} }
// Filter represents a filter for [Event] types.
type Filter interface {
ImplementsFilter()
}
// InputOp declares a tag for input routing at the current transformation
// and clip area hierarchy. It panics if tag is nil.
func InputOp(o *op.Ops, tag Tag) {
if tag == nil {
panic("Tag must be non-nil")
}
data := ops.Write1(&o.Internal, ops.TypeInputLen, tag)
data[0] = byte(ops.TypeInput)
}
+3 -3
View File
@@ -29,8 +29,8 @@ func TestClipboardDuplicateEvent(t *testing.T) {
} }
router.Queue(event) router.Queue(event)
assertClipboardReadCmd(t, router, 0) assertClipboardReadCmd(t, router, 0)
assertClipboardEvent(t, router.Events(&handler[0]), true) assertClipboardEvent(t, router.Events(&handler[0], transfer.TargetFilter{Type: "application/text"}), true)
assertClipboardEvent(t, router.Events(&handler[1]), true) assertClipboardEvent(t, router.Events(&handler[1], transfer.TargetFilter{Type: "application/text"}), true)
ops.Reset() ops.Reset()
// No ReadCmd // No ReadCmd
@@ -81,7 +81,7 @@ func TestQueueProcessReadClipboard(t *testing.T) {
} }
router.Queue(event) router.Queue(event)
assertClipboardReadCmd(t, router, 0) 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() ops.Reset()
// No ReadCmd // No ReadCmd
+15 -14
View File
@@ -269,25 +269,24 @@ func TestFocusScroll(t *testing.T) {
r := new(Router) r := new(Router)
h := new(int) 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) parent := clip.Rect(image.Rect(1, 1, 14, 39)).Push(ops)
cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops) cl := clip.Rect(image.Rect(10, -20, 20, 30)).Push(ops)
key.InputOp{Tag: h}.Add(ops) key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{ event.InputOp(ops, h)
Tag: h,
Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100),
}.Add(ops)
// Test that h is scrolled even if behind another handler. // Test that h is scrolled even if behind another handler.
pointer.InputOp{ event.InputOp(ops, new(int))
Tag: new(int),
}.Add(ops)
cl.Pop() cl.Pop()
parent.Pop() parent.Pop()
r.Frame(ops) r.Frame(ops)
r.MoveFocus(key.FocusLeft) r.MoveFocus(key.FocusLeft)
r.RevealFocus(image.Rect(0, 0, 15, 40)) 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)) assertScrollEvent(t, evts[len(evts)-1], f32.Pt(6, -9))
} }
@@ -296,18 +295,20 @@ func TestFocusClick(t *testing.T) {
r := new(Router) r := new(Router)
h := new(int) 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) cl := clip.Rect(image.Rect(0, 0, 10, 10)).Push(ops)
key.InputOp{Tag: h}.Add(ops) key.InputOp{Tag: h}.Add(ops)
pointer.InputOp{ event.InputOp(ops, h)
Tag: h,
Kinds: pointer.Press | pointer.Release,
}.Add(ops)
cl.Pop() cl.Pop()
r.Frame(ops) r.Frame(ops)
r.MoveFocus(key.FocusLeft) r.MoveFocus(key.FocusLeft)
r.ClickFocus() 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) { 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{ h = &pointerHandler{
area: -1, area: -1,
} }
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
q.handlers[tag] = h q.handlers[tag] = h
// Cancel handlers on (each) first appearance, but don't // Cancel handlers on (each) first appearance, but don't
// trigger redraw. // trigger redraw.
events.AddNoRedraw(tag, pointer.Event{Kind: pointer.Cancel}) 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 h.active = true
return h 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() areaID := c.currentArea()
area := &c.q.areas[areaID] area := &c.q.areas[areaID]
area.semantic.content.tag = op.Tag area.semantic.content.tag = tag
if op.Kinds&(pointer.Press|pointer.Release) != 0 { c.newHandler(tag, events)
area.semantic.content.gestures |= ClickGesture }
}
if op.Kinds&pointer.Scroll != 0 { func (q *pointerQueue) filterTag(tag event.Tag, f pointer.Filter, events *handlerEvents) {
area.semantic.content.gestures |= ScrollGesture h := q.handlerFor(tag, events)
} h.types = h.types | f.Kinds
area.semantic.valid = area.semantic.content.gestures != 0 h.scrollRange = h.scrollRange.Union(f.ScrollBounds)
h := c.newHandler(op.Tag, events)
h.types = h.types | op.Kinds
h.scrollRange = op.ScrollBounds
} }
func (c *pointerCollector) semanticLabel(lbl string) { func (c *pointerCollector) semanticLabel(lbl string) {
@@ -345,14 +351,14 @@ func (c *pointerCollector) cursor(cursor pointer.Cursor) {
area.cursor = cursor area.cursor = cursor
} }
func (c *pointerCollector) sourceOp(op transfer.SourceOp, events *handlerEvents) { func (q *pointerQueue) sourceFilter(tag event.Tag, f transfer.SourceFilter, events *handlerEvents) {
h := c.newHandler(op.Tag, events) h := q.handlerFor(tag, events)
h.sourceMimes = append(h.sourceMimes, op.Type) h.sourceMimes = append(h.sourceMimes, f.Type)
} }
func (c *pointerCollector) targetOp(op transfer.TargetOp, events *handlerEvents) { func (q *pointerQueue) targetFilter(tag event.Tag, f transfer.TargetFilter, events *handlerEvents) {
h := c.newHandler(op.Tag, events) h := q.handlerFor(tag, events)
h.targetMimes = append(h.targetMimes, op.Type) h.targetMimes = append(h.targetMimes, f.Type)
} }
func (q *pointerQueue) offerData(req transfer.OfferCmd, events *handlerEvents) { 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() { func (q *pointerQueue) reset() {
if q.handlers == nil {
q.handlers = make(map[event.Tag]*pointerHandler)
}
for _, h := range q.handlers { for _, h := range q.handlers {
// Reset handler. // Reset handler.
h.active = false
h.area = -1 h.area = -1
h.types = 0
h.sourceMimes = h.sourceMimes[:0]
h.targetMimes = h.targetMimes[:0]
} }
q.hitTree = q.hitTree[:0] q.hitTree = q.hitTree[:0]
q.areas = q.areas[:0] q.areas = q.areas[:0]
@@ -612,6 +611,18 @@ func (q *pointerQueue) Frame(events *handlerEvents) {
if !h.active { if !h.active {
q.dropHandler(nil, k) q.dropHandler(nil, k)
delete(q.handlers, 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 { for i := range q.pointers {
@@ -669,7 +680,7 @@ func (q *pointerQueue) Deliver(areaIdx int, e pointer.Event, events *handlerEven
continue continue
} }
h := q.handlers[n.tag] h := q.handlers[n.tag]
if e.Kind&h.types == 0 { if h == nil || e.Kind&h.types == 0 {
continue continue
} }
e := e 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 return s.r != nil
} }
// Events returns the available events for the handler tag. // Events returns the events for the handler tag that matches one
func (s Source) Events(k event.Tag) []event.Event { // or more of filters.
func (s Source) Events(k event.Tag, filters ...event.Filter) []event.Event {
if !s.Enabled() { if !s.Enabled() {
return nil return nil
} }
return s.r.Events(k) return s.r.Events(k, filters...)
} }
func (q *Router) Events(k event.Tag) []event.Event { func (q *Router) Events(k event.Tag, filters ...event.Filter) []event.Event {
events := q.handlers.Events(k) 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 return events
} }
@@ -424,7 +435,6 @@ func (q *Router) collect() {
kq := &q.key.queue kq := &q.key.queue
q.key.queue.Reset() q.key.queue.Reset()
var t f32.Affine2D var t f32.Affine2D
bo := binary.LittleEndian
for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() { for encOp, ok := q.reader.Decode(); ok; encOp, ok = q.reader.Decode() {
switch ops.OpType(encOp.Data[0]) { switch ops.OpType(encOp.Data[0]) {
case ops.TypeInvalidate: case ops.TypeInvalidate:
@@ -464,42 +474,18 @@ func (q *Router) collect() {
q.transStack = q.transStack[:n-1] q.transStack = q.transStack[:n-1]
pc.setTrans(t) pc.setTrans(t)
case ops.TypeInput:
tag := encOp.Refs[0].(event.Tag)
pc.inputOp(tag, &q.handlers)
// Pointer ops. // Pointer ops.
case ops.TypePass: case ops.TypePass:
pc.pass() pc.pass()
case ops.TypePopPass: case ops.TypePopPass:
pc.popPass() 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: case ops.TypeCursor:
name := pointer.Cursor(encOp.Data[1]) name := pointer.Cursor(encOp.Data[1])
pc.cursor(name) 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: case ops.TypeActionInput:
act := system.Action(encOp.Data[1]) act := system.Action(encOp.Data[1])
pc.actionInputOp(act) pc.actionInputOp(act)
@@ -570,12 +556,55 @@ func (h *handlerEvents) HadEvents() bool {
return u 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 { if events, ok := h.handlers[k]; ok {
h.handlers[k] = h.handlers[k][:0] i := 0
return events 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() { 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" "testing"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/semantic" "gioui.org/io/semantic"
"gioui.org/op" "gioui.org/op"
@@ -74,13 +75,18 @@ func TestSemanticTree(t *testing.T) {
func TestSemanticDescription(t *testing.T) { func TestSemanticDescription(t *testing.T) {
var ops op.Ops 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.DescriptionOp("description").Add(&ops)
semantic.LabelOp("label").Add(&ops) semantic.LabelOp("label").Add(&ops)
semantic.Button.Add(&ops) semantic.Button.Add(&ops)
semantic.EnabledOp(false).Add(&ops) semantic.EnabledOp(false).Add(&ops)
semantic.SelectedOp(true).Add(&ops) semantic.SelectedOp(true).Add(&ops)
var r Router var r Router
r.Events(h, pointer.Filter{
Kinds: pointer.Press | pointer.Release,
})
r.Frame(&ops) r.Frame(&ops)
tree := r.AppendSemantics(nil) tree := r.AppendSemantics(nil)
got := tree[0].Desc got := tree[0].Desc
+5 -28
View File
@@ -3,8 +3,6 @@
package pointer package pointer
import ( import (
"encoding/binary"
"fmt"
"image" "image"
"strings" "strings"
"time" "time"
@@ -56,11 +54,9 @@ type PassStack struct {
macroID uint32 macroID uint32
} }
// InputOp declares an input handler ready for pointer // Filter matches [Event]s.
// events. type Filter struct {
type InputOp struct { // Kinds is a bitwise-or of event types to match.
Tag event.Tag
// Kinds is a bitwise-or of event types to receive.
Kinds Kind Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both // ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy // axes. Specifically, any Event e delivered to Tag will satisfy
@@ -240,27 +236,6 @@ func (op Cursor) Add(o *op.Ops) {
data[1] = byte(op) data[1] = byte(op)
} }
// Add panics if the scroll range does not contain zero.
func (op InputOp) Add(o *op.Ops) {
if op.Tag == nil {
panic("Tag must be non-nil")
}
if b := op.ScrollBounds; b.Min.X > 0 || b.Max.X < 0 || b.Min.Y > 0 || b.Max.Y < 0 {
panic(fmt.Errorf("invalid scroll range value %v", b))
}
if op.Kinds>>16 > 0 {
panic(fmt.Errorf("value in Types overflows uint16"))
}
data := ops.Write1(&o.Internal, ops.TypePointerInputLen, op.Tag)
data[0] = byte(ops.TypePointerInput)
bo := binary.LittleEndian
bo.PutUint16(data[1:], uint16(op.Kinds))
bo.PutUint32(data[3:], uint32(op.ScrollBounds.Min.X))
bo.PutUint32(data[7:], uint32(op.ScrollBounds.Min.Y))
bo.PutUint32(data[11:], uint32(op.ScrollBounds.Max.X))
bo.PutUint32(data[15:], uint32(op.ScrollBounds.Max.Y))
}
func (t Kind) String() string { func (t Kind) String() string {
if t == Cancel { if t == Cancel {
return "Cancel" return "Cancel"
@@ -406,3 +381,5 @@ func (c Cursor) String() string {
func (Event) ImplementsEvent() {} func (Event) ImplementsEvent() {}
func (GrabCmd) ImplementsCommand() {} func (GrabCmd) ImplementsCommand() {}
func (Filter) ImplementsFilter() {}
+15 -25
View File
@@ -2,10 +2,10 @@
// //
// The transfer protocol is as follows: // The transfer protocol is as follows:
// //
// - Data sources use [SourceFilter] to receive [InitiateEvent]s when a drag // - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag
// is initiated, and [RequestEvent]s for each initiation of a data transfer. // is initiated, and an [RequestEvent] for each initiation of a data transfer.
// Sources respond to requests with [OfferCommand]. // Sources respond to requests with [OfferCmd].
// - Data targets use [TargetFilter] to receive [DataEvent]s for receiving data. // - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data.
// The target must close the data event after use. // The target must close the data event after use.
// //
// When a user initiates a pointer-guided drag and drop transfer, the // When a user initiates a pointer-guided drag and drop transfer, the
@@ -20,9 +20,7 @@ package transfer
import ( import (
"io" "io"
"gioui.org/internal/ops"
"gioui.org/io/event" "gioui.org/io/event"
"gioui.org/op"
) )
// OfferCmd is used by data sources as a response to a RequestEvent. // OfferCmd is used by data sources as a response to a RequestEvent.
@@ -34,38 +32,27 @@ type OfferCmd struct {
// Data contains the offered data. It is closed when the // Data contains the offered data. It is closed when the
// transfer is complete or cancelled. // transfer is complete or cancelled.
// Data must be kept valid until closed, and it may be used from // Data must be kept valid until closed, and it may be used from
// a goroutine separate from the one processing the frame.. // a goroutine separate from the one processing the frame.
Data io.ReadCloser Data io.ReadCloser
} }
func (OfferCmd) ImplementsCommand() {} func (OfferCmd) ImplementsCommand() {}
// SourceOp registers a tag as a data source for a MIME type. // SourceFilter filters for any [RequestEvent] that match a MIME type
// Use multiple SourceOps if a tag supports multiple types. // as well as [InitiateEvent] and [CancelEvent].
type SourceOp struct { // Use multiple filters to offer multiple types.
Tag event.Tag type SourceFilter struct {
// Type is the MIME type supported by this source. // Type is the MIME type supported by this source.
Type string Type string
} }
// TargetOp registers a tag as a data target. // TargetFilter filters for any [DataEvent] whose type matches a MIME type
// Use multiple TargetOps if a tag supports multiple types. // as well as [CancelEvent]. Use multiple filters to accept multiple types.
type TargetOp struct { type TargetFilter struct {
Tag event.Tag
// Type is the MIME type accepted by this target. // Type is the MIME type accepted by this target.
Type string Type string
} }
func (op SourceOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeSourceLen, op.Tag, op.Type)
data[0] = byte(ops.TypeSource)
}
func (op TargetOp) Add(o *op.Ops) {
data := ops.Write2(&o.Internal, ops.TypeTargetLen, op.Tag, op.Type)
data[0] = byte(ops.TypeTarget)
}
// RequestEvent requests data from a data source. The source must // RequestEvent requests data from a data source. The source must
// respond with an OfferCmd. // respond with an OfferCmd.
type RequestEvent struct { type RequestEvent struct {
@@ -99,3 +86,6 @@ type DataEvent struct {
} }
func (DataEvent) ImplementsEvent() {} func (DataEvent) ImplementsEvent() {}
func (SourceFilter) ImplementsFilter() {}
func (TargetFilter) ImplementsFilter() {}
+20 -20
View File
@@ -144,7 +144,25 @@ func (l *List) Dragging() bool {
} }
func (l *List) update(gtx Context) { func (l *List) update(gtx Context) {
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis)) min, max := int(-inf), int(inf)
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange)
l.scrollDelta = d l.scrollDelta = d
l.Position.Offset += d l.Position.Offset += d
} }
@@ -332,25 +350,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
call := macro.Stop() call := macro.Stop()
defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop()
min, max := int(-inf), int(inf) l.scroll.Add(ops)
if l.Position.First == 0 {
// Use the size of the invisible part as scroll boundary.
min = -l.Position.Offset
if min > 0 {
min = 0
}
}
if l.Position.First+l.Position.Count == l.len {
max = -l.Position.OffsetLast
if max < 0 {
max = 0
}
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
l.scroll.Add(ops, scrollRange)
call.Add(ops) call.Add(ops)
return Dimensions{Size: dims} return Dimensions{Size: dims}
+3 -5
View File
@@ -5,6 +5,7 @@ import (
"gioui.org/f32" "gioui.org/f32"
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/event"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/transfer" "gioui.org/io/transfer"
"gioui.org/layout" "gioui.org/layout"
@@ -31,10 +32,7 @@ func (d *Draggable) Layout(gtx layout.Context, w, drag layout.Widget) layout.Dim
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
d.drag.Add(gtx.Ops) d.drag.Add(gtx.Ops)
transfer.SourceOp{ event.InputOp(gtx.Ops, &d.handle)
Tag: &d.handle,
Type: d.Type,
}.Add(gtx.Ops)
stack.Pop() stack.Pop()
if drag != nil && d.drag.Pressed() { if drag != nil && d.drag.Pressed() {
@@ -67,7 +65,7 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) {
} }
d.pos = pos d.pos = pos
for _, ev := range gtx.Source.Events(&d.handle) { for _, ev := range gtx.Events(&d.handle, transfer.SourceFilter{Type: d.Type}) {
if e, ok := ev.(transfer.RequestEvent); ok { if e, ok := ev.(transfer.RequestEvent); ok {
return e.Type, true return e.Type, true
} }
+7 -5
View File
@@ -5,6 +5,7 @@ import (
"testing" "testing"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input" "gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/transfer" "gioui.org/io/transfer"
@@ -29,12 +30,11 @@ func TestDraggable(t *testing.T) {
return layout.Dimensions{Size: gtx.Constraints.Min} return layout.Dimensions{Size: gtx.Constraints.Min}
}, nil) }, nil)
stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops)
transfer.TargetOp{ event.InputOp(gtx.Ops, drag)
Tag: drag,
Type: drag.Type,
}.Add(gtx.Ops)
stack.Pop() stack.Pop()
drag.Update(gtx)
r.Events(drag, transfer.TargetFilter{Type: drag.Type})
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
r.Queue( r.Queue(
pointer.Event{ pointer.Event{
@@ -52,9 +52,11 @@ func TestDraggable(t *testing.T) {
) )
ofr := &offer{data: "hello"} ofr := &offer{data: "hello"}
drag.Offer(gtx, "file", ofr) drag.Offer(gtx, "file", ofr)
drag.Update(gtx)
r.Events(drag, transfer.TargetFilter{Type: drag.Type})
r.Frame(gtx.Ops) r.Frame(gtx.Ops)
evs := r.Events(drag) evs := r.Events(drag, transfer.TargetFilter{Type: drag.Type})
if len(evs) != 2 { if len(evs) != 2 {
t.Fatalf("expected 2 event, got %d", len(evs)) t.Fatalf("expected 2 event, got %d", len(evs))
} }
+15 -14
View File
@@ -225,7 +225,19 @@ func (e *Editor) processPointer(gtx layout.Context) {
axis = gesture.Vertical axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y smin, smax = sbounds.Min.Y, sbounds.Max.Y
} }
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis) var scrollRange image.Rectangle
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions()
if e.SingleLine {
scrollOffX := e.text.ScrollOff().X
scrollRange.Min.X = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else {
scrollOffY := e.text.ScrollOff().Y
scrollRange.Min.Y = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
}
sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollRange)
var soff int var soff int
if e.SingleLine { if e.SingleLine {
e.text.ScrollRel(sdist, 0) e.text.ScrollRel(sdist, 0)
@@ -320,7 +332,7 @@ func (e *Editor) processKey(gtx layout.Context) {
} }
// adjust keeps track of runes dropped because of MaxLen. // adjust keeps track of runes dropped because of MaxLen.
var adjust int var adjust int
for _, ke := range gtx.Events(&e.eventKey) { for _, ke := range gtx.Events(&e.eventKey, transfer.TargetFilter{Type: "application/text"}) {
e.blinkStart = gtx.Now e.blinkStart = gtx.Now
switch ke := ke.(type) { switch ke := ke.(type) {
case key.FocusEvent: case key.FocusEvent:
@@ -609,7 +621,6 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
e.scrollCaret = false e.scrollCaret = false
e.text.ScrollToCaret() e.text.ScrollToCaret()
} }
textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions() visibleDims := e.text.Dimensions()
defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop() defer clip.Rect(image.Rectangle{Max: visibleDims.Size}).Push(gtx.Ops).Pop()
@@ -642,17 +653,7 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call
} }
key.InputOp{Tag: &e.eventKey, Hint: e.InputHint, Keys: keys}.Add(gtx.Ops) key.InputOp{Tag: &e.eventKey, Hint: e.InputHint, Keys: keys}.Add(gtx.Ops)
var scrollRange image.Rectangle e.scroller.Add(gtx.Ops)
if e.SingleLine {
scrollOffX := e.text.ScrollOff().X
scrollRange.Min.X = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else {
scrollOffY := e.text.ScrollOff().Y
scrollRange.Min.Y = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y))
}
e.scroller.Add(gtx.Ops, scrollRange)
e.clicker.Add(gtx.Ops) e.clicker.Add(gtx.Ops)
e.dragger.Add(gtx.Ops) e.dragger.Add(gtx.Ops)
-1
View File
@@ -900,7 +900,6 @@ g 2 4 6 8 g
var tim time.Duration var tim time.Duration
selected := func(start, end int) string { selected := func(start, end int) string {
// Layout once with no events; populate e.lines. // Layout once with no events; populate e.lines.
gtx = gtx.Disabled()
e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{})
e.Events() // throw away any events from this layout e.Events() // throw away any events from this layout
+11 -15
View File
@@ -5,8 +5,11 @@ package widget_test
import ( import (
"fmt" "fmt"
"image" "image"
"io"
"strings"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/io/event"
"gioui.org/io/input" "gioui.org/io/input"
"gioui.org/io/pointer" "gioui.org/io/pointer"
"gioui.org/io/transfer" "gioui.org/io/transfer"
@@ -79,7 +82,7 @@ func ExampleDraggable_Layout() {
} }
// mime is the type used to match drag and drop operations. // mime is the type used to match drag and drop operations.
// It could be left empty in this example. // It could be left empty in this example.
mime := "MyMime" const mime = "MyMime"
drag := &widget.Draggable{Type: mime} drag := &widget.Draggable{Type: mime}
var drop int var drop int
// widget lays out the drag and drop handlers and processes // widget lays out the drag and drop handlers and processes
@@ -94,7 +97,7 @@ func ExampleDraggable_Layout() {
// drag must respond with an Offer event when requested. // drag must respond with an Offer event when requested.
// Use the drag method for this. // Use the drag method for this.
if m, ok := drag.Update(gtx); ok { if m, ok := drag.Update(gtx); ok {
drag.Offer(gtx, m, offer{Data: "hello world"}) drag.Offer(gtx, m, io.NopCloser(strings.NewReader("hello world")))
} }
// Setup the area for drops. // Setup the area for drops.
@@ -102,17 +105,17 @@ func ExampleDraggable_Layout() {
Min: image.Pt(20, 20), Min: image.Pt(20, 20),
Max: image.Pt(40, 40), Max: image.Pt(40, 40),
}.Push(gtx.Ops) }.Push(gtx.Ops)
transfer.TargetOp{ event.InputOp(gtx.Ops, &drop)
Tag: &drop,
Type: mime, // this must match the drag Type for the drop to succeed
}.Add(gtx.Ops)
ds.Pop() ds.Pop()
// Check for the received data. // Check for the received data.
for _, ev := range gtx.Events(&drop) { for _, ev := range gtx.Events(&drop, transfer.TargetFilter{Type: mime}) {
switch e := ev.(type) { switch e := ev.(type) {
case transfer.DataEvent: case transfer.DataEvent:
data := e.Open() data := e.Open()
fmt.Println(data.(offer).Data) defer data.Close()
content, _ := io.ReadAll(data)
fmt.Println(string(content))
} }
} }
} }
@@ -145,10 +148,3 @@ func ExampleDraggable_Layout() {
// Output: // Output:
// hello world // hello world
} }
type offer struct {
Data string
}
func (offer) Read([]byte) (int, error) { return 0, nil }
func (offer) Close() error { return nil }