mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
io/pointer: support nested scrollables
Fixes #185. Signed-off-by: pierre <pierre.curto@gmail.com>
This commit is contained in:
+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
|
||||
|
||||
@@ -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
|
||||
|
||||
+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) {
|
||||
|
||||
+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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
+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}
|
||||
}
|
||||
|
||||
+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
|
||||
|
||||
Reference in New Issue
Block a user