From 508330e818ecca62ad29f334e193b332a4251d38 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Wed, 30 Mar 2022 19:21:29 +0200 Subject: [PATCH] layout: layout one invisible child at each end of a List A recent change added automatic scrolling to move focused widgets into view. This change modifies List to layout an extra child at each of its ends, to enable focus to move to them and trigger automatic scrolling of the list. For https://github.com/tailscale/tailscale/issues/4278. Signed-off-by: Elias Naur --- layout/list.go | 43 ++++++++++++++++++++++++++++++++++++++----- layout/list_test.go | 18 ++++++++++++++++++ 2 files changed, 56 insertions(+), 5 deletions(-) diff --git a/layout/list.go b/layout/list.go index 8e451648..69beabb8 100644 --- a/layout/list.go +++ b/layout/list.go @@ -181,12 +181,25 @@ func (l *List) nextDir() iterationDir { if l.Position.Offset < 0 && l.Position.First == 0 { l.Position.Offset = 0 } + // Lay out an extra (invisible) child at each end to enable focus to + // move to them, triggering automatic scroll. + firstSize, lastSize := 0, 0 + if len(l.children) > 0 { + if l.Position.First > 0 { + firstChild := l.children[0] + firstSize = l.Axis.Convert(firstChild.size).X + } + if last < l.len { + lastChild := l.children[len(l.children)-1] + lastSize = l.Axis.Convert(lastChild.size).X + } + } switch { case len(l.children) == l.len: return iterateNone - case l.maxSize-l.Position.Offset < vsize: + case l.maxSize-l.Position.Offset-lastSize < vsize: return iterateForward - case l.Position.Offset < 0: + case l.Position.Offset-firstSize < 0: return iterateBackward } return iterateNone @@ -219,9 +232,11 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { } mainMin, mainMax := l.Axis.mainConstraint(l.cs) children := l.children - // Skip invisible children + var first scrollChild + // Skip invisible children. for len(children) > 0 { - sz := children[0].size + child := children[0] + sz := child.size mainSize := l.Axis.Convert(sz).X if l.Position.Offset < mainSize { // First child is partially visible. @@ -229,10 +244,12 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { } l.Position.First++ l.Position.Offset -= mainSize + first = child children = children[1:] } size := -l.Position.Offset var maxCross int + var last scrollChild for i, child := range children { sz := l.Axis.Convert(child.size) if c := sz.Y; c > maxCross { @@ -240,6 +257,9 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { } size += sz.X if size >= mainMax { + if i < len(children)-1 { + last = children[i+1] + } children = children[:i+1] break } @@ -251,7 +271,7 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 { pos += space } - for _, child := range children { + layout := func(child scrollChild) { sz := l.Axis.Convert(child.size) var cross int switch l.Alignment { @@ -281,6 +301,19 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { cl.Pop() pos += childSize } + // Lay out leading invisible child. + if first != (scrollChild{}) { + sz := l.Axis.Convert(first.size) + pos -= sz.X + layout(first) + } + for _, child := range children { + layout(child) + } + // Lay out trailing invisible child. + if last != (scrollChild{}) { + layout(last) + } atStart := l.Position.First == 0 && l.Position.Offset <= 0 atEnd := l.Position.First+len(children) == l.len && mainMax >= pos if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { diff --git a/layout/list_test.go b/layout/list_test.go index eae2ec9f..1ba2691f 100644 --- a/layout/list_test.go +++ b/layout/list_test.go @@ -140,3 +140,21 @@ func TestListPosition(t *testing.T) { }) } } + +func TestExtraChildren(t *testing.T) { + var l List + l.Position.First = 1 + gtx := Context{ + Ops: new(op.Ops), + Constraints: Exact(image.Pt(10, 10)), + } + count := 0 + const all = 3 + l.Layout(gtx, all, func(gtx Context, idx int) Dimensions { + count++ + return Dimensions{Size: image.Pt(10, 10)} + }) + if count != all { + t.Errorf("laid out %d of %d children", count, all) + } +}