forked from joejulian/gio
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:
+82
-68
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user