mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
2782436ffc
Almost every layout and widget need the ui.Config for its environment, an ui.Ops to store operations. Stateful widgets need an input.Queue for events. Add all these common objects to Context, greatly simplifying the function signatures for Gio programs. Fixes gio#33 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/ui"
|
|
"gioui.org/ui/gesture"
|
|
"gioui.org/ui/paint"
|
|
"gioui.org/ui/pointer"
|
|
)
|
|
|
|
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(c *Context, len int) {
|
|
if l.more() {
|
|
panic("unfinished child")
|
|
}
|
|
l.ctx = c
|
|
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(c.Ops)
|
|
l.next()
|
|
}
|
|
|
|
// Layout the List and return its dimensions.
|
|
func (l *List) Layout(c *Context, len int, w ListElement) {
|
|
for l.init(c, len); l.more(); l.next() {
|
|
cs := axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.ctx.Constraints))
|
|
i := l.index()
|
|
l.end(c.Layout(cs, func() {
|
|
w(i)
|
|
}))
|
|
}
|
|
c.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}
|
|
}
|