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 <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-03-30 19:21:29 +02:00
parent a699fb89ac
commit 508330e818
2 changed files with 56 additions and 5 deletions
+38 -5
View File
@@ -181,12 +181,25 @@ func (l *List) nextDir() iterationDir {
if l.Position.Offset < 0 && l.Position.First == 0 { if l.Position.Offset < 0 && l.Position.First == 0 {
l.Position.Offset = 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 { switch {
case len(l.children) == l.len: case len(l.children) == l.len:
return iterateNone return iterateNone
case l.maxSize-l.Position.Offset < vsize: case l.maxSize-l.Position.Offset-lastSize < vsize:
return iterateForward return iterateForward
case l.Position.Offset < 0: case l.Position.Offset-firstSize < 0:
return iterateBackward return iterateBackward
} }
return iterateNone return iterateNone
@@ -219,9 +232,11 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
} }
mainMin, mainMax := l.Axis.mainConstraint(l.cs) mainMin, mainMax := l.Axis.mainConstraint(l.cs)
children := l.children children := l.children
// Skip invisible children var first scrollChild
// Skip invisible children.
for len(children) > 0 { for len(children) > 0 {
sz := children[0].size child := children[0]
sz := child.size
mainSize := l.Axis.Convert(sz).X mainSize := l.Axis.Convert(sz).X
if l.Position.Offset < mainSize { if l.Position.Offset < mainSize {
// First child is partially visible. // 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.First++
l.Position.Offset -= mainSize l.Position.Offset -= mainSize
first = child
children = children[1:] children = children[1:]
} }
size := -l.Position.Offset size := -l.Position.Offset
var maxCross int var maxCross int
var last scrollChild
for i, child := range children { for i, child := range children {
sz := l.Axis.Convert(child.size) sz := l.Axis.Convert(child.size)
if c := sz.Y; c > maxCross { if c := sz.Y; c > maxCross {
@@ -240,6 +257,9 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
} }
size += sz.X size += sz.X
if size >= mainMax { if size >= mainMax {
if i < len(children)-1 {
last = children[i+1]
}
children = children[:i+1] children = children[:i+1]
break 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 { if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 {
pos += space pos += space
} }
for _, child := range children { layout := func(child scrollChild) {
sz := l.Axis.Convert(child.size) sz := l.Axis.Convert(child.size)
var cross int var cross int
switch l.Alignment { switch l.Alignment {
@@ -281,6 +301,19 @@ func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
cl.Pop() cl.Pop()
pos += childSize 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 atStart := l.Position.First == 0 && l.Position.Offset <= 0
atEnd := l.Position.First+len(children) == l.len && mainMax >= pos atEnd := l.Position.First+len(children) == l.len && mainMax >= pos
if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
+18
View File
@@ -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)
}
}