ui/layout: rewrite List

Inverted lists used to behave as if its top and bottom edges were
flipped. That was easy but also wrong: when the underlying children
changed size, they would move relative to the top edge of the list.

As illustrated by issue gio#34, Invert should only do two things:

- End lign lists smaller than the containing area.
- Scroll to end, but only as long as the user hasn't scrolled away.

List also had a bug where it didn't handle shrinking lists, so
this change rewrites List to fix that bug, fix Invert behaviour and
hopefully be a little simpler.

Fixes gio#34
This commit is contained in:
Elias Naur
2019-09-05 16:39:56 +02:00
parent 063b4a5659
commit 1529f20eb7
+82 -68
View File
@@ -22,35 +22,41 @@ type scrollChild struct {
// the subsection. // the subsection.
type List struct { type List struct {
Axis Axis Axis Axis
// Invert inverts a List so it is anchored from its end. // Inverted lists stay scrolled to the far end position
// until the user scrolls away.
Invert bool Invert bool
// Alignment is the cross axis alignment. // Alignment is the cross axis alignment of list elements.
Alignment Alignment Alignment Alignment
// The distance scrolled since last call to Init. // Distance is the difference in scroll position
// since the last call to Init.
Distance int Distance int
// beforeEnd tracks whether the List position is before
// the very end.
beforeEnd bool
config ui.Config config ui.Config
ops *ui.Ops ops *ui.Ops
queue input.Queue queue input.Queue
macro ui.MacroOp macro ui.MacroOp
child ui.MacroOp child ui.MacroOp
scroll gesture.Scroll scroll gesture.Scroll
scrollDir int scrollDelta int
offset int // first is the index of the first visible child.
first int first int
// offset is the signed distance from the top edge
// to the child with index first.
offset int
cs Constraints cs Constraints
len int len int
// maxSize is the total size of visible children.
maxSize int maxSize int
children []scrollChild children []scrollChild
dir iterationDir dir iterationDir
// Iterator state.
index int
more bool
} }
type iterationDir uint8 type iterationDir uint8
@@ -65,26 +71,35 @@ const inf = 1e6
// Init prepares the list for iterating through its children with Next. // Init prepares the list for iterating through its children with Next.
func (l *List) Init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, len int) { func (l *List) Init(cfg ui.Config, q input.Queue, ops *ui.Ops, cs Constraints, len int) {
if l.more { if l.More() {
panic("unfinished child") panic("unfinished child")
} }
l.config = cfg l.config = cfg
l.queue = q l.queue = q
l.update() l.update()
l.ops = ops l.ops = ops
l.dir = iterateNone
l.maxSize = 0 l.maxSize = 0
l.children = l.children[:0] l.children = l.children[:0]
l.cs = cs l.cs = cs
l.len = len l.len = len
l.more = true // Inverted lists scroll to the very end as long as the user hasn't
// scrolled away.
if l.scrollToEnd() {
l.offset = 0
l.first = len
}
if l.first > len { if l.first > len {
l.offset = 0
l.first = len l.first = len
} }
l.macro.Record(ops) l.macro.Record(ops)
l.Next() l.Next()
} }
func (l *List) scrollToEnd() bool {
return l.Invert && !l.beforeEnd
}
// Dragging reports whether the List is being dragged. // Dragging reports whether the List is being dragged.
func (l *List) Dragging() bool { func (l *List) Dragging() bool {
return l.scroll.State() == gesture.StateDragging return l.scroll.State() == gesture.StateDragging
@@ -93,34 +108,36 @@ func (l *List) Dragging() bool {
func (l *List) update() { func (l *List) update() {
l.Distance = 0 l.Distance = 0
d := l.scroll.Scroll(l.config, l.queue, gesture.Axis(l.Axis)) d := l.scroll.Scroll(l.config, l.queue, gesture.Axis(l.Axis))
if l.Invert { l.scrollDelta = d
d = -d
}
l.scrollDir = d
l.Distance += d l.Distance += d
l.offset += d l.offset += d
} }
// Next advances to the next child. // Next advances to the next child.
func (l *List) Next() { func (l *List) Next() {
if !l.more { l.dir = l.next()
panic("end of list reached") // The user scroll offset is applied after scrolling to
// list end.
if l.scrollToEnd() && !l.More() && l.scrollDelta < 0 {
l.beforeEnd = true
l.offset += l.scrollDelta
l.dir = l.next()
} }
i, more := l.next() if l.More() {
l.more = more
if !more {
return
}
if l.Invert {
i = l.len - 1 - i
}
l.index = i
l.child.Record(l.ops) l.child.Record(l.ops)
}
} }
// Index is current child's position in the underlying list. // Index is current child's position in the underlying list.
func (l *List) Index() int { func (l *List) Index() int {
return l.index switch l.dir {
case iterateBackward:
return l.first - 1
case iterateForward:
return l.first + len(l.children)
default:
panic("Index called before Next")
}
} }
// Constraints is the constraints for the current child. // Constraints is the constraints for the current child.
@@ -130,48 +147,43 @@ func (l *List) Constraints() Constraints {
// More reports whether more children are needed. // More reports whether more children are needed.
func (l *List) More() bool { func (l *List) More() bool {
return l.more return l.dir != iterateNone
} }
func (l *List) next() (int, bool) { func (l *List) next() iterationDir {
mainc := axisMainConstraint(l.Axis, l.cs) vsize := axisMainConstraint(l.Axis, l.cs).Max
if l.offset <= 0 { last := l.first + len(l.children)
if l.first > 0 { // Clamp offset.
l.dir = iterateBackward if l.maxSize-l.offset < vsize && last == l.len {
return l.first - 1, true l.offset = l.maxSize - vsize
} }
if l.offset < 0 && l.first == 0 {
l.offset = 0 l.offset = 0
} }
if l.maxSize-l.offset < mainc.Max { switch {
i := l.first + len(l.children) case len(l.children) == l.len:
if i < l.len { return iterateNone
l.dir = iterateForward case l.maxSize-l.offset < vsize:
return i, true return iterateForward
case l.offset < 0:
return iterateBackward
} }
missing := mainc.Max - (l.maxSize - l.offset) return iterateNone
if missing > l.offset {
missing = l.offset
}
l.offset -= missing
}
return 0, false
} }
// End the current child by specifying its dimensions. // End the current child by specifying its dimensions.
func (l *List) End(dims Dimensions) { func (l *List) End(dims Dimensions) {
l.child.Stop() l.child.Stop()
child := scrollChild{dims.Size, l.child} child := scrollChild{dims.Size, l.child}
mainSize := axisMain(l.Axis, child.size)
l.maxSize += mainSize
switch l.dir { switch l.dir {
case iterateForward: case iterateForward:
mainSize := axisMain(l.Axis, child.size)
l.maxSize += mainSize
l.children = append(l.children, child) l.children = append(l.children, child)
case iterateBackward: case iterateBackward:
l.first--
mainSize := axisMain(l.Axis, child.size)
l.offset += mainSize
l.maxSize += mainSize
l.children = append([]scrollChild{child}, l.children...) l.children = append([]scrollChild{child}, l.children...)
l.first--
l.offset += mainSize
default: default:
panic("call Next before End") panic("call Next before End")
} }
@@ -180,36 +192,42 @@ func (l *List) End(dims Dimensions) {
// Layout the List and return its dimensions. // Layout the List and return its dimensions.
func (l *List) Layout() Dimensions { func (l *List) Layout() Dimensions {
if l.more { if l.More() {
panic("unfinished child") panic("unfinished child")
} }
mainc := axisMainConstraint(l.Axis, l.cs) mainc := axisMainConstraint(l.Axis, l.cs)
for len(l.children) > 0 { children := l.children
sz := l.children[0].size // Skip invisible children
for len(children) > 0 {
sz := children[0].size
mainSize := axisMain(l.Axis, sz) mainSize := axisMain(l.Axis, sz)
if l.offset <= mainSize { if l.offset <= mainSize {
break break
} }
l.first++ l.first++
l.offset -= mainSize l.offset -= mainSize
l.children = l.children[1:] children = children[1:]
} }
size := -l.offset size := -l.offset
var maxCross int var maxCross int
for i, child := range l.children { for i, child := range children {
sz := child.size sz := child.size
if c := axisCross(l.Axis, sz); c > maxCross { if c := axisCross(l.Axis, sz); c > maxCross {
maxCross = c maxCross = c
} }
size += axisMain(l.Axis, sz) size += axisMain(l.Axis, sz)
if size >= mainc.Max { if size >= mainc.Max {
l.children = l.children[:i+1] children = children[:i+1]
break break
} }
} }
ops := l.ops ops := l.ops
pos := -l.offset pos := -l.offset
for _, child := range l.children { // Inverted lists are end aligned.
if space := mainc.Max - size; l.Invert && space > 0 {
pos += space
}
for _, child := range children {
sz := child.size sz := child.size
var cross int var cross int
switch l.Alignment { switch l.Alignment {
@@ -227,11 +245,6 @@ func (l *List) Layout() Dimensions {
if min < 0 { if min < 0 {
min = 0 min = 0
} }
transPos := pos
if l.Invert {
transPos = mainc.Max - transPos - childSize
min, max = mainc.Max-max, mainc.Max-min
}
r := image.Rectangle{ r := image.Rectangle{
Min: axisPoint(l.Axis, min, -inf), Min: axisPoint(l.Axis, min, -inf),
Max: axisPoint(l.Axis, max, inf), Max: axisPoint(l.Axis, max, inf),
@@ -239,16 +252,17 @@ func (l *List) Layout() Dimensions {
var stack ui.StackOp var stack ui.StackOp
stack.Push(ops) stack.Push(ops)
paint.RectClip(r).Add(ops) paint.RectClip(r).Add(ops)
ui.TransformOp{}.Offset(toPointF(axisPoint(l.Axis, transPos, cross))).Add(ops) ui.TransformOp{}.Offset(toPointF(axisPoint(l.Axis, pos, cross))).Add(ops)
child.macro.Add(ops) child.macro.Add(ops)
stack.Pop() stack.Pop()
pos += childSize pos += childSize
} }
atStart := l.first == 0 && l.offset <= 0 atStart := l.first == 0 && l.offset <= 0
atEnd := l.first+len(l.children) == l.len && mainc.Max >= pos atEnd := l.first+len(children) == l.len && mainc.Max >= pos
if atStart && l.scrollDir < 0 || atEnd && l.scrollDir > 0 { if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
l.scroll.Stop() l.scroll.Stop()
} }
l.beforeEnd = !atEnd
dims := axisPoint(l.Axis, mainc.Constrain(pos), maxCross) dims := axisPoint(l.Axis, mainc.Constrain(pos), maxCross)
l.macro.Stop() l.macro.Stop()
pointer.RectAreaOp{Rect: image.Rectangle{Max: dims}}.Add(ops) pointer.RectAreaOp{Rect: image.Rectangle{Max: dims}}.Add(ops)