diff --git a/layout/list.go b/layout/list.go index 85eb9114..5aa69e7a 100644 --- a/layout/list.go +++ b/layout/list.go @@ -69,6 +69,8 @@ type Position struct { // Offset is the distance in pixels from the top edge to the child at index // First. Offset int + // Count is the number of visible children. + Count int } const ( @@ -206,7 +208,8 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { for len(children) > 0 { sz := children[0].size mainSize := l.Axis.Convert(sz).X - if l.Position.Offset <= mainSize { + if l.Position.Offset < mainSize { + // First child is partially visible. break } l.Position.First++ @@ -226,6 +229,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { break } } + l.Position.Count = len(children) pos := -l.Position.Offset // ScrollToEnd lists are end aligned. if space := mainMax - size; l.ScrollToEnd && space > 0 { diff --git a/layout/list_test.go b/layout/list_test.go new file mode 100644 index 00000000..18213443 --- /dev/null +++ b/layout/list_test.go @@ -0,0 +1,102 @@ +// SPDX-License-Identifier: Unlicense OR MIT + +package layout + +import ( + "image" + "testing" + + "gioui.org/f32" + "gioui.org/io/event" + "gioui.org/io/pointer" + "gioui.org/io/router" + "gioui.org/op" +) + +func TestListPosition(t *testing.T) { + _s := func(e ...event.Event) []event.Event { return e } + r := new(router.Router) + gtx := Context{ + Ops: new(op.Ops), + Constraints: Constraints{ + Max: image.Pt(20, 10), + }, + Queue: r, + } + el := func(gtx Context, idx int) Dimensions { + return Dimensions{Size: image.Pt(10, 10)} + } + for _, tc := range []struct { + label string + num int + scroll []event.Event + first int + count int + }{ + {label: "no item"}, + {label: "1 visible 0 hidden", num: 1, count: 1}, + {label: "2 visible 0 hidden", num: 2, count: 2}, + {label: "2 visible 1 hidden", num: 3, count: 2}, + {label: "3 visible 0 hidden small scroll", num: 3, count: 3, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonLeft, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(5, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonLeft, + Type: pointer.Release, + Position: f32.Pt(5, 0), + }, + )}, + {label: "2 visible 1 hidden large scroll", num: 3, count: 2, first: 1, + scroll: _s( + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonLeft, + Type: pointer.Press, + Position: f32.Pt(0, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Type: pointer.Scroll, + Scroll: f32.Pt(10, 0), + }, + pointer.Event{ + Source: pointer.Mouse, + Buttons: pointer.ButtonLeft, + Type: pointer.Release, + Position: f32.Pt(15, 0), + }, + )}, + } { + t.Run(tc.label, func(t *testing.T) { + gtx.Ops.Reset() + + var list List + // Initialize the list. + list.Layout(gtx, tc.num, el) + // Generate the scroll events. + r.Frame(gtx.Ops) + r.Add(tc.scroll...) + // Let the list process the events. + list.Layout(gtx, tc.num, el) + + pos := list.Position + if got, want := pos.First, tc.first; got != want { + t.Errorf("List: invalid first position: got %v; want %v", got, want) + } + if got, want := pos.Count, tc.count; got != want { + t.Errorf("List: invalid number of visible children: got %v; want %v", got, want) + } + }) + } +}