diff --git a/gesture/gesture.go b/gesture/gesture.go index c43b3210..6f1c04b0 100644 --- a/gesture/gesture.go +++ b/gesture/gesture.go @@ -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 diff --git a/internal/opconst/ops.go b/internal/opconst/ops.go index 3c2ab62c..db9dd8dc 100644 --- a/internal/opconst/ops.go +++ b/internal/opconst/ops.go @@ -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 diff --git a/io/pointer/pointer.go b/io/pointer/pointer.go index bc363873..b495f06d 100644 --- a/io/pointer/pointer.go +++ b/io/pointer/pointer.go @@ -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) { diff --git a/io/router/pointer.go b/io/router/pointer.go index b1cf73bb..9d7ccdab 100644 --- a/io/router/pointer.go +++ b/io/router/pointer.go @@ -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 +} diff --git a/io/router/pointer_test.go b/io/router/pointer_test.go index b2b0a930..bf576c56 100644 --- a/io/router/pointer_test.go +++ b/io/router/pointer_test.go @@ -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 diff --git a/layout/list.go b/layout/list.go index 1614ea14..07858a6a 100644 --- a/layout/list.go +++ b/layout/list.go @@ -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} } diff --git a/widget/editor.go b/widget/editor.go index 99c66b96..19aef467 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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