From ce76c2e996669ac8156881a7af7fa731a07e6b78 Mon Sep 17 00:00:00 2001 From: Larry Clapp Date: Tue, 26 Nov 2019 11:36:54 -0500 Subject: [PATCH] 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 --- layout/list.go | 95 ++++++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/layout/list.go b/layout/list.go index bd4d08a2..6af3da04 100644 --- a/layout/list.go +++ b/layout/list.go @@ -23,27 +23,24 @@ type scrollChild struct { type List struct { Axis Axis // ScrollToEnd instructs the list to stay scrolled to the far end position - // once reahed. A List with ScrollToEnd enabled also align its content to - // the end. + // once reached. A List with ScrollToEnd == true and Position.BeforeEnd == + // false draws its content with the last item at the bottom of the list + // area. ScrollToEnd bool // Alignment is the cross axis alignment of list elements. Alignment Alignment - // beforeEnd tracks whether the List position is before - // the very end. - beforeEnd bool - ctx *Context macro op.MacroOp child op.MacroOp scroll gesture.Scroll scrollDelta int - // first is the index of the first visible child. - first int - // offset is the signed distance from the top edge - // to the child with index first. - offset int + // Position is updated during Layout. To save the list scroll position, + // just save Position after Layout finishes. To scroll the list + // programatically, update Position (e.g. restore it from a saved value) + // before calling Layout. + Position Position len int @@ -59,6 +56,24 @@ type ListElement func(index int) 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 ( iterateNone iterationDir = iota iterateForward @@ -77,13 +92,9 @@ func (l *List) init(gtx *Context, len int) { l.children = l.children[:0] l.len = len l.update() - if l.scrollToEnd() { - l.offset = 0 - l.first = len - } - if l.first > len { - l.offset = 0 - l.first = len + if l.scrollToEnd() || l.Position.First > len { + l.Position.Offset = 0 + l.Position.First = len } l.macro.Record(gtx.Ops) l.next() @@ -102,7 +113,7 @@ func (l *List) Layout(gtx *Context, len int, w ListElement) { } func (l *List) scrollToEnd() bool { - return l.ScrollToEnd && !l.beforeEnd + return l.ScrollToEnd && !l.Position.BeforeEnd } // Dragging reports whether the List is being dragged. @@ -113,7 +124,7 @@ func (l *List) Dragging() bool { func (l *List) update() { d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, l.ctx.Now(), gesture.Axis(l.Axis)) l.scrollDelta = d - l.offset += d + l.Position.Offset += d } // next advances to the next child. @@ -122,8 +133,8 @@ func (l *List) next() { // 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.Position.BeforeEnd = true + l.Position.Offset += l.scrollDelta l.dir = l.nextDir() } if l.more() { @@ -135,9 +146,9 @@ func (l *List) next() { func (l *List) index() int { switch l.dir { case iterateBackward: - return l.first - 1 + return l.Position.First - 1 case iterateForward: - return l.first + len(l.children) + return l.Position.First + len(l.children) default: panic("Index called before Next") } @@ -150,20 +161,20 @@ func (l *List) more() bool { func (l *List) nextDir() iterationDir { vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max - last := l.first + len(l.children) + last := l.Position.First + len(l.children) // Clamp offset. - if l.maxSize-l.offset < vsize && last == l.len { - l.offset = l.maxSize - vsize + if l.maxSize-l.Position.Offset < vsize && last == l.len { + l.Position.Offset = l.maxSize - vsize } - if l.offset < 0 && l.first == 0 { - l.offset = 0 + if l.Position.Offset < 0 && l.Position.First == 0 { + l.Position.Offset = 0 } switch { case len(l.children) == l.len: return iterateNone - case l.maxSize-l.offset < vsize: + case l.maxSize-l.Position.Offset < vsize: return iterateForward - case l.offset < 0: + case l.Position.Offset < 0: return iterateBackward } return iterateNone @@ -180,8 +191,8 @@ func (l *List) end(dims Dimensions) { l.children = append(l.children, child) case iterateBackward: l.children = append([]scrollChild{child}, l.children...) - l.first-- - l.offset += mainSize + l.Position.First-- + l.Position.Offset += mainSize default: panic("call Next before End") } @@ -199,14 +210,14 @@ func (l *List) layout() Dimensions { for len(children) > 0 { sz := children[0].size mainSize := axisMain(l.Axis, sz) - if l.offset <= mainSize { + if l.Position.Offset <= mainSize { break } - l.first++ - l.offset -= mainSize + l.Position.First++ + l.Position.Offset -= mainSize children = children[1:] } - size := -l.offset + size := -l.Position.Offset var maxCross int for i, child := range children { sz := child.size @@ -220,8 +231,8 @@ func (l *List) layout() Dimensions { } } ops := l.ctx.Ops - pos := -l.offset - // ScrollToEnd lists lists are end aligned. + pos := -l.Position.Offset + // ScrollToEnd lists are end aligned. if space := mainc.Max - size; l.ScrollToEnd && space > 0 { pos += space } @@ -255,12 +266,12 @@ func (l *List) layout() Dimensions { stack.Pop() pos += childSize } - atStart := l.first == 0 && l.offset <= 0 - atEnd := l.first+len(children) == l.len && mainc.Max >= pos + atStart := l.Position.First == 0 && l.Position.Offset <= 0 + atEnd := l.Position.First+len(children) == l.len && mainc.Max >= pos if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { l.scroll.Stop() } - l.beforeEnd = !atEnd + l.Position.BeforeEnd = !atEnd dims := axisPoint(l.Axis, mainc.Constrain(pos), maxCross) l.macro.Stop() pointer.Rect(image.Rectangle{Max: dims}).Add(ops)