Files
gio-patched/layout/list.go
T
Elias Naur 7a259e68f7 io: give event packages a common prefix
Packages that provide support for external events such as pointer, key and
system are only the beginning. Future packages are expected for clipboard
access, drag and drop, gps positions and so on.

To keep the number of top-level packages under control, move such I/O packages
to the new `io` directory.

The `system` package name was the previous solution to keeping the number of
top-level packages under control: I named it `system` instead of the narrower
`profile` because I expected to put all the less common events into it, turning
`system` into a "package util" smell.

With `io`, package system can be renamed to `profile`.

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-09-30 14:50:55 +02:00

270 lines
5.9 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"gioui.org/gesture"
"gioui.org/io/pointer"
"gioui.org/paint"
"gioui.org/ui"
)
type scrollChild struct {
size image.Point
macro ui.MacroOp
}
// List displays a subsection of a potentially infinitely
// large underlying list. List accepts user input to scroll
// the subsection.
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.
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 ui.MacroOp
child ui.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
len int
// maxSize is the total size of visible children.
maxSize int
children []scrollChild
dir iterationDir
}
// ListElement is a function that computes the dimensions of
// a list element.
type ListElement func(index int)
type iterationDir uint8
const (
iterateNone iterationDir = iota
iterateForward
iterateBackward
)
const inf = 1e6
// init prepares the list for iterating through its children with next.
func (l *List) init(gtx *Context, len int) {
if l.more() {
panic("unfinished child")
}
l.ctx = gtx
l.maxSize = 0
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
}
l.macro.Record(gtx.Ops)
l.next()
}
// Layout the List and return its dimensions.
func (l *List) Layout(gtx *Context, len int, w ListElement) {
for l.init(gtx, len); l.more(); l.next() {
cs := axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.ctx.Constraints))
i := l.index()
l.end(gtx.Layout(cs, func() {
w(i)
}))
}
gtx.Dimensions = l.layout()
}
func (l *List) scrollToEnd() bool {
return l.ScrollToEnd && !l.beforeEnd
}
// Dragging reports whether the List is being dragged.
func (l *List) Dragging() bool {
return l.scroll.State() == gesture.StateDragging
}
func (l *List) update() {
d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, gesture.Axis(l.Axis))
l.scrollDelta = d
l.offset += d
}
// next advances to the next child.
func (l *List) next() {
l.dir = l.nextDir()
// 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.nextDir()
}
if l.more() {
l.child.Record(l.ctx.Ops)
}
}
// index is current child's position in the underlying list.
func (l *List) index() int {
switch l.dir {
case iterateBackward:
return l.first - 1
case iterateForward:
return l.first + len(l.children)
default:
panic("Index called before Next")
}
}
// more reports whether more children are needed.
func (l *List) more() bool {
return l.dir != iterateNone
}
func (l *List) nextDir() iterationDir {
vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max
last := l.first + len(l.children)
// Clamp offset.
if l.maxSize-l.offset < vsize && last == l.len {
l.offset = l.maxSize - vsize
}
if l.offset < 0 && l.first == 0 {
l.offset = 0
}
switch {
case len(l.children) == l.len:
return iterateNone
case l.maxSize-l.offset < vsize:
return iterateForward
case l.offset < 0:
return iterateBackward
}
return iterateNone
}
// End the current child by specifying its dimensions.
func (l *List) end(dims Dimensions) {
l.child.Stop()
child := scrollChild{dims.Size, l.child}
mainSize := axisMain(l.Axis, child.size)
l.maxSize += mainSize
switch l.dir {
case iterateForward:
l.children = append(l.children, child)
case iterateBackward:
l.children = append([]scrollChild{child}, l.children...)
l.first--
l.offset += mainSize
default:
panic("call Next before End")
}
l.dir = iterateNone
}
// Layout the List and return its dimensions.
func (l *List) layout() Dimensions {
if l.more() {
panic("unfinished child")
}
mainc := axisMainConstraint(l.Axis, l.ctx.Constraints)
children := l.children
// Skip invisible children
for len(children) > 0 {
sz := children[0].size
mainSize := axisMain(l.Axis, sz)
if l.offset <= mainSize {
break
}
l.first++
l.offset -= mainSize
children = children[1:]
}
size := -l.offset
var maxCross int
for i, child := range children {
sz := child.size
if c := axisCross(l.Axis, sz); c > maxCross {
maxCross = c
}
size += axisMain(l.Axis, sz)
if size >= mainc.Max {
children = children[:i+1]
break
}
}
ops := l.ctx.Ops
pos := -l.offset
// ScrollToEnd lists lists are end aligned.
if space := mainc.Max - size; l.ScrollToEnd && space > 0 {
pos += space
}
for _, child := range children {
sz := child.size
var cross int
switch l.Alignment {
case End:
cross = maxCross - axisCross(l.Axis, sz)
case Middle:
cross = (maxCross - axisCross(l.Axis, sz)) / 2
}
childSize := axisMain(l.Axis, sz)
max := childSize + pos
if max > mainc.Max {
max = mainc.Max
}
min := pos
if min < 0 {
min = 0
}
r := image.Rectangle{
Min: axisPoint(l.Axis, min, -inf),
Max: axisPoint(l.Axis, max, inf),
}
var stack ui.StackOp
stack.Push(ops)
paint.RectClip(r).Add(ops)
ui.TransformOp{}.Offset(toPointF(axisPoint(l.Axis, pos, cross))).Add(ops)
child.macro.Add(ops)
stack.Pop()
pos += childSize
}
atStart := l.first == 0 && l.offset <= 0
atEnd := l.first+len(children) == l.len && mainc.Max >= pos
if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
l.scroll.Stop()
}
l.beforeEnd = !atEnd
dims := axisPoint(l.Axis, mainc.Constrain(pos), maxCross)
l.macro.Stop()
pointer.RectAreaOp{Rect: image.Rectangle{Max: dims}}.Add(ops)
l.scroll.Add(ops)
l.macro.Add(ops)
return Dimensions{Size: dims}
}