forked from joejulian/gio
7a259e68f7
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>
270 lines
5.9 KiB
Go
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}
|
|
}
|