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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2024-04-14 08:54:12 +02:00
parent 42ef3476cc
commit ee6cdec60b
7 changed files with 57 additions and 37 deletions
+5 -4
View File
@@ -271,12 +271,13 @@ func (s *Scroll) Stop() {
} }
// Update state and report the scroll distance along axis. // 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 total := 0
f := pointer.Filter{ f := pointer.Filter{
Target: s, Target: s,
Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel, Kinds: pointer.Press | pointer.Drag | pointer.Release | pointer.Scroll | pointer.Cancel,
ScrollBounds: bounds, ScrollX: scrollx,
ScrollY: scrolly,
} }
for { for {
evt, ok := q.Event(f) evt, ok := q.Event(f)
+4 -3
View File
@@ -251,9 +251,10 @@ func TestFocusScroll(t *testing.T) {
filters := []event.Filter{ filters := []event.Filter{
key.FocusFilter{Target: h}, key.FocusFilter{Target: h},
pointer.Filter{ pointer.Filter{
Target: h, Target: h,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rect(-100, -100, 100, 100), ScrollX: pointer.ScrollRange{Min: -100, Max: +100},
ScrollY: pointer.ScrollRange{Min: -100, Max: +100},
}, },
} }
events(r, -1, filters...) events(r, -1, filters...)
+7 -5
View File
@@ -72,7 +72,7 @@ type pointerHandler struct {
type pointerFilter struct { type pointerFilter struct {
kinds pointer.Kind kinds pointer.Kind
// min and max horizontal/vertical scroll // min and max horizontal/vertical scroll
scrollRange image.Rectangle scrollX, scrollY pointer.ScrollRange
sourceMimes []string sourceMimes []string
targetMimes []string targetMimes []string
@@ -297,7 +297,8 @@ func (p *pointerFilter) Add(f event.Filter) {
p.targetMimes = append(p.targetMimes, f.Type) p.targetMimes = append(p.targetMimes, f.Type)
case pointer.Filter: case pointer.Filter:
p.kinds = p.kinds | f.Kinds 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) { func (p *pointerFilter) Merge(p2 pointerFilter) {
p.kinds = p.kinds | p2.kinds 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.sourceMimes = append(p.sourceMimes, p2.sourceMimes...)
p.targetMimes = append(p.targetMimes, p2.targetMimes...) 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 // clampScroll splits a scroll distance in the remaining scroll and the
// scroll accepted by the filter. // scroll accepted by the filter.
func (p *pointerFilter) clampScroll(scroll f32.Point) (left, scrolled f32.Point) { 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.X, scrolled.X = clampSplit(scroll.X, p.scrollX.Min, p.scrollX.Max)
left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollRange.Min.Y, p.scrollRange.Max.Y) left.Y, scrolled.Y = clampSplit(scroll.Y, p.scrollY.Min, p.scrollY.Max)
return return
} }
+10 -9
View File
@@ -300,9 +300,9 @@ func TestPointerPriority(t *testing.T) {
r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops) r1 := clip.Rect(image.Rect(0, 0, 100, 100)).Push(&ops)
f1 := func(t event.Tag) event.Filter { f1 := func(t event.Tag) event.Filter {
return pointer.Filter{ return pointer.Filter{
Target: t, Target: t,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 100}}, ScrollX: pointer.ScrollRange{Max: 100},
} }
} }
events(&r, -1, f1(handler1)) 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) r2 := clip.Rect(image.Rect(0, 0, 100, 50)).Push(&ops)
f2 := func(t event.Tag) event.Filter { f2 := func(t event.Tag) event.Filter {
return pointer.Filter{ return pointer.Filter{
Target: t, Target: t,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Max: image.Point{X: 20}}, ScrollX: pointer.ScrollRange{Max: 20},
} }
} }
events(&r, -1, f2(handler2)) 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) r3 := clip.Rect(image.Rect(0, 100, 100, 200)).Push(&ops)
f3 := func(t event.Tag) event.Filter { f3 := func(t event.Tag) event.Filter {
return pointer.Filter{ return pointer.Filter{
Target: t, Target: t,
Kinds: pointer.Scroll, Kinds: pointer.Scroll,
ScrollBounds: image.Rectangle{Min: image.Point{X: -20, Y: -40}}, ScrollX: pointer.ScrollRange{Min: -20},
ScrollY: pointer.ScrollRange{Min: -40},
} }
} }
events(&r, -1, f3(handler3)) events(&r, -1, f3(handler3))
+19 -6
View File
@@ -3,7 +3,6 @@
package pointer package pointer
import ( import (
"image"
"strings" "strings"
"time" "time"
@@ -61,12 +60,19 @@ type Filter struct {
Target event.Tag Target event.Tag
// Kinds is a bitwise-or of event types to match. // Kinds is a bitwise-or of event types to match.
Kinds Kind Kinds Kind
// ScrollBounds describe the maximum scrollable distances in both // ScrollX and ScrollY constrain the range of scrolling events delivered
// axes. Specifically, any Event e delivered to Tag will satisfy // to Target. Specifically, any Event e delivered to Tag will satisfy
// //
// ScrollBounds.Min.X <= e.Scroll.X <= ScrollBounds.Max.X (horizontal axis) // ScrollX.Min <= e.Scroll.X <= ScrollX.Max (horizontal axis)
// ScrollBounds.Min.Y <= e.Scroll.Y <= ScrollBounds.Max.Y (vertical axis) // ScrollY.Min <= e.Scroll.Y <= ScrollY.Max (vertical axis)
ScrollBounds image.Rectangle 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. // GrabCmd requests a pointer grab on the pointer identified by ID.
@@ -219,6 +225,13 @@ const (
ButtonTertiary 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. // Push the current pass mode to the pass stack and set the pass mode.
func (p PassOp) Push(o *op.Ops) PassStack { func (p PassOp) Push(o *op.Ops) PassStack {
id, mid := ops.PushOp(&o.Internal, ops.PassStack) id, mid := ops.PushOp(&o.Internal, ops.PassStack)
+6 -4
View File
@@ -7,6 +7,7 @@ import (
"math" "math"
"gioui.org/gesture" "gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/op" "gioui.org/op"
"gioui.org/op/clip" "gioui.org/op/clip"
) )
@@ -158,11 +159,12 @@ func (l *List) update(gtx Context) {
max = 0 max = 0
} }
} }
scrollRange := image.Rectangle{ xrange := pointer.ScrollRange{Min: min, Max: max}
Min: l.Axis.Convert(image.Pt(min, 0)), yrange := pointer.ScrollRange{}
Max: l.Axis.Convert(image.Pt(max, 0)), 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.scrollDelta = d
l.Position.Offset += d l.Position.Offset += d
} }
+6 -6
View File
@@ -228,19 +228,19 @@ func (e *Editor) processPointer(gtx layout.Context) (EditorEvent, bool) {
axis = gesture.Vertical axis = gesture.Vertical
smin, smax = sbounds.Min.Y, sbounds.Max.Y smin, smax = sbounds.Min.Y, sbounds.Max.Y
} }
var scrollRange image.Rectangle var scrollX, scrollY pointer.ScrollRange
textDims := e.text.FullDimensions() textDims := e.text.FullDimensions()
visibleDims := e.text.Dimensions() visibleDims := e.text.Dimensions()
if e.SingleLine { if e.SingleLine {
scrollOffX := e.text.ScrollOff().X scrollOffX := e.text.ScrollOff().X
scrollRange.Min.X = min(-scrollOffX, 0) scrollX.Min = min(-scrollOffX, 0)
scrollRange.Max.X = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X)) scrollX.Max = max(0, textDims.Size.X-(scrollOffX+visibleDims.Size.X))
} else { } else {
scrollOffY := e.text.ScrollOff().Y scrollOffY := e.text.ScrollOff().Y
scrollRange.Min.Y = -scrollOffY scrollY.Min = -scrollOffY
scrollRange.Max.Y = max(0, textDims.Size.Y-(scrollOffY+visibleDims.Size.Y)) 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 var soff int
if e.SingleLine { if e.SingleLine {
e.text.ScrollRel(sdist, 0) e.text.ScrollRel(sdist, 0)