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 }