From ef8171b971fb35dbcf28a706b05ab1f1e81f99f4 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Mon, 16 Oct 2023 17:57:27 -0500 Subject: [PATCH] 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 --- gesture/gesture.go | 54 ++-- gesture/gesture_test.go | 2 + internal/ops/ops.go | 20 +- io/event/event.go | 20 ++ io/input/clipboard_test.go | 6 +- io/input/key_test.go | 29 ++- io/input/pointer.go | 63 +++-- io/input/pointer_test.go | 505 ++++++++++++++++++------------------- io/input/router.go | 105 +++++--- io/input/router_test.go | 24 ++ io/input/semantic_test.go | 8 +- io/pointer/pointer.go | 33 +-- io/transfer/transfer.go | 40 ++- layout/list.go | 40 +-- widget/dnd.go | 8 +- widget/dnd_test.go | 12 +- widget/editor.go | 29 ++- widget/editor_test.go | 1 - widget/example_test.go | 26 +- 19 files changed, 521 insertions(+), 504 deletions(-) create mode 100644 io/input/router_test.go diff --git a/gesture/gesture.go b/gesture/gesture.go index d95e0a38..b43779c3 100644 --- a/gesture/gesture.go +++ b/gesture/gesture.go @@ -17,6 +17,7 @@ import ( "gioui.org/f32" "gioui.org/internal/fling" + "gioui.org/io/event" "gioui.org/io/input" "gioui.org/io/key" "gioui.org/io/pointer" @@ -37,15 +38,12 @@ type Hover struct { // Add the gesture to detect hovering over the current pointer area. func (h *Hover) Add(ops *op.Ops) { - pointer.InputOp{ - Tag: h, - Kinds: pointer.Enter | pointer.Leave, - }.Add(ops) + event.InputOp(ops, h) } // Update state and report whether a pointer is inside the area. 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) if !ok { continue @@ -114,7 +112,6 @@ type Drag struct { // movements as well as drag and fling touch gestures. type Scroll struct { dragging bool - axis Axis estimator fling.Extrapolation flinger fling.Animation pid pointer.ID @@ -159,10 +156,7 @@ const touchSlop = unit.Dp(3) // Add the handler to the operation list to receive click events. func (c *Click) Add(ops *op.Ops) { - pointer.InputOp{ - Tag: c, - Kinds: pointer.Press | pointer.Release | pointer.Enter | pointer.Leave, - }.Add(ops) + event.InputOp(ops, c) } // 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. func (c *Click) Update(q input.Source) []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) if !ok { continue @@ -248,13 +242,8 @@ func (ClickEvent) ImplementsEvent() {} // Add the handler to the operation list to receive scroll events. // The bounds variable refers to the scrolling boundaries // as defined in io/pointer.InputOp. -func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) { - oph := pointer.InputOp{ - Tag: s, - Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll, - ScrollBounds: bounds, - } - oph.Add(ops) +func (s *Scroll) Add(ops *op.Ops) { + event.InputOp(ops, s) if s.flinger.Active() { op.InvalidateOp{}.Add(ops) } @@ -266,13 +255,13 @@ func (s *Scroll) Stop() { } // 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 { - if s.axis != axis { - s.axis = axis - return 0 - } +func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, bounds image.Rectangle) int { 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) if !ok { continue @@ -289,7 +278,7 @@ func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis) } s.Stop() s.estimator = fling.Extrapolation{} - v := s.val(e.Position) + v := s.val(axis, e.Position) s.last = int(math.Round(float64(v))) s.estimator.Sample(e.Time, v) 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: s.dragging = false case pointer.Scroll: - switch s.axis { + switch axis { case Horizontal: s.scroll += e.Scroll.X 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 { continue } - val := s.val(e.Position) + val := s.val(axis, e.Position) s.estimator.Sample(e.Time, val) v := int(math.Round(float64(val))) 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 } -func (s *Scroll) val(p f32.Point) float32 { - if s.axis == Horizontal { +func (s *Scroll) val(axis Axis, p f32.Point) float32 { + if axis == Horizontal { return p.X } else { return p.Y @@ -360,16 +349,13 @@ func (s *Scroll) State() ScrollState { // Add the handler to the operation list to receive drag events. func (d *Drag) Add(ops *op.Ops) { - pointer.InputOp{ - Tag: d, - Kinds: pointer.Press | pointer.Drag | pointer.Release, - }.Add(ops) + event.InputOp(ops, d) } // Update state and return the drag events. func (d *Drag) Update(cfg unit.Metric, q input.Source, axis Axis) []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) if !ok { continue diff --git a/gesture/gesture_test.go b/gesture/gesture_test.go index 0bef2a07..a68dad3c 100644 --- a/gesture/gesture_test.go +++ b/gesture/gesture_test.go @@ -23,6 +23,7 @@ func TestHover(t *testing.T) { h.Add(ops) stack.Pop() r := new(input.Router) + h.Update(r.Source()) r.Frame(ops) r.Queue( @@ -72,6 +73,7 @@ func TestMouseClicks(t *testing.T) { click.Add(&ops) var r input.Router + click.Update(r.Source()) r.Frame(&ops) r.Queue(tc.events...) diff --git a/internal/ops/ops.go b/internal/ops/ops.go index c4f0cd80..d7250f69 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -62,9 +62,7 @@ const ( TypeLinearGradient TypePass TypePopPass - TypePointerInput - TypeSource - TypeTarget + TypeInput TypeKeyInput TypeSave TypeLoad @@ -140,9 +138,7 @@ const ( TypeLinearGradientLen = 1 + 8*2 + 4*2 TypePassLen = 1 TypePopPassLen = 1 - TypePointerInputLen = 1 + 1*2 + 2*4 + 2*4 - TypeSourceLen = 1 - TypeTargetLen = 1 + TypeInputLen = 1 TypeKeyInputLen = 1 + 1 TypeSaveLen = 1 + 4 TypeLoadLen = 1 + 4 @@ -416,9 +412,7 @@ var opProps = [0x100]opProp{ TypeLinearGradient: {Size: TypeLinearGradientLen, NumRefs: 0}, TypePass: {Size: TypePassLen, NumRefs: 0}, TypePopPass: {Size: TypePopPassLen, NumRefs: 0}, - TypePointerInput: {Size: TypePointerInputLen, NumRefs: 1}, - TypeSource: {Size: TypeSourceLen, NumRefs: 2}, - TypeTarget: {Size: TypeTargetLen, NumRefs: 2}, + TypeInput: {Size: TypeInputLen, NumRefs: 1}, TypeKeyInput: {Size: TypeKeyInputLen, NumRefs: 2}, TypeSave: {Size: TypeSaveLen, NumRefs: 0}, TypeLoad: {Size: TypeLoadLen, NumRefs: 0}, @@ -479,12 +473,8 @@ func (t OpType) String() string { return "Pass" case TypePopPass: return "PopPass" - case TypePointerInput: - return "PointerInput" - case TypeSource: - return "Source" - case TypeTarget: - return "Target" + case TypeInput: + return "Input" case TypeKeyInput: return "KeyInput" case TypeSave: diff --git a/io/event/event.go b/io/event/event.go index c664d63a..2f13b855 100644 --- a/io/event/event.go +++ b/io/event/event.go @@ -3,6 +3,11 @@ // Package event contains types for event handling. package event +import ( + "gioui.org/internal/ops" + "gioui.org/op" +) + // Tag is the stable identifier for an event handler. // For a handler h, the tag is typically &h. type Tag interface{} @@ -11,3 +16,18 @@ type Tag interface{} type Event interface { 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) +} diff --git a/io/input/clipboard_test.go b/io/input/clipboard_test.go index 626fda93..fea3973f 100644 --- a/io/input/clipboard_test.go +++ b/io/input/clipboard_test.go @@ -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 diff --git a/io/input/key_test.go b/io/input/key_test.go index 4c383694..9265ae6a 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -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) { diff --git a/io/input/pointer.go b/io/input/pointer.go index cb7156d6..7e774e00 100644 --- a/io/input/pointer.go +++ b/io/input/pointer.go @@ -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 diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index fb9e8ae4..1242949e 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -22,24 +22,22 @@ import ( func TestPointerWakeup(t *testing.T) { handler := new(int) var ops op.Ops - addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) - var r Router + addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100)) + // Test that merely adding a handler doesn't trigger redraw. r.Frame(&ops) if _, wake := r.WakeupTime(); wake { t.Errorf("adding pointer.InputOp triggered a redraw") } - // However, adding a handler queues a Cancel event. - assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel) } func TestPointerDrag(t *testing.T) { handler := new(int) var ops op.Ops - addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) - var r Router + f := addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100)) + r.Frame(&ops) r.Queue( // Press. @@ -53,15 +51,15 @@ func TestPointerDrag(t *testing.T) { Position: f32.Pt(150, 150), }, ) - assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) + assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) } func TestPointerDragNegative(t *testing.T) { handler := new(int) var ops op.Ops - addPointerHandler(&ops, handler, image.Rect(-100, -100, 0, 0)) - var r Router + f := addPointerHandler(&r, &ops, handler, image.Rect(-100, -100, 0, 0)) + r.Frame(&ops) r.Queue( // Press. @@ -75,7 +73,7 @@ func TestPointerDragNegative(t *testing.T) { Position: f32.Pt(-150, -150), }, ) - assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) + assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Enter, pointer.Press, pointer.Leave, pointer.Drag) } func TestPointerGrab(t *testing.T) { @@ -84,13 +82,16 @@ func TestPointerGrab(t *testing.T) { handler3 := new(int) var ops op.Ops - types := pointer.Press | pointer.Release + filter := pointer.Filter{Kinds: pointer.Press | pointer.Release} - pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops) - pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops) - pointer.InputOp{Tag: handler3, Kinds: types}.Add(&ops) + event.InputOp(&ops, handler1) + event.InputOp(&ops, handler2) + event.InputOp(&ops, handler3) var r Router + assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Cancel) r.Frame(&ops) r.Queue( pointer.Event{ @@ -99,9 +100,9 @@ func TestPointerGrab(t *testing.T) { }, ) r.Source().Queue(pointer.GrabCmd{Tag: handler1}) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Cancel, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(handler3), pointer.Cancel, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Press) r.Frame(&ops) r.Queue( pointer.Event{ @@ -109,9 +110,9 @@ func TestPointerGrab(t *testing.T) { Position: f32.Pt(50, 50), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Release) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Cancel) - assertEventPointerTypeSequence(t, r.Events(handler3), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Release) + assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler3, filter), pointer.Cancel) } func TestPointerGrabSameHandlerTwice(t *testing.T) { @@ -119,13 +120,15 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) { handler2 := new(int) var ops op.Ops - types := pointer.Press | pointer.Release + filter := pointer.Filter{Kinds: pointer.Press | pointer.Release} - pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops) - pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops) - pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops) + event.InputOp(&ops, handler1) + event.InputOp(&ops, handler1) + event.InputOp(&ops, handler2) var r Router + assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel) r.Frame(&ops) r.Queue( pointer.Event{ @@ -134,8 +137,8 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) { }, ) r.Source().Queue(pointer.GrabCmd{Tag: handler1}) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Cancel, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Press) r.Frame(&ops) r.Queue( pointer.Event{ @@ -143,8 +146,8 @@ func TestPointerGrabSameHandlerTwice(t *testing.T) { Position: f32.Pt(50, 50), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Release) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler1, filter), pointer.Release) + assertEventPointerTypeSequence(t, r.Events(handler2, filter), pointer.Cancel) } func TestPointerMove(t *testing.T) { @@ -152,18 +155,22 @@ func TestPointerMove(t *testing.T) { handler2 := new(int) var ops op.Ops - types := pointer.Move | pointer.Enter | pointer.Leave + f := pointer.Filter{ + Kinds: pointer.Move | pointer.Enter | pointer.Leave, + } // Handler 1 area: (0, 0) - (100, 100) r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops) + event.InputOp(&ops, handler1) // Handler 2 area: (50, 50) - (100, 100) (areas intersect). r2 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops) - pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops) + event.InputOp(&ops, handler2) r2.Pop() r1.Pop() var r Router + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Cancel) r.Frame(&ops) r.Queue( // Hit both handlers. @@ -185,21 +192,22 @@ func TestPointerMove(t *testing.T) { Kind: pointer.Cancel, }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Move, pointer.Move, pointer.Leave, pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Enter, pointer.Move, pointer.Leave, pointer.Cancel) } func TestPointerTypes(t *testing.T) { handler := new(int) var ops op.Ops r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - pointer.InputOp{ - Tag: handler, + f := pointer.Filter{ Kinds: pointer.Press | pointer.Release, - }.Add(&ops) + } + event.InputOp(&ops, handler) r1.Pop() var r Router + assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Cancel) r.Frame(&ops) r.Queue( pointer.Event{ @@ -215,7 +223,7 @@ func TestPointerTypes(t *testing.T) { Position: f32.Pt(150, 150), }, ) - assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Press, pointer.Release) + assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Press, pointer.Release) } func TestPointerSystemAction(t *testing.T) { @@ -260,32 +268,35 @@ func TestPointerPriority(t *testing.T) { handler2 := new(int) handler3 := new(int) var ops op.Ops + var r Router r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - pointer.InputOp{ - Tag: handler1, + f1 := pointer.Filter{ Kinds: pointer.Scroll, ScrollBounds: image.Rectangle{Max: image.Point{X: 100}}, - }.Add(&ops) + } + r.Events(handler1, f1) + event.InputOp(&ops, handler1) r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops) - pointer.InputOp{ - Tag: handler2, + f2 := pointer.Filter{ Kinds: pointer.Scroll, ScrollBounds: image.Rectangle{Max: image.Point{X: 20}}, - }.Add(&ops) + } + r.Events(handler2, f2) + event.InputOp(&ops, handler2) r2.Pop() r1.Pop() r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops) - pointer.InputOp{ - Tag: handler3, + f3 := pointer.Filter{ Kinds: pointer.Scroll, ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}}, - }.Add(&ops) + } + r.Events(handler3, f3) + event.InputOp(&ops, handler3) r3.Pop() - var r Router r.Frame(&ops) r.Queue( // Hit handler 1 and 2. @@ -313,33 +324,33 @@ func TestPointerPriority(t *testing.T) { }, ) - hev1 := r.Events(handler1) - hev2 := r.Events(handler2) - hev3 := r.Events(handler3) - assertEventPointerTypeSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll) - assertEventPointerTypeSequence(t, hev2, pointer.Cancel, pointer.Scroll) - assertEventPointerTypeSequence(t, hev3, pointer.Cancel, pointer.Scroll) - assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared, pointer.Foremost) - assertEventPriorities(t, hev2, pointer.Shared, pointer.Foremost) - assertEventPriorities(t, hev3, pointer.Shared, pointer.Foremost) - assertScrollEvent(t, hev1[1], f32.Pt(30, 0)) - assertScrollEvent(t, hev2[1], f32.Pt(20, 0)) - assertScrollEvent(t, hev1[2], f32.Pt(50, 0)) - assertScrollEvent(t, hev3[1], f32.Pt(-20, -30)) + hev1 := r.Events(handler1, f1) + hev2 := r.Events(handler2, f2) + hev3 := r.Events(handler3, f3) + assertEventPointerTypeSequence(t, hev1, pointer.Scroll, pointer.Scroll) + assertEventPointerTypeSequence(t, hev2, pointer.Scroll) + assertEventPointerTypeSequence(t, hev3, pointer.Scroll) + assertEventPriorities(t, hev1, pointer.Shared, pointer.Foremost) + assertEventPriorities(t, hev2, pointer.Foremost) + assertEventPriorities(t, hev3, pointer.Foremost) + assertScrollEvent(t, hev1[0], f32.Pt(30, 0)) + assertScrollEvent(t, hev2[0], f32.Pt(20, 0)) + assertScrollEvent(t, hev1[1], f32.Pt(50, 0)) + assertScrollEvent(t, hev3[0], f32.Pt(-20, -30)) } func TestPointerEnterLeave(t *testing.T) { handler1 := new(int) handler2 := new(int) var ops op.Ops + var r Router // Handler 1 area: (0, 0) - (100, 100) - addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + f1 := addPointerHandler(&r, &ops, handler1, image.Rect(0, 0, 100, 100)) // Handler 2 area: (50, 50) - (200, 200) (areas overlap). - addPointerHandler(&ops, handler2, image.Rect(50, 50, 200, 200)) + f2 := addPointerHandler(&r, &ops, handler2, image.Rect(50, 50, 200, 200)) - var r Router r.Frame(&ops) // Hit both handlers. r.Queue( @@ -351,8 +362,8 @@ func TestPointerEnterLeave(t *testing.T) { // First event for a handler is always a Cancel. // Only handler2 should receive the enter/move events because it is on top // and handler1 is not an ancestor in the hit tree. - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler1, f1)) + assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Enter, pointer.Move) // Leave the second area by moving into the first. r.Queue( @@ -362,8 +373,8 @@ func TestPointerEnterLeave(t *testing.T) { }, ) // The cursor leaves handler2 and enters handler1. - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Enter, pointer.Move) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Leave) + assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Enter, pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Leave) // Move, but stay within the same hit area. r.Queue( @@ -372,8 +383,8 @@ func TestPointerEnterLeave(t *testing.T) { Position: f32.Pt(40, 40), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Move) - assertEventPointerTypeSequence(t, r.Events(handler2)) + assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler2, f2)) // Move outside of both inputs. r.Queue( @@ -382,8 +393,8 @@ func TestPointerEnterLeave(t *testing.T) { Position: f32.Pt(300, 300), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Leave) - assertEventPointerTypeSequence(t, r.Events(handler2)) + assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Leave) + assertEventPointerTypeSequence(t, r.Events(handler2, f2)) // Check that a Press event generates Enter Events. r.Queue( @@ -392,8 +403,8 @@ func TestPointerEnterLeave(t *testing.T) { Position: f32.Pt(125, 125), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1)) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler1, f1)) + assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Enter, pointer.Press) // Check that a drag only affects the participating handlers. r.Queue( @@ -408,8 +419,8 @@ func TestPointerEnterLeave(t *testing.T) { Position: f32.Pt(50, 50), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1)) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Leave, pointer.Drag, pointer.Enter, pointer.Drag) + assertEventPointerTypeSequence(t, r.Events(handler1, f1)) + assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Leave, pointer.Drag, pointer.Enter, pointer.Drag) // Check that a Release event generates Enter/Leave Events. r.Queue( @@ -419,25 +430,24 @@ func TestPointerEnterLeave(t *testing.T) { 25), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Enter) + assertEventPointerTypeSequence(t, r.Events(handler1, f1), pointer.Enter) // The second handler gets the release event because the press started inside it. - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) - + assertEventPointerTypeSequence(t, r.Events(handler2, f2), pointer.Release, pointer.Leave) } func TestMultipleAreas(t *testing.T) { handler := new(int) var ops op.Ops + var r Router - addPointerHandler(&ops, handler, image.Rect(0, 0, 100, 100)) + f := addPointerHandler(&r, &ops, handler, image.Rect(0, 0, 100, 100)) r1 := clip.Rect(image.Rect(50, 50, 200, 200)).Push(&ops) - // Second area has no Types set, yet should receive events because - // Types for the same handles are or-ed together. - pointer.InputOp{Tag: handler}.Add(&ops) + // Test that declaring a handler twice doesn't affect event handling. + event.InputOp(&ops, handler) r1.Pop() - var r Router + assertEventPointerTypeSequence(t, r.Events(handler, f)) r.Frame(&ops) // Hit first area, then second area, then both. r.Queue( @@ -454,7 +464,7 @@ func TestMultipleAreas(t *testing.T) { Position: f32.Pt(50, 50), }, ) - assertEventPointerTypeSequence(t, r.Events(handler), pointer.Cancel, pointer.Enter, pointer.Move, pointer.Move, pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler, f), pointer.Enter, pointer.Move, pointer.Move, pointer.Move) } func TestPointerEnterLeaveNested(t *testing.T) { @@ -462,19 +472,21 @@ func TestPointerEnterLeaveNested(t *testing.T) { handler2 := new(int) var ops op.Ops - types := pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave + f := pointer.Filter{Kinds: pointer.Press | pointer.Move | pointer.Release | pointer.Enter | pointer.Leave} // Handler 1 area: (0, 0) - (100, 100) r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) - pointer.InputOp{Tag: handler1, Kinds: types}.Add(&ops) + event.InputOp(&ops, handler1) // Handler 2 area: (25, 25) - (75, 75) (nested within first). r2 := clip.Rect(image.Rect(25, 25, 75, 75)).Push(&ops) - pointer.InputOp{Tag: handler2, Kinds: types}.Add(&ops) + event.InputOp(&ops, handler2) r2.Pop() r1.Pop() var r Router + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Cancel) r.Frame(&ops) // Hit both handlers. r.Queue( @@ -485,8 +497,8 @@ func TestPointerEnterLeaveNested(t *testing.T) { ) // First event for a handler is always a Cancel. // Both handlers should receive the Enter and Move events because handler2 is a child of handler1. - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Cancel, pointer.Enter, pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Enter, pointer.Move) // Leave the second area by moving into the first. r.Queue( @@ -495,8 +507,8 @@ func TestPointerEnterLeaveNested(t *testing.T) { Position: f32.Pt(20, 20), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Move) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Leave) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Leave) // Move, but stay within the same hit area. r.Queue( @@ -505,8 +517,8 @@ func TestPointerEnterLeaveNested(t *testing.T) { Position: f32.Pt(10, 10), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Move) - assertEventPointerTypeSequence(t, r.Events(handler2)) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler2, f)) // Move outside of both inputs. r.Queue( @@ -515,8 +527,8 @@ func TestPointerEnterLeaveNested(t *testing.T) { Position: f32.Pt(200, 200), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Leave) - assertEventPointerTypeSequence(t, r.Events(handler2)) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Leave) + assertEventPointerTypeSequence(t, r.Events(handler2, f)) // Check that a Press event generates Enter Events. r.Queue( @@ -525,8 +537,8 @@ func TestPointerEnterLeaveNested(t *testing.T) { Position: f32.Pt(50, 50), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Enter, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Enter, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Enter, pointer.Press) // Check that a Release event generates Enter/Leave Events. r.Queue( @@ -535,8 +547,8 @@ func TestPointerEnterLeaveNested(t *testing.T) { Position: f32.Pt(20, 20), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Release) - assertEventPointerTypeSequence(t, r.Events(handler2), pointer.Release, pointer.Leave) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Release) + assertEventPointerTypeSequence(t, r.Events(handler2, f), pointer.Release, pointer.Leave) } func TestPointerActiveInputDisappears(t *testing.T) { @@ -546,7 +558,7 @@ func TestPointerActiveInputDisappears(t *testing.T) { // Draw handler. ops.Reset() - addPointerHandler(&ops, handler1, image.Rect(0, 0, 100, 100)) + f := addPointerHandler(&r, &ops, handler1, image.Rect(0, 0, 100, 100)) r.Frame(&ops) r.Queue( pointer.Event{ @@ -554,7 +566,8 @@ func TestPointerActiveInputDisappears(t *testing.T) { Position: f32.Pt(25, 25), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1), pointer.Cancel, pointer.Enter, pointer.Move) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Enter, pointer.Move) + r.Frame(&ops) // Re-render with handler missing. ops.Reset() @@ -565,21 +578,21 @@ func TestPointerActiveInputDisappears(t *testing.T) { Position: f32.Pt(25, 25), }, ) - assertEventPointerTypeSequence(t, r.Events(handler1)) + assertEventPointerTypeSequence(t, r.Events(handler1, f), pointer.Cancel) } func TestMultitouch(t *testing.T) { var ops op.Ops + var r Router // 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)) + f1 := addPointerHandler(&r, &ops, h1, image.Rect(0, 0, 100, 100)) + f2 := addPointerHandler(&r, &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.Queue( pointer.Event{ @@ -602,119 +615,94 @@ func TestMultitouch(t *testing.T) { PointerID: p2, }, ) - assertEventPointerTypeSequence(t, r.Events(h1), pointer.Cancel, pointer.Enter, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(h2), pointer.Cancel, pointer.Enter, pointer.Press, pointer.Release) + assertEventPointerTypeSequence(t, r.Events(h1, f1), pointer.Enter, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(h2, f2), pointer.Enter, pointer.Press, pointer.Release) } func TestCursor(t *testing.T) { - ops := new(op.Ops) - var r Router - var h, h2 int - var widget2 func() - widget := func() { - // This is the area where the cursor is changed to CursorPointer. - defer clip.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Push(ops).Pop() - // The cursor is checked and changed upon cursor movement. - pointer.InputOp{Tag: &h}.Add(ops) - pointer.CursorPointer.Add(ops) - if widget2 != nil { - widget2() - } - } - // Register the handlers. - widget() - // No cursor change as the mouse has not moved yet. - if got, want := r.Cursor(), pointer.CursorDefault; got != want { - t.Errorf("got %q; want %q", got, want) - } - - _at := func(x, y float32) pointer.Event { - return pointer.Event{ + _at := func(x, y float32) []event.Event { + return []event.Event{pointer.Event{ Kind: pointer.Move, Source: pointer.Mouse, Buttons: pointer.ButtonPrimary, Position: f32.Pt(x, y), - } + }} } + ops := new(op.Ops) + var r Router for _, tc := range []struct { - label string - event interface{} - want pointer.Cursor + label string + events []event.Event + cursors []pointer.Cursor + want pointer.Cursor }{ + {label: "no movement", + cursors: []pointer.Cursor{pointer.CursorPointer}, + want: pointer.CursorDefault, + }, {label: "move inside", - event: _at(50, 50), - want: pointer.CursorPointer, + cursors: []pointer.Cursor{pointer.CursorPointer}, + events: _at(50, 50), + want: pointer.CursorPointer, }, {label: "move outside", - event: _at(200, 200), - want: pointer.CursorDefault, + cursors: []pointer.Cursor{pointer.CursorPointer}, + events: _at(200, 200), + want: pointer.CursorDefault, }, {label: "move back inside", - event: _at(50, 50), - want: pointer.CursorPointer, + cursors: []pointer.Cursor{pointer.CursorPointer}, + events: _at(50, 50), + want: pointer.CursorPointer, }, {label: "send key events while inside", - event: []event.Event{ + cursors: []pointer.Cursor{pointer.CursorPointer}, + events: []event.Event{ key.Event{Name: "A", State: key.Press}, key.Event{Name: "A", State: key.Release}, }, want: pointer.CursorPointer, }, {label: "send key events while outside", - event: []event.Event{ + cursors: []pointer.Cursor{pointer.CursorPointer}, + events: append( _at(200, 200), key.Event{Name: "A", State: key.Press}, key.Event{Name: "A", State: key.Release}, - }, + ), want: pointer.CursorDefault, }, {label: "add new input on top while inside", - event: func() []event.Event { - widget2 = func() { - pointer.InputOp{Tag: &h2}.Add(ops) - pointer.CursorCrosshair.Add(ops) - } - return []event.Event{ - _at(50, 50), - key.Event{ - Name: "A", - State: key.Press, - }, - } - }, + cursors: []pointer.Cursor{pointer.CursorPointer, pointer.CursorCrosshair}, + events: append( + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + ), want: pointer.CursorCrosshair, }, {label: "remove input on top while inside", - event: func() []event.Event { - widget2 = nil - return []event.Event{ - _at(50, 50), - key.Event{ - Name: "A", - State: key.Press, - }, - } - }, + cursors: []pointer.Cursor{pointer.CursorPointer}, + events: append( + _at(50, 50), + key.Event{ + Name: "A", + State: key.Press, + }, + ), want: pointer.CursorPointer, }, } { t.Run(tc.label, func(t *testing.T) { ops.Reset() - widget() - r.Frame(ops) - switch ev := tc.event.(type) { - case event.Event: - r.Queue(ev) - case []event.Event: - r.Queue(ev...) - case func() event.Event: - r.Queue(ev()) - case func() []event.Event: - r.Queue(ev()...) - default: - panic(fmt.Sprintf("unknown event %T", ev)) + defer clip.Rect(image.Rectangle{Max: image.Pt(100, 100)}).Push(ops).Pop() + for _, c := range tc.cursors { + c.Add(ops) } - widget() + r.Frame(ops) + r.Queue(tc.events...) r.Frame(ops) // The cursor should now have been changed if the mouse moved over the declared area. if got, want := r.Cursor(), tc.want; got != want { @@ -730,45 +718,55 @@ func TestPassOp(t *testing.T) { h1, h2, h3, h4 := new(int), new(int), new(int), new(int) area := clip.Rect(image.Rect(0, 0, 100, 100)) root := area.Push(&ops) - pointer.InputOp{Tag: h1, Kinds: pointer.Press}.Add(&ops) + event.InputOp(&ops, &h1) + event.InputOp(&ops, h1) child1 := area.Push(&ops) - pointer.InputOp{Tag: h2, Kinds: pointer.Press}.Add(&ops) + event.InputOp(&ops, h2) child1.Pop() child2 := area.Push(&ops) pass := pointer.PassOp{}.Push(&ops) - pointer.InputOp{Tag: h3, Kinds: pointer.Press}.Add(&ops) - pointer.InputOp{Tag: h4, Kinds: pointer.Press}.Add(&ops) + event.InputOp(&ops, h3) + event.InputOp(&ops, h4) pass.Pop() child2.Pop() root.Pop() var r Router + f := pointer.Filter{Kinds: pointer.Press} + assertEventPointerTypeSequence(t, r.Events(h1, f), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(h2, f), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(h3, f), pointer.Cancel) + assertEventPointerTypeSequence(t, r.Events(h4, f), pointer.Cancel) r.Frame(&ops) r.Queue( pointer.Event{ Kind: pointer.Press, }, ) - assertEventPointerTypeSequence(t, r.Events(h1), pointer.Cancel, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(h2), pointer.Cancel, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(h3), pointer.Cancel, pointer.Press) - assertEventPointerTypeSequence(t, r.Events(h4), pointer.Cancel, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(h1, f), pointer.Press) + assertEventPointerTypeSequence(t, r.Events(h2, f), pointer.Press) + assertEventPointerTypeSequence(t, r.Events(h3, f), pointer.Press) + assertEventPointerTypeSequence(t, r.Events(h4, f), pointer.Press) } func TestAreaPassthrough(t *testing.T) { var ops op.Ops h := new(int) - pointer.InputOp{Tag: h, Kinds: pointer.Press}.Add(&ops) + event.InputOp(&ops, h) clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops).Pop() var r Router + f := pointer.Filter{ + Kinds: pointer.Press, + } + assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Cancel) r.Frame(&ops) r.Queue( pointer.Event{ Kind: pointer.Press, }, ) - assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Press) } func TestEllipse(t *testing.T) { @@ -776,9 +774,13 @@ func TestEllipse(t *testing.T) { h := new(int) cl := clip.Ellipse(image.Rect(0, 0, 100, 100)).Push(&ops) - pointer.InputOp{Tag: h, Kinds: pointer.Press}.Add(&ops) + event.InputOp(&ops, h) cl.Pop() var r Router + f := pointer.Filter{ + Kinds: pointer.Press, + } + assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Cancel) r.Frame(&ops) r.Queue( // Outside ellipse. @@ -795,38 +797,32 @@ func TestEllipse(t *testing.T) { Kind: pointer.Press, }, ) - assertEventPointerTypeSequence(t, r.Events(h), pointer.Cancel, pointer.Press) + assertEventPointerTypeSequence(t, r.Events(h, f), pointer.Press) } func TestTransfer(t *testing.T) { srcArea := image.Rect(0, 0, 20, 20) tgtArea := srcArea.Add(image.Pt(40, 0)) - setup := func(ops *op.Ops, srcType, tgtType string) (src, tgt event.Tag) { + setup := func(r *Router, ops *op.Ops, srcType, tgtType string) (src, tgt event.Tag) { src, tgt = new(int), new(int) + r.Events(src, transfer.SourceFilter{Type: srcType}) + r.Events(tgt, transfer.TargetFilter{Type: tgtType}) srcStack := clip.Rect(srcArea).Push(ops) - transfer.SourceOp{ - Tag: src, - Type: srcType, - }.Add(ops) + event.InputOp(ops, src) srcStack.Pop() tgt1Stack := clip.Rect(tgtArea).Push(ops) - transfer.TargetOp{ - Tag: tgt, - Type: tgtType, - }.Add(ops) + event.InputOp(ops, tgt) tgt1Stack.Pop() return src, tgt } - // Cancel is received when the pointer is first seen. - cancel := pointer.Event{Kind: pointer.Cancel} t.Run("drop on no target", func(t *testing.T) { ops := new(op.Ops) - src, tgt := setup(ops, "file", "file") var r Router + src, tgt := setup(&r, ops, "file", "file") r.Frame(ops) // Initiate a drag. r.Queue( @@ -839,8 +835,8 @@ func TestTransfer(t *testing.T) { Kind: pointer.Move, }, ) - assertEventSequence(t, r.Events(src), cancel) - assertEventSequence(t, r.Events(tgt), cancel, transfer.InitiateEvent{}) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"})) + assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"}), transfer.InitiateEvent{}) // Drop. r.Queue( @@ -853,21 +849,19 @@ func TestTransfer(t *testing.T) { Kind: pointer.Release, }, ) - assertEventSequence(t, r.Events(src), transfer.CancelEvent{}) - assertEventSequence(t, r.Events(tgt), transfer.CancelEvent{}) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{}) + assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"}), transfer.CancelEvent{}) }) t.Run("drag with valid and invalid targets", func(t *testing.T) { ops := new(op.Ops) - src, tgt1 := setup(ops, "file", "file") - tgt2 := new(int) - stack := clip.Rect(tgtArea).Push(ops) - transfer.TargetOp{ - Tag: tgt2, - Type: "nofile", - }.Add(ops) - stack.Pop() var r Router + src, tgt1 := setup(&r, ops, "file", "file") + tgt2 := new(int) + r.Events(tgt2, transfer.TargetFilter{Type: "nofile"}) + stack := clip.Rect(tgtArea).Push(ops) + event.InputOp(ops, tgt2) + stack.Pop() r.Frame(ops) // Initiate a drag. r.Queue( @@ -880,15 +874,15 @@ func TestTransfer(t *testing.T) { Kind: pointer.Move, }, ) - assertEventSequence(t, r.Events(src), cancel) - assertEventSequence(t, r.Events(tgt1), cancel, transfer.InitiateEvent{}) - assertEventSequence(t, r.Events(tgt2), cancel) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"})) + assertEventSequence(t, r.Events(tgt1, transfer.TargetFilter{Type: "file"}), transfer.InitiateEvent{}) + assertEventSequence(t, r.Events(tgt2, transfer.TargetFilter{Type: "nofile"})) }) t.Run("drop on invalid target", func(t *testing.T) { ops := new(op.Ops) - src, tgt := setup(ops, "file", "nofile") var r Router + src, tgt := setup(&r, ops, "file", "nofile") r.Frame(ops) // Drag. r.Queue( @@ -901,8 +895,8 @@ func TestTransfer(t *testing.T) { Kind: pointer.Move, }, ) - assertEventSequence(t, r.Events(src), cancel) - assertEventSequence(t, r.Events(tgt), cancel) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"})) + assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "nofile"})) // Drop. r.Queue( @@ -911,21 +905,16 @@ func TestTransfer(t *testing.T) { Kind: pointer.Release, }, ) - assertEventSequence(t, r.Events(src), transfer.CancelEvent{}) - assertEventSequence(t, r.Events(tgt)) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{}) + assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "nofile"})) }) t.Run("drop on valid target", func(t *testing.T) { ops := new(op.Ops) - src, tgt := setup(ops, "file", "file") - // Make the target also a source. This should have no effect. - stack := clip.Rect(tgtArea).Push(ops) - transfer.SourceOp{ - Tag: tgt, - Type: "file", - }.Add(ops) - stack.Pop() var r Router + src, tgt := setup(&r, ops, "file", "file") + // Make the target also a source. This should have no effect. + r.Events(tgt, transfer.SourceFilter{Type: "file"}) r.Frame(ops) // Drag. r.Queue( @@ -938,8 +927,8 @@ func TestTransfer(t *testing.T) { Kind: pointer.Move, }, ) - assertEventSequence(t, r.Events(src), cancel) - assertEventSequence(t, r.Events(tgt), cancel, transfer.InitiateEvent{}) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"})) + assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"}), transfer.InitiateEvent{}) // Drop. r.Queue( @@ -948,13 +937,14 @@ func TestTransfer(t *testing.T) { Kind: pointer.Release, }, ) - assertEventSequence(t, r.Events(src), transfer.RequestEvent{Type: "file"}) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.RequestEvent{Type: "file"}) // Offer valid type and data. ofr := &offer{data: "hello"} r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr}) r.Frame(ops) - evs := r.Events(tgt) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{}) + evs := r.Events(tgt, transfer.TargetFilter{Type: "file"}) if len(evs) != 2 { t.Fatalf("unexpected number of events: %d, want 2", len(evs)) } @@ -974,21 +964,14 @@ func TestTransfer(t *testing.T) { if ofr.closed { t.Error("offer closed prematurely") } - assertEventSequence(t, r.Events(src), transfer.CancelEvent{}) - r.Frame(ops) }) t.Run("drop on valid target, DataEvent not used", func(t *testing.T) { ops := new(op.Ops) - src, tgt := setup(ops, "file", "file") - // Make the target also a source. This should have no effect. - stack := clip.Rect(tgtArea).Push(ops) - transfer.SourceOp{ - Tag: tgt, - Type: "file", - }.Add(ops) - stack.Pop() var r Router + src, tgt := setup(&r, ops, "file", "file") + // Make the target also a source. This should have no effect. + r.Events(tgt, transfer.SourceFilter{Type: "file"}) r.Frame(ops) // Drag. r.Queue( @@ -1007,10 +990,12 @@ func TestTransfer(t *testing.T) { ) ofr := &offer{data: "hello"} r.Source().Queue(transfer.OfferCmd{Tag: src, Type: "file", Data: ofr}) + r.Events(src, transfer.SourceFilter{Type: "file"}) + r.Events(tgt, transfer.TargetFilter{Type: "file"}) r.Frame(ops) - assertEventSequence(t, r.Events(src), transfer.CancelEvent{}) + assertEventSequence(t, r.Events(src, transfer.SourceFilter{Type: "file"}), transfer.CancelEvent{}) // Ignore DataEvent and verify that the next frame closes it as unused. - assertEventSequence(t, r.Events(tgt)[1:], transfer.CancelEvent{}) + assertEventSequence(t, r.Events(tgt, transfer.TargetFilter{Type: "file"})[1:], transfer.CancelEvent{}) r.Frame(ops) if !ofr.closed { t.Error("offer was not closed") @@ -1036,13 +1021,13 @@ func TestPassCursor(t *testing.T) { rect := clip.Rect(image.Rect(0, 0, 100, 100)) background := rect.Push(&ops) - pointer.InputOp{Tag: 1}.Add(&ops) + event.InputOp(&ops, 1) pointer.CursorDefault.Add(&ops) background.Pop() overlayPass := pointer.PassOp{}.Push(&ops) overlay := rect.Push(&ops) - pointer.InputOp{Tag: 2}.Add(&ops) + event.InputOp(&ops, 2) want := pointer.CursorPointer want.Add(&ops) overlay.Pop() @@ -1071,12 +1056,14 @@ func (o *offer) Close() error { // addPointerHandler adds a pointer.InputOp for the tag in a // rectangular area. -func addPointerHandler(ops *op.Ops, tag event.Tag, area image.Rectangle) { - defer clip.Rect(area).Push(ops).Pop() - pointer.InputOp{ - Tag: tag, +func addPointerHandler(r *Router, ops *op.Ops, tag event.Tag, area image.Rectangle) pointer.Filter { + f := pointer.Filter{ Kinds: pointer.Press | pointer.Release | pointer.Move | pointer.Drag | pointer.Enter | pointer.Leave, - }.Add(ops) + } + r.Events(tag, f) + defer clip.Rect(area).Push(ops).Pop() + event.InputOp(ops, tag) + return f } // pointerTypes converts a sequence of event.Event to their pointer.Types. It assumes @@ -1181,6 +1168,7 @@ func BenchmarkRouterAdd(b *testing.B) { } var ops op.Ops + var r Router for i := range handlers { clip.Rect(image.Rectangle{ Max: image.Point{ @@ -1189,12 +1177,9 @@ func BenchmarkRouterAdd(b *testing.B) { }, }). Push(&ops) - pointer.InputOp{ - Tag: handlers[i], - Kinds: pointer.Move, - }.Add(&ops) + r.Events(handlers[i], pointer.Filter{Kinds: pointer.Move}) + event.InputOp(&ops, handlers[i]) } - var r Router r.Frame(&ops) b.ReportAllocs() b.ResetTimer() diff --git a/io/input/router.go b/io/input/router.go index 39e531df..3a405f2e 100644 --- a/io/input/router.go +++ b/io/input/router.go @@ -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() { diff --git a/io/input/router_test.go b/io/input/router_test.go new file mode 100644 index 00000000..a0e902bd --- /dev/null +++ b/io/input/router_test.go @@ -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) + } +} diff --git a/io/input/semantic_test.go b/io/input/semantic_test.go index 004bc609..6587ae32 100644 --- a/io/input/semantic_test.go +++ b/io/input/semantic_test.go @@ -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 diff --git a/io/pointer/pointer.go b/io/pointer/pointer.go index 8c8ffecb..493d08bb 100644 --- a/io/pointer/pointer.go +++ b/io/pointer/pointer.go @@ -3,8 +3,6 @@ package pointer import ( - "encoding/binary" - "fmt" "image" "strings" "time" @@ -56,11 +54,9 @@ type PassStack struct { macroID uint32 } -// InputOp declares an input handler ready for pointer -// events. -type InputOp struct { - Tag event.Tag - // Kinds is a bitwise-or of event types to receive. +// Filter matches [Event]s. +type Filter struct { + // Kinds is a bitwise-or of event types to match. Kinds Kind // ScrollBounds describe the maximum scrollable distances in both // 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) } -// 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 { if t == Cancel { return "Cancel" @@ -406,3 +381,5 @@ func (c Cursor) String() string { func (Event) ImplementsEvent() {} func (GrabCmd) ImplementsCommand() {} + +func (Filter) ImplementsFilter() {} diff --git a/io/transfer/transfer.go b/io/transfer/transfer.go index 1862c24e..00f0089f 100644 --- a/io/transfer/transfer.go +++ b/io/transfer/transfer.go @@ -2,10 +2,10 @@ // // The transfer protocol is as follows: // -// - Data sources use [SourceFilter] to receive [InitiateEvent]s when a drag -// is initiated, and [RequestEvent]s for each initiation of a data transfer. -// Sources respond to requests with [OfferCommand]. -// - Data targets use [TargetFilter] to receive [DataEvent]s for receiving data. +// - Data sources use [SourceFilter] to receive an [InitiateEvent] when a drag +// is initiated, and an [RequestEvent] for each initiation of a data transfer. +// Sources respond to requests with [OfferCmd]. +// - Data targets use [TargetFilter] to receive an [DataEvent] for receiving data. // The target must close the data event after use. // // When a user initiates a pointer-guided drag and drop transfer, the @@ -20,9 +20,7 @@ package transfer import ( "io" - "gioui.org/internal/ops" "gioui.org/io/event" - "gioui.org/op" ) // 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 // transfer is complete or cancelled. // 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 } func (OfferCmd) ImplementsCommand() {} -// SourceOp registers a tag as a data source for a MIME type. -// Use multiple SourceOps if a tag supports multiple types. -type SourceOp struct { - Tag event.Tag +// SourceFilter filters for any [RequestEvent] that match a MIME type +// as well as [InitiateEvent] and [CancelEvent]. +// Use multiple filters to offer multiple types. +type SourceFilter struct { // Type is the MIME type supported by this source. Type string } -// TargetOp registers a tag as a data target. -// Use multiple TargetOps if a tag supports multiple types. -type TargetOp struct { - Tag event.Tag +// TargetFilter filters for any [DataEvent] whose type matches a MIME type +// as well as [CancelEvent]. Use multiple filters to accept multiple types. +type TargetFilter struct { // Type is the MIME type accepted by this target. 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 // respond with an OfferCmd. type RequestEvent struct { @@ -99,3 +86,6 @@ type DataEvent struct { } func (DataEvent) ImplementsEvent() {} + +func (SourceFilter) ImplementsFilter() {} +func (TargetFilter) ImplementsFilter() {} diff --git a/layout/list.go b/layout/list.go index 9d8c568c..c34fc901 100644 --- a/layout/list.go +++ b/layout/list.go @@ -144,7 +144,25 @@ func (l *List) Dragging() bool { } 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.Position.Offset += d } @@ -332,25 +350,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { call := macro.Stop() defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() - 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)), - } - l.scroll.Add(ops, scrollRange) + l.scroll.Add(ops) call.Add(ops) return Dimensions{Size: dims} diff --git a/widget/dnd.go b/widget/dnd.go index 6ced658e..bac8a6ab 100644 --- a/widget/dnd.go +++ b/widget/dnd.go @@ -5,6 +5,7 @@ import ( "gioui.org/f32" "gioui.org/gesture" + "gioui.org/io/event" "gioui.org/io/pointer" "gioui.org/io/transfer" "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) d.drag.Add(gtx.Ops) - transfer.SourceOp{ - Tag: &d.handle, - Type: d.Type, - }.Add(gtx.Ops) + event.InputOp(gtx.Ops, &d.handle) stack.Pop() if drag != nil && d.drag.Pressed() { @@ -67,7 +65,7 @@ func (d *Draggable) Update(gtx layout.Context) (mime string, requested bool) { } 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 { return e.Type, true } diff --git a/widget/dnd_test.go b/widget/dnd_test.go index 998eec4c..af915388 100644 --- a/widget/dnd_test.go +++ b/widget/dnd_test.go @@ -5,6 +5,7 @@ import ( "testing" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/input" "gioui.org/io/pointer" "gioui.org/io/transfer" @@ -29,12 +30,11 @@ func TestDraggable(t *testing.T) { return layout.Dimensions{Size: gtx.Constraints.Min} }, nil) stack := clip.Rect{Max: dims.Size}.Push(gtx.Ops) - transfer.TargetOp{ - Tag: drag, - Type: drag.Type, - }.Add(gtx.Ops) + event.InputOp(gtx.Ops, drag) stack.Pop() + drag.Update(gtx) + r.Events(drag, transfer.TargetFilter{Type: drag.Type}) r.Frame(gtx.Ops) r.Queue( pointer.Event{ @@ -52,9 +52,11 @@ func TestDraggable(t *testing.T) { ) ofr := &offer{data: "hello"} drag.Offer(gtx, "file", ofr) + drag.Update(gtx) + r.Events(drag, transfer.TargetFilter{Type: drag.Type}) r.Frame(gtx.Ops) - evs := r.Events(drag) + evs := r.Events(drag, transfer.TargetFilter{Type: drag.Type}) if len(evs) != 2 { t.Fatalf("expected 2 event, got %d", len(evs)) } diff --git a/widget/editor.go b/widget/editor.go index 4ccf1b8a..32328fe9 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -225,7 +225,19 @@ func (e *Editor) processPointer(gtx layout.Context) { axis = gesture.Vertical 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 if e.SingleLine { 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. 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 switch ke := ke.(type) { case key.FocusEvent: @@ -609,7 +621,6 @@ func (e *Editor) layout(gtx layout.Context, textMaterial, selectMaterial op.Call e.scrollCaret = false e.text.ScrollToCaret() } - textDims := e.text.FullDimensions() visibleDims := e.text.Dimensions() 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) - var scrollRange image.Rectangle - 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.scroller.Add(gtx.Ops) e.clicker.Add(gtx.Ops) e.dragger.Add(gtx.Ops) diff --git a/widget/editor_test.go b/widget/editor_test.go index 43236b91..4ea9094e 100644 --- a/widget/editor_test.go +++ b/widget/editor_test.go @@ -900,7 +900,6 @@ g 2 4 6 8 g var tim time.Duration selected := func(start, end int) string { // Layout once with no events; populate e.lines. - gtx = gtx.Disabled() e.Layout(gtx, cache, font, fontSize, op.CallOp{}, op.CallOp{}) e.Events() // throw away any events from this layout diff --git a/widget/example_test.go b/widget/example_test.go index e4edb3aa..92ad89e4 100644 --- a/widget/example_test.go +++ b/widget/example_test.go @@ -5,8 +5,11 @@ package widget_test import ( "fmt" "image" + "io" + "strings" "gioui.org/f32" + "gioui.org/io/event" "gioui.org/io/input" "gioui.org/io/pointer" "gioui.org/io/transfer" @@ -79,7 +82,7 @@ func ExampleDraggable_Layout() { } // mime is the type used to match drag and drop operations. // It could be left empty in this example. - mime := "MyMime" + const mime = "MyMime" drag := &widget.Draggable{Type: mime} var drop int // 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. // Use the drag method for this. 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. @@ -102,17 +105,17 @@ func ExampleDraggable_Layout() { Min: image.Pt(20, 20), Max: image.Pt(40, 40), }.Push(gtx.Ops) - transfer.TargetOp{ - Tag: &drop, - Type: mime, // this must match the drag Type for the drop to succeed - }.Add(gtx.Ops) + event.InputOp(gtx.Ops, &drop) ds.Pop() + // 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) { case transfer.DataEvent: 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: // hello world } - -type offer struct { - Data string -} - -func (offer) Read([]byte) (int, error) { return 0, nil } -func (offer) Close() error { return nil }