gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/layout/list.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package layout 4 5 import ( 6 "image" 7 8 "gioui.org/ui" 9 "gioui.org/ui/gesture" 10 "gioui.org/ui/paint" 11 "gioui.org/ui/pointer" 12 ) 13 14 type scrollChild struct { 15 size image.Point 16 macro ui.MacroOp 17 } 18 19 // List displays a subsection of a potentially infinitely 20 // large underlying list. List accepts user input to scroll 21 // the subsection. 22 type List struct { 23 Axis Axis 24 // ScrollToEnd instructs the list to stay scrolled to the far end position 25 // once reahed. A List with ScrollToEnd enabled also align its content to 26 // the end. 27 ScrollToEnd bool 28 // Alignment is the cross axis alignment of list elements. 29 Alignment Alignment 30 31 // beforeEnd tracks whether the List position is before 32 // the very end. 33 beforeEnd bool 34 35 ctx *Context 36 macro ui.MacroOp 37 child ui.MacroOp 38 scroll gesture.Scroll 39 scrollDelta int 40 41 // first is the index of the first visible child. 42 first int 43 // offset is the signed distance from the top edge 44 // to the child with index first. 45 offset int 46 47 len int 48 49 // maxSize is the total size of visible children. 50 maxSize int 51 children []scrollChild 52 dir iterationDir 53 } 54 55 // ListElement is a function that computes the dimensions of 56 // a list element. 57 type ListElement func(index int) 58 59 type iterationDir uint8 60 61 const ( 62 iterateNone iterationDir = iota 63 iterateForward 64 iterateBackward 65 ) 66 67 const inf = 1e6 68 69 // init prepares the list for iterating through its children with next. 70 func (l *List) init(gtx *Context, len int) { 71 if l.more() { 72 panic("unfinished child") 73 } 74 l.ctx = gtx 75 l.maxSize = 0 76 l.children = l.children[:0] 77 l.len = len 78 l.update() 79 if l.scrollToEnd() { 80 l.offset = 0 81 l.first = len 82 } 83 if l.first > len { 84 l.offset = 0 85 l.first = len 86 } 87 l.macro.Record(gtx.Ops) 88 l.next() 89 } 90 91 // Layout the List and return its dimensions. 92 func (l *List) Layout(gtx *Context, len int, w ListElement) { 93 for l.init(gtx, len); l.more(); l.next() { 94 cs := axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.ctx.Constraints)) 95 i := l.index() 96 l.end(gtx.Layout(cs, func() { 97 w(i) 98 })) 99 } 100 gtx.Dimensions = l.layout() 101 } 102 103 func (l *List) scrollToEnd() bool { 104 return l.ScrollToEnd && !l.beforeEnd 105 } 106 107 // Dragging reports whether the List is being dragged. 108 func (l *List) Dragging() bool { 109 return l.scroll.State() == gesture.StateDragging 110 } 111 112 func (l *List) update() { 113 d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, gesture.Axis(l.Axis)) 114 l.scrollDelta = d 115 l.offset += d 116 } 117 118 // next advances to the next child. 119 func (l *List) next() { 120 l.dir = l.nextDir() 121 // The user scroll offset is applied after scrolling to 122 // list end. 123 if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { 124 l.beforeEnd = true 125 l.offset += l.scrollDelta 126 l.dir = l.nextDir() 127 } 128 if l.more() { 129 l.child.Record(l.ctx.Ops) 130 } 131 } 132 133 // index is current child's position in the underlying list. 134 func (l *List) index() int { 135 switch l.dir { 136 case iterateBackward: 137 return l.first - 1 138 case iterateForward: 139 return l.first + len(l.children) 140 default: 141 panic("Index called before Next") 142 } 143 } 144 145 // more reports whether more children are needed. 146 func (l *List) more() bool { 147 return l.dir != iterateNone 148 } 149 150 func (l *List) nextDir() iterationDir { 151 vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max 152 last := l.first + len(l.children) 153 // Clamp offset. 154 if l.maxSize-l.offset < vsize && last == l.len { 155 l.offset = l.maxSize - vsize 156 } 157 if l.offset < 0 && l.first == 0 { 158 l.offset = 0 159 } 160 switch { 161 case len(l.children) == l.len: 162 return iterateNone 163 case l.maxSize-l.offset < vsize: 164 return iterateForward 165 case l.offset < 0: 166 return iterateBackward 167 } 168 return iterateNone 169 } 170 171 // End the current child by specifying its dimensions. 172 func (l *List) end(dims Dimensions) { 173 l.child.Stop() 174 child := scrollChild{dims.Size, l.child} 175 mainSize := axisMain(l.Axis, child.size) 176 l.maxSize += mainSize 177 switch l.dir { 178 case iterateForward: 179 l.children = append(l.children, child) 180 case iterateBackward: 181 l.children = append([]scrollChild{child}, l.children...) 182 l.first-- 183 l.offset += mainSize 184 default: 185 panic("call Next before End") 186 } 187 l.dir = iterateNone 188 } 189 190 // Layout the List and return its dimensions. 191 func (l *List) layout() Dimensions { 192 if l.more() { 193 panic("unfinished child") 194 } 195 mainc := axisMainConstraint(l.Axis, l.ctx.Constraints) 196 children := l.children 197 // Skip invisible children 198 for len(children) > 0 { 199 sz := children[0].size 200 mainSize := axisMain(l.Axis, sz) 201 if l.offset <= mainSize { 202 break 203 } 204 l.first++ 205 l.offset -= mainSize 206 children = children[1:] 207 } 208 size := -l.offset 209 var maxCross int 210 for i, child := range children { 211 sz := child.size 212 if c := axisCross(l.Axis, sz); c > maxCross { 213 maxCross = c 214 } 215 size += axisMain(l.Axis, sz) 216 if size >= mainc.Max { 217 children = children[:i+1] 218 break 219 } 220 } 221 ops := l.ctx.Ops 222 pos := -l.offset 223 // ScrollToEnd lists lists are end aligned. 224 if space := mainc.Max - size; l.ScrollToEnd && space > 0 { 225 pos += space 226 } 227 for _, child := range children { 228 sz := child.size 229 var cross int 230 switch l.Alignment { 231 case End: 232 cross = maxCross - axisCross(l.Axis, sz) 233 case Middle: 234 cross = (maxCross - axisCross(l.Axis, sz)) / 2 235 } 236 childSize := axisMain(l.Axis, sz) 237 max := childSize + pos 238 if max > mainc.Max { 239 max = mainc.Max 240 } 241 min := pos 242 if min < 0 { 243 min = 0 244 } 245 r := image.Rectangle{ 246 Min: axisPoint(l.Axis, min, -inf), 247 Max: axisPoint(l.Axis, max, inf), 248 } 249 var stack ui.StackOp 250 stack.Push(ops) 251 paint.RectClip(r).Add(ops) 252 ui.TransformOp{}.Offset(toPointF(axisPoint(l.Axis, pos, cross))).Add(ops) 253 child.macro.Add(ops) 254 stack.Pop() 255 pos += childSize 256 } 257 atStart := l.first == 0 && l.offset <= 0 258 atEnd := l.first+len(children) == l.len && mainc.Max >= pos 259 if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { 260 l.scroll.Stop() 261 } 262 l.beforeEnd = !atEnd 263 dims := axisPoint(l.Axis, mainc.Constrain(pos), maxCross) 264 l.macro.Stop() 265 pointer.RectAreaOp{Rect: image.Rectangle{Max: dims}}.Add(ops) 266 l.scroll.Add(ops) 267 l.macro.Add(ops) 268 return Dimensions{Size: dims} 269 }