io/pointer: support nested scrollables

Fixes #185.

Signed-off-by: pierre <pierre.curto@gmail.com>
This commit is contained in:
pierre
2021-03-31 08:25:08 +02:00
committed by Elias Naur
parent f3d75f38a9
commit 5e1a662b94
7 changed files with 158 additions and 20 deletions
+3 -4
View File
@@ -10,6 +10,7 @@ and scrolling.
package gesture package gesture
import ( import (
"image"
"math" "math"
"runtime" "runtime"
"time" "time"
@@ -205,11 +206,12 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
func (ClickEvent) ImplementsEvent() {} func (ClickEvent) ImplementsEvent() {}
// Add the handler to the operation list to receive scroll events. // Add the handler to the operation list to receive scroll events.
func (s *Scroll) Add(ops *op.Ops) { func (s *Scroll) Add(ops *op.Ops, bounds image.Rectangle) {
oph := pointer.InputOp{ oph := pointer.InputOp{
Tag: s, Tag: s,
Grab: s.grab, Grab: s.grab,
Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll, Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
ScrollBounds: bounds,
} }
oph.Add(ops) oph.Add(ops)
if s.flinger.Active() { if s.flinger.Active() {
@@ -265,9 +267,6 @@ func (s *Scroll) Scroll(cfg unit.Metric, q event.Queue, t time.Time, axis Axis)
s.dragging = false s.dragging = false
s.grab = false s.grab = false
case pointer.Scroll: case pointer.Scroll:
if e.Priority < pointer.Foremost {
continue
}
switch s.axis { switch s.axis {
case Horizontal: case Horizontal:
s.scroll += e.Scroll.X s.scroll += e.Scroll.X
+1 -1
View File
@@ -46,7 +46,7 @@ const (
TypeColorLen = 1 + 4 TypeColorLen = 1 + 4
TypeLinearGradientLen = 1 + 8*2 + 4*2 TypeLinearGradientLen = 1 + 8*2 + 4*2
TypeAreaLen = 1 + 1 + 4*4 TypeAreaLen = 1 + 1 + 4*4
TypePointerInputLen = 1 + 1 + 1 TypePointerInputLen = 1 + 1 + 1 + 2*4 + 2*4
TypePassLen = 1 + 1 TypePassLen = 1 + 1
TypeClipboardReadLen = 1 TypeClipboardReadLen = 1
TypeClipboardWriteLen = 1 TypeClipboardWriteLen = 1
+21 -5
View File
@@ -4,6 +4,7 @@ package pointer
import ( import (
"encoding/binary" "encoding/binary"
"fmt"
"image" "image"
"strings" "strings"
"time" "time"
@@ -63,6 +64,12 @@ type InputOp struct {
Grab bool Grab bool
// Types is a bitwise-or of event types to receive. // Types is a bitwise-or of event types to receive.
Types Type Types Type
// ScrollBounds describe the maximum scrollable distances in both
// axes. Specifically, any Event e delivered to Tag will satisfy
//
// ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis)
// ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis)
ScrollBounds image.Rectangle
} }
// PassOp sets the pass-through mode. // PassOp sets the pass-through mode.
@@ -195,16 +202,25 @@ func (op CursorNameOp) Add(o *op.Ops) {
data[0] = byte(opconst.TypeCursor) data[0] = byte(opconst.TypeCursor)
} }
func (h InputOp) Add(o *op.Ops) { // Add panics if the scroll range does not contain zero.
if h.Tag == nil { func (op InputOp) Add(o *op.Ops) {
if op.Tag == nil {
panic("Tag must be non-nil") panic("Tag must be non-nil")
} }
data := o.Write1(opconst.TypePointerInputLen, h.Tag) 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))
}
data := o.Write1(opconst.TypePointerInputLen, op.Tag)
data[0] = byte(opconst.TypePointerInput) data[0] = byte(opconst.TypePointerInput)
if h.Grab { if op.Grab {
data[1] = 1 data[1] = 1
} }
data[2] = byte(h.Types) data[2] = byte(op.Types)
bo := binary.LittleEndian
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 (op PassOp) Add(o *op.Ops) { func (op PassOp) Add(o *op.Ops) {
+54 -1
View File
@@ -4,6 +4,7 @@ package router
import ( import (
"encoding/binary" "encoding/binary"
"image"
"gioui.org/f32" "gioui.org/f32"
"gioui.org/internal/opconst" "gioui.org/internal/opconst"
@@ -59,6 +60,8 @@ type pointerHandler struct {
active bool active bool
wantsGrab bool wantsGrab bool
types pointer.Type types pointer.Type
// min and max horizontal/vertical scroll
scrollRange image.Rectangle
} }
type areaOp struct { type areaOp struct {
@@ -155,6 +158,17 @@ func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents) {
h.area = state.area h.area = state.area
h.wantsGrab = h.wantsGrab || op.Grab h.wantsGrab = h.wantsGrab || op.Grab
h.types = h.types | op.Types h.types = h.types | op.Types
bo := binary.LittleEndian.Uint32
h.scrollRange = image.Rectangle{
Min: image.Point{
X: int(int32(bo(encOp.Data[3:]))),
Y: int(int32(bo(encOp.Data[7:]))),
},
Max: image.Point{
X: int(int32(bo(encOp.Data[11:]))),
Y: int(int32(bo(encOp.Data[15:]))),
},
}
case opconst.TypeCursor: case opconst.TypeCursor:
q.cursors = append(q.cursors, cursorNode{ q.cursors = append(q.cursors, cursorNode{
name: encOp.Refs[0].(pointer.CursorName), name: encOp.Refs[0].(pointer.CursorName),
@@ -320,7 +334,11 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
if e.Type == pointer.Press { if e.Type == pointer.Press {
p.pressed = true p.pressed = true
} }
if e.Type != pointer.Release { switch e.Type {
case pointer.Release:
case pointer.Scroll:
q.deliverScrollEvent(p, events, e)
default:
q.deliverEvent(p, events, e) q.deliverEvent(p, events, e)
} }
if !p.pressed && len(p.entered) == 0 { if !p.pressed && len(p.entered) == 0 {
@@ -350,6 +368,31 @@ func (q *pointerQueue) deliverEvent(p *pointerInfo, events *handlerEvents, e poi
} }
} }
func (q *pointerQueue) deliverScrollEvent(p *pointerInfo, events *handlerEvents, e pointer.Event) {
foremost := true
if p.pressed && len(p.handlers) == 1 {
e.Priority = pointer.Grabbed
foremost = false
}
var sx, sy = e.Scroll.X, e.Scroll.Y
for _, k := range p.handlers {
if sx == 0 && sy == 0 {
return
}
h := q.handlers[k]
// Distribute the scroll to the handler based on its ScrollRange.
sx, e.Scroll.X = setScrollEvent(sx, h.scrollRange.Min.X, h.scrollRange.Max.X)
sy, e.Scroll.Y = setScrollEvent(sy, h.scrollRange.Min.Y, h.scrollRange.Max.Y)
e := e
if foremost {
foremost = false
e.Priority = pointer.Foremost
}
e.Position = q.invTransform(h.area, e.Position)
events.Add(k, e)
}
}
func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) { func (q *pointerQueue) deliverEnterLeaveEvents(p *pointerInfo, events *handlerEvents, e pointer.Event) {
q.scratch = q.scratch[:0] q.scratch = q.scratch[:0]
q.opHit(&q.scratch, e.Position) q.opHit(&q.scratch, e.Position)
@@ -454,3 +497,13 @@ func (op *areaOp) Hit(pos f32.Point) bool {
panic("invalid area kind") panic("invalid area kind")
} }
} }
func setScrollEvent(scroll float32, min, max int) (left, scrolled float32) {
if v := float32(max); scroll > v {
return scroll - v, v
}
if v := float32(min); scroll < v {
return scroll - v, v
}
return 0, scroll
}
+46 -4
View File
@@ -185,39 +185,73 @@ func TestPointerTypes(t *testing.T) {
func TestPointerPriority(t *testing.T) { func TestPointerPriority(t *testing.T) {
handler1 := new(int) handler1 := new(int)
handler2 := new(int) handler2 := new(int)
handler3 := new(int)
var ops op.Ops var ops op.Ops
st := op.Save(&ops)
pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops) pointer.Rect(image.Rect(0, 0, 100, 100)).Add(&ops)
pointer.InputOp{Tag: handler1, Types: pointer.Scroll}.Add(&ops) pointer.InputOp{
Tag: handler1,
Types: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}},
}.Add(&ops)
pointer.Rect(image.Rect(0, 0, 100, 50)).Add(&ops) pointer.Rect(image.Rect(0, 0, 100, 50)).Add(&ops)
pointer.InputOp{Tag: handler2, Types: pointer.Scroll}.Add(&ops) pointer.InputOp{
Tag: handler2,
Types: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}},
}.Add(&ops)
st.Load()
pointer.Rect(image.Rect(0, 100, 100, 200)).Add(&ops)
pointer.InputOp{
Tag: handler3,
Types: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}},
}.Add(&ops)
var r Router var r Router
r.Frame(&ops) r.Frame(&ops)
r.Queue( r.Queue(
// Hit both handlers. // Hit handler 1 and 2.
pointer.Event{ pointer.Event{
Type: pointer.Scroll, Type: pointer.Scroll,
Position: f32.Pt(50, 25), Position: f32.Pt(50, 25),
Scroll: f32.Pt(50, 0),
}, },
// Hit handler 1. // Hit handler 1.
pointer.Event{ pointer.Event{
Type: pointer.Scroll, Type: pointer.Scroll,
Position: f32.Pt(50, 75), Position: f32.Pt(50, 75),
Scroll: f32.Pt(50, 50),
},
// Hit handler 3.
pointer.Event{
Type: pointer.Scroll,
Position: f32.Pt(50, 150),
Scroll: f32.Pt(-30, -30),
}, },
// Hit no handlers. // Hit no handlers.
pointer.Event{ pointer.Event{
Type: pointer.Scroll, Type: pointer.Scroll,
Position: f32.Pt(50, 125), Position: f32.Pt(50, 225),
}, },
) )
hev1 := r.Events(handler1) hev1 := r.Events(handler1)
hev2 := r.Events(handler2) hev2 := r.Events(handler2)
hev3 := r.Events(handler3)
assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll) assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, pointer.Scroll)
assertEventSequence(t, hev2, pointer.Cancel, pointer.Scroll) assertEventSequence(t, hev2, pointer.Cancel, pointer.Scroll)
assertEventSequence(t, hev3, pointer.Cancel, pointer.Scroll)
assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared, pointer.Foremost) assertEventPriorities(t, hev1, pointer.Shared, pointer.Shared, pointer.Foremost)
assertEventPriorities(t, hev2, 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))
} }
func TestPointerEnterLeave(t *testing.T) { func TestPointerEnterLeave(t *testing.T) {
@@ -663,6 +697,14 @@ func assertEventPriorities(t *testing.T, events []event.Event, prios ...pointer.
} }
} }
// assertScrollEvent checks that the event scrolling amount matches the supplied value.
func assertScrollEvent(t *testing.T, ev event.Event, scroll f32.Point) {
t.Helper()
if got, want := ev.(pointer.Event).Scroll, scroll; got != want {
t.Errorf("got %v; want %v", got, want)
}
}
func BenchmarkRouterAdd(b *testing.B) { func BenchmarkRouterAdd(b *testing.B) {
// Set this to the number of overlapping handlers that you want to // Set this to the number of overlapping handlers that you want to
// evaluate performance for. Typical values for the example applications // evaluate performance for. Typical values for the example applications
+19 -1
View File
@@ -285,7 +285,25 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
call := macro.Stop() call := macro.Stop()
defer op.Save(ops).Load() defer op.Save(ops).Load()
pointer.Rect(image.Rectangle{Max: dims}).Add(ops) pointer.Rect(image.Rectangle{Max: dims}).Add(ops)
l.scroll.Add(ops)
var min, max int
if o := l.Position.Offset; o > 0 {
// Use the size of the invisible part as scroll boundary.
min = -o
} else if l.Position.First > 0 {
min = -inf
}
if o := l.Position.OffsetLast; o < 0 {
max = -o
} else if l.Position.First+l.Position.Count < l.len {
max = inf
}
scrollRange := image.Rectangle{
Min: l.Axis.Convert(image.Pt(min, 0)),
Max: l.Axis.Convert(image.Pt(max, 0)),
}
l.scroll.Add(ops, scrollRange)
call.Add(ops) call.Add(ops)
return Dimensions{Size: dims} return Dimensions{Size: dims}
} }
+11 -1
View File
@@ -547,7 +547,17 @@ func (e *Editor) layout(gtx layout.Context) layout.Dimensions {
r.Max.X += pointerPadding r.Max.X += pointerPadding
pointer.Rect(r).Add(gtx.Ops) pointer.Rect(r).Add(gtx.Ops)
pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops) pointer.CursorNameOp{Name: pointer.CursorText}.Add(gtx.Ops)
e.scroller.Add(gtx.Ops)
var scrollRange image.Rectangle
if e.SingleLine {
scrollRange.Min.X = -e.scrollOff.X
scrollRange.Max.X = max(0, e.dims.Size.X-(e.scrollOff.X+e.viewSize.X))
} else {
scrollRange.Min.Y = -e.scrollOff.Y
scrollRange.Max.Y = max(0, e.dims.Size.Y-(e.scrollOff.Y+e.viewSize.Y))
}
e.scroller.Add(gtx.Ops, scrollRange)
e.clicker.Add(gtx.Ops) e.clicker.Add(gtx.Ops)
e.dragger.Add(gtx.Ops) e.dragger.Add(gtx.Ops)
e.caret.on = false e.caret.on = false