layout: make list scroll position settable

Put List.{first|offset|beforeEnd} into a new exported Position slot, and
also export each individually.

Have to put BeforeEnd into the Position slot to support ScrollToEnd
lists.

Signed-off-by: Larry Clapp <larry@theclapp.org>
This commit is contained in:
Larry Clapp
2019-11-26 11:36:54 -05:00
committed by Elias Naur
parent 8321745de9
commit ce76c2e996
+53 -42
View File
@@ -23,27 +23,24 @@ type scrollChild struct {
type List struct { type List struct {
Axis Axis Axis Axis
// ScrollToEnd instructs the list to stay scrolled to the far end position // ScrollToEnd instructs the list to stay scrolled to the far end position
// once reahed. A List with ScrollToEnd enabled also align its content to // once reached. A List with ScrollToEnd == true and Position.BeforeEnd ==
// the end. // false draws its content with the last item at the bottom of the list
// area.
ScrollToEnd bool ScrollToEnd bool
// Alignment is the cross axis alignment of list elements. // Alignment is the cross axis alignment of list elements.
Alignment Alignment Alignment Alignment
// beforeEnd tracks whether the List position is before
// the very end.
beforeEnd bool
ctx *Context ctx *Context
macro op.MacroOp macro op.MacroOp
child op.MacroOp child op.MacroOp
scroll gesture.Scroll scroll gesture.Scroll
scrollDelta int scrollDelta int
// first is the index of the first visible child. // Position is updated during Layout. To save the list scroll position,
first int // just save Position after Layout finishes. To scroll the list
// offset is the signed distance from the top edge // programatically, update Position (e.g. restore it from a saved value)
// to the child with index first. // before calling Layout.
offset int Position Position
len int len int
@@ -59,6 +56,24 @@ type ListElement func(index int)
type iterationDir uint8 type iterationDir uint8
// Position is a List scroll offset represented as an offset from the top edge
// of a child element.
type Position struct {
// BeforeEnd tracks whether the List position is before the very end. We
// use "before end" instead of "at end" so that the zero value of a
// Position struct is useful.
//
// When laying out a list, if ScrollToEnd is true and BeforeEnd is false,
// then First and Offset are ignored, and the list is drawn with the last
// item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored.
BeforeEnd bool
// First is the index of the first visible child.
First int
// Offset is the distance in pixels from the top edge to the child at index
// First.
Offset int
}
const ( const (
iterateNone iterationDir = iota iterateNone iterationDir = iota
iterateForward iterateForward
@@ -77,13 +92,9 @@ func (l *List) init(gtx *Context, len int) {
l.children = l.children[:0] l.children = l.children[:0]
l.len = len l.len = len
l.update() l.update()
if l.scrollToEnd() { if l.scrollToEnd() || l.Position.First > len {
l.offset = 0 l.Position.Offset = 0
l.first = len l.Position.First = len
}
if l.first > len {
l.offset = 0
l.first = len
} }
l.macro.Record(gtx.Ops) l.macro.Record(gtx.Ops)
l.next() l.next()
@@ -102,7 +113,7 @@ func (l *List) Layout(gtx *Context, len int, w ListElement) {
} }
func (l *List) scrollToEnd() bool { func (l *List) scrollToEnd() bool {
return l.ScrollToEnd && !l.beforeEnd return l.ScrollToEnd && !l.Position.BeforeEnd
} }
// Dragging reports whether the List is being dragged. // Dragging reports whether the List is being dragged.
@@ -113,7 +124,7 @@ func (l *List) Dragging() bool {
func (l *List) update() { func (l *List) update() {
d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, l.ctx.Now(), gesture.Axis(l.Axis)) d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, l.ctx.Now(), gesture.Axis(l.Axis))
l.scrollDelta = d l.scrollDelta = d
l.offset += d l.Position.Offset += d
} }
// next advances to the next child. // next advances to the next child.
@@ -122,8 +133,8 @@ func (l *List) next() {
// The user scroll offset is applied after scrolling to // The user scroll offset is applied after scrolling to
// list end. // list end.
if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 {
l.beforeEnd = true l.Position.BeforeEnd = true
l.offset += l.scrollDelta l.Position.Offset += l.scrollDelta
l.dir = l.nextDir() l.dir = l.nextDir()
} }
if l.more() { if l.more() {
@@ -135,9 +146,9 @@ func (l *List) next() {
func (l *List) index() int { func (l *List) index() int {
switch l.dir { switch l.dir {
case iterateBackward: case iterateBackward:
return l.first - 1 return l.Position.First - 1
case iterateForward: case iterateForward:
return l.first + len(l.children) return l.Position.First + len(l.children)
default: default:
panic("Index called before Next") panic("Index called before Next")
} }
@@ -150,20 +161,20 @@ func (l *List) more() bool {
func (l *List) nextDir() iterationDir { func (l *List) nextDir() iterationDir {
vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max
last := l.first + len(l.children) last := l.Position.First + len(l.children)
// Clamp offset. // Clamp offset.
if l.maxSize-l.offset < vsize && last == l.len { if l.maxSize-l.Position.Offset < vsize && last == l.len {
l.offset = l.maxSize - vsize l.Position.Offset = l.maxSize - vsize
} }
if l.offset < 0 && l.first == 0 { if l.Position.Offset < 0 && l.Position.First == 0 {
l.offset = 0 l.Position.Offset = 0
} }
switch { switch {
case len(l.children) == l.len: case len(l.children) == l.len:
return iterateNone return iterateNone
case l.maxSize-l.offset < vsize: case l.maxSize-l.Position.Offset < vsize:
return iterateForward return iterateForward
case l.offset < 0: case l.Position.Offset < 0:
return iterateBackward return iterateBackward
} }
return iterateNone return iterateNone
@@ -180,8 +191,8 @@ func (l *List) end(dims Dimensions) {
l.children = append(l.children, child) l.children = append(l.children, child)
case iterateBackward: case iterateBackward:
l.children = append([]scrollChild{child}, l.children...) l.children = append([]scrollChild{child}, l.children...)
l.first-- l.Position.First--
l.offset += mainSize l.Position.Offset += mainSize
default: default:
panic("call Next before End") panic("call Next before End")
} }
@@ -199,14 +210,14 @@ func (l *List) layout() Dimensions {
for len(children) > 0 { for len(children) > 0 {
sz := children[0].size sz := children[0].size
mainSize := axisMain(l.Axis, sz) mainSize := axisMain(l.Axis, sz)
if l.offset <= mainSize { if l.Position.Offset <= mainSize {
break break
} }
l.first++ l.Position.First++
l.offset -= mainSize l.Position.Offset -= mainSize
children = children[1:] children = children[1:]
} }
size := -l.offset size := -l.Position.Offset
var maxCross int var maxCross int
for i, child := range children { for i, child := range children {
sz := child.size sz := child.size
@@ -220,8 +231,8 @@ func (l *List) layout() Dimensions {
} }
} }
ops := l.ctx.Ops ops := l.ctx.Ops
pos := -l.offset pos := -l.Position.Offset
// ScrollToEnd lists lists are end aligned. // ScrollToEnd lists are end aligned.
if space := mainc.Max - size; l.ScrollToEnd && space > 0 { if space := mainc.Max - size; l.ScrollToEnd && space > 0 {
pos += space pos += space
} }
@@ -255,12 +266,12 @@ func (l *List) layout() Dimensions {
stack.Pop() stack.Pop()
pos += childSize pos += childSize
} }
atStart := l.first == 0 && l.offset <= 0 atStart := l.Position.First == 0 && l.Position.Offset <= 0
atEnd := l.first+len(children) == l.len && mainc.Max >= pos atEnd := l.Position.First+len(children) == l.len && mainc.Max >= pos
if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
l.scroll.Stop() l.scroll.Stop()
} }
l.beforeEnd = !atEnd l.Position.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.Rect(image.Rectangle{Max: dims}).Add(ops) pointer.Rect(image.Rectangle{Max: dims}).Add(ops)