~eliasnaur/gio

5e1a662b940dd1177395e95999980f97c000d2c4 — pierre a month ago f3d75f3
io/pointer: support nested scrollables

Fixes #185.

Signed-off-by: pierre <pierre.curto@gmail.com>
M gesture/gesture.go => gesture/gesture.go +6 -7
@@ 10,6 10,7 @@ and scrolling.
package gesture

import (
	"image"
	"math"
	"runtime"
	"time"


@@ 205,11 206,12 @@ func (c *Click) Events(q event.Queue) []ClickEvent {
func (ClickEvent) ImplementsEvent() {}

// 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{
		Tag:   s,
		Grab:  s.grab,
		Types: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
		Tag:          s,
		Grab:         s.grab,
		Types:        pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll,
		ScrollBounds: bounds,
	}
	oph.Add(ops)
	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.grab = false
		case pointer.Scroll:
			if e.Priority < pointer.Foremost {
				continue
			}
			switch s.axis {
			case Horizontal:
				s.scroll += e.Scroll.X

M internal/opconst/ops.go => internal/opconst/ops.go +1 -1
@@ 46,7 46,7 @@ const (
	TypeColorLen           = 1 + 4
	TypeLinearGradientLen  = 1 + 8*2 + 4*2
	TypeAreaLen            = 1 + 1 + 4*4
	TypePointerInputLen    = 1 + 1 + 1
	TypePointerInputLen    = 1 + 1 + 1 + 2*4 + 2*4
	TypePassLen            = 1 + 1
	TypeClipboardReadLen   = 1
	TypeClipboardWriteLen  = 1

M io/pointer/pointer.go => io/pointer/pointer.go +21 -5
@@ 4,6 4,7 @@ package pointer

import (
	"encoding/binary"
	"fmt"
	"image"
	"strings"
	"time"


@@ 63,6 64,12 @@ type InputOp struct {
	Grab bool
	// Types is a bitwise-or of event types to receive.
	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.


@@ 195,16 202,25 @@ func (op CursorNameOp) Add(o *op.Ops) {
	data[0] = byte(opconst.TypeCursor)
}

func (h InputOp) Add(o *op.Ops) {
	if h.Tag == nil {
// 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")
	}
	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)
	if h.Grab {
	if op.Grab {
		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) {

M io/router/pointer.go => io/router/pointer.go +54 -1
@@ 4,6 4,7 @@ package router

import (
	"encoding/binary"
	"image"

	"gioui.org/f32"
	"gioui.org/internal/opconst"


@@ 59,6 60,8 @@ type pointerHandler struct {
	active    bool
	wantsGrab bool
	types     pointer.Type
	// min and max horizontal/vertical scroll
	scrollRange image.Rectangle
}

type areaOp struct {


@@ 155,6 158,17 @@ func (q *pointerQueue) collectHandlers(r *ops.Reader, events *handlerEvents) {
			h.area = state.area
			h.wantsGrab = h.wantsGrab || op.Grab
			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:
			q.cursors = append(q.cursors, cursorNode{
				name: encOp.Refs[0].(pointer.CursorName),


@@ 320,7 334,11 @@ func (q *pointerQueue) Push(e pointer.Event, events *handlerEvents) {
	if e.Type == pointer.Press {
		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)
	}
	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) {
	q.scratch = q.scratch[:0]
	q.opHit(&q.scratch, e.Position)


@@ 454,3 497,13 @@ func (op *areaOp) Hit(pos f32.Point) bool {
		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
}

M io/router/pointer_test.go => io/router/pointer_test.go +46 -4
@@ 185,39 185,73 @@ func TestPointerTypes(t *testing.T) {
func TestPointerPriority(t *testing.T) {
	handler1 := new(int)
	handler2 := new(int)
	handler3 := new(int)
	var ops op.Ops

	st := op.Save(&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.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
	r.Frame(&ops)
	r.Queue(
		// Hit both handlers.
		// Hit handler 1 and 2.
		pointer.Event{
			Type:     pointer.Scroll,
			Position: f32.Pt(50, 25),
			Scroll:   f32.Pt(50, 0),
		},
		// Hit handler 1.
		pointer.Event{
			Type:     pointer.Scroll,
			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.
		pointer.Event{
			Type:     pointer.Scroll,
			Position: f32.Pt(50, 125),
			Position: f32.Pt(50, 225),
		},
	)

	hev1 := r.Events(handler1)
	hev2 := r.Events(handler2)
	hev3 := r.Events(handler3)
	assertEventSequence(t, hev1, pointer.Cancel, pointer.Scroll, 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, 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) {


@@ 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) {
	// Set this to the number of overlapping handlers that you want to
	// evaluate performance for. Typical values for the example applications

M layout/list.go => layout/list.go +19 -1
@@ 285,7 285,25 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
	call := macro.Stop()
	defer op.Save(ops).Load()
	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)
	return Dimensions{Size: dims}
}

M widget/editor.go => widget/editor.go +11 -1
@@ 547,7 547,17 @@ func (e *Editor) layout(gtx layout.Context) layout.Dimensions {
	r.Max.X += pointerPadding
	pointer.Rect(r).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.dragger.Add(gtx.Ops)
	e.caret.on = false