From ee6cdec60b8356f95036fa3a749a6d95f8866163 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Sun, 14 Apr 2024 08:54:12 +0200 Subject: [PATCH] io/pointer: [API] split scroll bounds into two separate axes A single image.Rectangle for the scroll bounds introduced a subtle issue with zero area rectangles (see #572). To avoid that and similar issues, split the bounds into two separate one-dimensional ranges. Fixes: https://todo.sr.ht/~eliasnaur/gio/572 Signed-off-by: Elias Naur --- gesture/gesture.go | 9 +++++---- io/input/key_test.go | 7 ++++--- io/input/pointer.go | 12 +++++++----- io/input/pointer_test.go | 19 ++++++++++--------- io/pointer/pointer.go | 25 +++++++++++++++++++------ layout/list.go | 10 ++++++---- widget/editor.go | 12 ++++++------ 7 files changed, 57 insertions(+), 37 deletions(-) diff --git a/gesture/gesture.go b/gesture/gesture.go index 70e87707..0800a53c 100644 --- a/gesture/gesture.go +++ b/gesture/gesture.go @@ -271,12 +271,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, bounds image.Rectangle) int { +func (s *Scroll) Update(cfg unit.Metric, q input.Source, t time.Time, axis Axis, scrollx, scrolly pointer.ScrollRange) int { total := 0 f := pointer.Filter{ - Target: s, - Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel, - ScrollBounds: bounds, + Target: s, + Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel, + ScrollX: scrollx, + ScrollY: scrolly, } for { evt, ok := q.Event(f) diff --git a/io/input/key_test.go b/io/input/key_test.go index bc6c13e9..2e8c35c1 100644 --- a/io/input/key_test.go +++ b/io/input/key_test.go @@ -251,9 +251,10 @@ func TestFocusScroll(t *testing.T) { filters := []event.Filter{ key.FocusFilter{Target: h}, pointer.Filter{ - Target: h, - Kinds: pointer.Scroll, - ScrollBounds: image.Rect(-100, -100, 100, 100), + Target: h, + Kinds: pointer.Scroll, + ScrollX: pointer.ScrollRange{Min: -100, Max: +100}, + ScrollY: pointer.ScrollRange{Min: -100, Max: +100}, }, } events(r, -1, filters...) diff --git a/io/input/pointer.go b/io/input/pointer.go index cc90f3f8..e084e780 100644 --- a/io/input/pointer.go +++ b/io/input/pointer.go @@ -72,7 +72,7 @@ type pointerHandler struct { type pointerFilter struct { kinds pointer.Kind // min and max horizontal/vertical scroll - scrollRange image.Rectangle + scrollX, scrollY pointer.ScrollRange sourceMimes []string targetMimes []string @@ -297,7 +297,8 @@ func (p *pointerFilter) Add(f event.Filter) { p.targetMimes = append(p.targetMimes, f.Type) case pointer.Filter: p.kinds = p.kinds | f.Kinds - p.scrollRange = p.scrollRange.Union(f.ScrollBounds) + p.scrollX = p.scrollX.Union(f.ScrollX) + p.scrollY = p.scrollY.Union(f.ScrollY) } } @@ -325,7 +326,8 @@ func (p *pointerFilter) Matches(e event.Event) bool { func (p *pointerFilter) Merge(p2 pointerFilter) { p.kinds = p.kinds | p2.kinds - p.scrollRange = p.scrollRange.Union(p2.scrollRange) + p.scrollX = p.scrollX.Union(p2.scrollX) + p.scrollY = p.scrollY.Union(p2.scrollY) p.sourceMimes = append(p.sourceMimes, p2.sourceMimes...) p.targetMimes = append(p.targetMimes, p2.targetMimes...) } @@ -333,8 +335,8 @@ func (p *pointerFilter) Merge(p2 pointerFilter) { // clampScroll splits a scroll distance in the remaining scroll and the // scroll accepted by the filter. func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) { - left.X, scrolled.X = clampSplit(scroll.X, p.scrollRange.Min.X, p.scrollRange.Max.X) - left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y) + left.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max) + left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max) return } diff --git a/io/input/pointer_test.go b/io/input/pointer_test.go index 16ac9628..4d9e2505 100644 --- a/io/input/pointer_test.go +++ b/io/input/pointer_test.go @@ -300,9 +300,9 @@ func TestPointerPriority(t *testing.T) { r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) f1 := func(t event.Tag) event.Filter { return pointer.Filter{ - Target: t, - Kinds: pointer.Scroll, - ScrollBounds: image.Rectangle{Max: image.Point{X: 100}}, + Target: t, + Kinds: pointer.Scroll, + ScrollX: pointer.ScrollRange{Max: 100}, } } events(&r, -1, f1(handler1)) @@ -311,9 +311,9 @@ func TestPointerPriority(t *testing.T) { r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops) f2 := func(t event.Tag) event.Filter { return pointer.Filter{ - Target: t, - Kinds: pointer.Scroll, - ScrollBounds: image.Rectangle{Max: image.Point{X: 20}}, + Target: t, + Kinds: pointer.Scroll, + ScrollX: pointer.ScrollRange{Max: 20}, } } events(&r, -1, f2(handler2)) @@ -324,9 +324,10 @@ func TestPointerPriority(t *testing.T) { r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops) f3 := func(t event.Tag) event.Filter { return pointer.Filter{ - Target: t, - Kinds: pointer.Scroll, - ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}}, + Target: t, + Kinds: pointer.Scroll, + ScrollX: pointer.ScrollRange{Min: -20}, + ScrollY: pointer.ScrollRange{Min: -40}, } } events(&r, -1, f3(handler3)) diff --git a/io/pointer/pointer.go b/io/pointer/pointer.go index 8b9b42fb..45625991 100644 --- a/io/pointer/pointer.go +++ b/io/pointer/pointer.go @@ -3,7 +3,6 @@ package pointer import ( - "image" "strings" "time" @@ -61,12 +60,19 @@ type Filter struct { Target event.Tag // 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 + // ScrollX and ScrollY constrain the range of scrolling events delivered + // to Target. 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 + // ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis) + // ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis) + ScrollX ScrollRange + ScrollY ScrollRange +} + +// ScrollRange describes the range of scrolling distances in an +// axis. +type ScrollRange struct { + Min, Max int } // GrabCmd requests a pointer grab on the pointer identified by ID. @@ -219,6 +225,13 @@ const ( ButtonTertiary ) +func (s ScrollRange) Union(s2 ScrollRange) ScrollRange { + return ScrollRange{ + Min: min(s.Min, s2.Min), + Max: max(s.Max, s2.Max), + } +} + // Push the current pass mode to the pass stack and set the pass mode. func (p PassOp) Push(o *op.Ops) PassStack { id, mid := ops.PushOp(&o.Internal, ops.PassStack) diff --git a/layout/list.go b/layout/list.go index c34fc901..3cd7cea4 100644 --- a/layout/list.go +++ b/layout/list.go @@ -7,6 +7,7 @@ import ( "math" "gioui.org/gesture" + "gioui.org/io/pointer" "gioui.org/op" "gioui.org/op/clip" ) @@ -158,11 +159,12 @@ func (l *List) update(gtx Context) { max = 0 } } - scrollRange := image.Rectangle{ - Min: l.Axis.Convert(image.Pt(min, 0)), - Max: l.Axis.Convert(image.Pt(max, 0)), + xrange := pointer.ScrollRange{Min: min, Max: max} + yrange := pointer.ScrollRange{} + if l.Axis == Vertical { + xrange, yrange = yrange, xrange } - d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange) + d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), xrange, yrange) l.scrollDelta = d l.Position.Offset += d } diff --git a/widget/editor.go b/widget/editor.go index c43e794b..aa3ae444 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -228,19 +228,19 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) { axis = gesture.Vertical smin, smax = sbounds.Min.Y, sbounds.Max.Y } - var scrollRange image.Rectangle + var scrollX, scrollY pointer.ScrollRange 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)) + scrollX.Min = min(-scrollOffX, 0) + scrollX.Max = 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)) + scrollY.Min = -scrollOffY + scrollY.Max = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y)) } - sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollRange) + sdist := e.scroller.Update(gtx.Metric, gtx.Source, gtx.Now, axis, scrollX, scrollY) var soff int if e.SingleLine { e.text.ScrollRel(sdist, 0)