github.com/Seikaijyu/gio@v0.0.1/layout/list.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package layout 4 5 import ( 6 "image" 7 "math" 8 9 "github.com/Seikaijyu/gio/gesture" 10 "github.com/Seikaijyu/gio/op" 11 "github.com/Seikaijyu/gio/op/clip" 12 ) 13 14 type scrollChild struct { 15 size image.Point 16 call op.CallOp 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 reached. A List with ScrollToEnd == true and Position.BeforeEnd == 26 // false draws its content with the last item at the bottom of the list 27 // area. 28 ScrollToEnd bool 29 // Alignment is the cross axis alignment of list elements. 30 Alignment Alignment 31 32 cs Constraints 33 scroll gesture.Scroll 34 scrollDelta int 35 36 // Position is updated during Layout. To save the list scroll position, 37 // just save Position after Layout finishes. To scroll the list 38 // programmatically, update Position (e.g. restore it from a saved value) 39 // before calling Layout. 40 Position Position 41 42 len int 43 44 // maxSize is the total size of visible children. 45 maxSize int 46 children []scrollChild 47 dir iterationDir 48 } 49 50 // ListElement is a function that computes the dimensions of 51 // a list element. 52 type ListElement func(gtx Context, index int) Dimensions 53 54 type iterationDir uint8 55 56 // Position is a List scroll offset represented as an offset from the top edge 57 // of a child element. 58 type Position struct { 59 // BeforeEnd tracks whether the List position is before the very end. We 60 // use "before end" instead of "at end" so that the zero value of a 61 // Position struct is useful. 62 // 63 // When laying out a list, if ScrollToEnd is true and BeforeEnd is false, 64 // then First and Offset are ignored, and the list is drawn with the last 65 // item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored. 66 BeforeEnd bool 67 // First is the index of the first visible child. 68 First int 69 // Offset is the distance in pixels from the leading edge to the child at index 70 // First. 71 Offset int 72 // OffsetLast is the signed distance in pixels from the trailing edge to the 73 // bottom edge of the child at index First+Count. 74 OffsetLast int 75 // Count is the number of visible children. 76 Count int 77 // Length is the estimated total size of all children, measured in pixels. 78 Length int 79 } 80 81 const ( 82 iterateNone iterationDir = iota 83 iterateForward 84 iterateBackward 85 ) 86 87 const inf = 1e6 88 89 // init prepares the list for iterating through its children with next. 90 func (l *List) init(gtx Context, len int) { 91 if l.more() { 92 panic("unfinished child") 93 } 94 l.cs = gtx.Constraints 95 l.maxSize = 0 96 l.children = l.children[:0] 97 l.len = len 98 l.update(gtx) 99 if l.Position.First < 0 { 100 l.Position.Offset = 0 101 l.Position.First = 0 102 } 103 if l.scrollToEnd() || l.Position.First > len { 104 l.Position.Offset = 0 105 l.Position.First = len 106 } 107 } 108 109 // Layout a List of len items, where each item is implicitly defined 110 // by the callback w. Layout can handle very large lists because it only calls 111 // w to fill its viewport and the distance scrolled, if any. 112 func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions { 113 l.init(gtx, len) 114 crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints) 115 gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax) 116 macro := op.Record(gtx.Ops) 117 laidOutTotalLength := 0 118 numLaidOut := 0 119 120 for l.next(); l.more(); l.next() { 121 child := op.Record(gtx.Ops) 122 dims := w(gtx, l.index()) 123 call := child.Stop() 124 l.end(dims, call) 125 laidOutTotalLength += l.Axis.Convert(dims.Size).X 126 numLaidOut++ 127 } 128 129 if numLaidOut > 0 { 130 l.Position.Length = laidOutTotalLength * len / numLaidOut 131 } else { 132 l.Position.Length = 0 133 } 134 return l.layout(gtx.Ops, macro) 135 } 136 137 func (l *List) scrollToEnd() bool { 138 return l.ScrollToEnd && !l.Position.BeforeEnd 139 } 140 141 // Dragging reports whether the List is being dragged. 142 func (l *List) Dragging() bool { 143 return l.scroll.State() == gesture.StateDragging 144 } 145 146 func (l *List) update(gtx Context) { 147 d := l.scroll.Update(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis)) 148 l.scrollDelta = d 149 l.Position.Offset += d 150 } 151 152 // next advances to the next child. 153 func (l *List) next() { 154 l.dir = l.nextDir() 155 // The user scroll offset is applied after scrolling to 156 // list end. 157 if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { 158 l.Position.BeforeEnd = true 159 l.Position.Offset += l.scrollDelta 160 l.dir = l.nextDir() 161 } 162 } 163 164 // index is current child's position in the underlying list. 165 func (l *List) index() int { 166 switch l.dir { 167 case iterateBackward: 168 return l.Position.First - 1 169 case iterateForward: 170 return l.Position.First + len(l.children) 171 default: 172 panic("Index called before Next") 173 } 174 } 175 176 // more reports whether more children are needed. 177 func (l *List) more() bool { 178 return l.dir != iterateNone 179 } 180 181 func (l *List) nextDir() iterationDir { 182 _, vsize := l.Axis.mainConstraint(l.cs) 183 last := l.Position.First + len(l.children) 184 // Clamp offset. 185 if l.maxSize-l.Position.Offset < vsize && last == l.len { 186 l.Position.Offset = l.maxSize - vsize 187 } 188 if l.Position.Offset < 0 && l.Position.First == 0 { 189 l.Position.Offset = 0 190 } 191 // Lay out an extra (invisible) child at each end to enable focus to 192 // move to them, triggering automatic scroll. 193 firstSize, lastSize := 0, 0 194 if len(l.children) > 0 { 195 if l.Position.First > 0 { 196 firstChild := l.children[0] 197 firstSize = l.Axis.Convert(firstChild.size).X 198 } 199 if last < l.len { 200 lastChild := l.children[len(l.children)-1] 201 lastSize = l.Axis.Convert(lastChild.size).X 202 } 203 } 204 switch { 205 case len(l.children) == l.len: 206 return iterateNone 207 case l.maxSize-l.Position.Offset-lastSize < vsize: 208 return iterateForward 209 case l.Position.Offset-firstSize < 0: 210 return iterateBackward 211 } 212 return iterateNone 213 } 214 215 // End the current child by specifying its dimensions. 216 func (l *List) end(dims Dimensions, call op.CallOp) { 217 child := scrollChild{dims.Size, call} 218 mainSize := l.Axis.Convert(child.size).X 219 l.maxSize += mainSize 220 switch l.dir { 221 case iterateForward: 222 l.children = append(l.children, child) 223 case iterateBackward: 224 l.children = append(l.children, scrollChild{}) 225 copy(l.children[1:], l.children) 226 l.children[0] = child 227 l.Position.First-- 228 l.Position.Offset += mainSize 229 default: 230 panic("call Next before End") 231 } 232 l.dir = iterateNone 233 } 234 235 // Layout the List and return its dimensions. 236 func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { 237 if l.more() { 238 panic("unfinished child") 239 } 240 mainMin, mainMax := l.Axis.mainConstraint(l.cs) 241 children := l.children 242 var first scrollChild 243 // Skip invisible children. 244 for len(children) > 0 { 245 child := children[0] 246 sz := child.size 247 mainSize := l.Axis.Convert(sz).X 248 if l.Position.Offset < mainSize { 249 // First child is partially visible. 250 break 251 } 252 l.Position.First++ 253 l.Position.Offset -= mainSize 254 first = child 255 children = children[1:] 256 } 257 size := -l.Position.Offset 258 var maxCross int 259 var last scrollChild 260 for i, child := range children { 261 sz := l.Axis.Convert(child.size) 262 if c := sz.Y; c > maxCross { 263 maxCross = c 264 } 265 size += sz.X 266 if size >= mainMax { 267 if i < len(children)-1 { 268 last = children[i+1] 269 } 270 children = children[:i+1] 271 break 272 } 273 } 274 l.Position.Count = len(children) 275 l.Position.OffsetLast = mainMax - size 276 // ScrollToEnd lists are end aligned. 277 if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 { 278 l.Position.Offset -= space 279 } 280 pos := -l.Position.Offset 281 layout := func(child scrollChild) { 282 sz := l.Axis.Convert(child.size) 283 var cross int 284 switch l.Alignment { 285 case End: 286 cross = maxCross - sz.Y 287 case Middle: 288 cross = (maxCross - sz.Y) / 2 289 } 290 childSize := sz.X 291 min := pos 292 if min < 0 { 293 min = 0 294 } 295 pt := l.Axis.Convert(image.Pt(pos, cross)) 296 trans := op.Offset(pt).Push(ops) 297 child.call.Add(ops) 298 trans.Pop() 299 pos += childSize 300 } 301 // Lay out leading invisible child. 302 if first != (scrollChild{}) { 303 sz := l.Axis.Convert(first.size) 304 pos -= sz.X 305 layout(first) 306 } 307 for _, child := range children { 308 layout(child) 309 } 310 // Lay out trailing invisible child. 311 if last != (scrollChild{}) { 312 layout(last) 313 } 314 atStart := l.Position.First == 0 && l.Position.Offset <= 0 315 atEnd := l.Position.First+len(children) == l.len && mainMax >= pos 316 if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { 317 l.scroll.Stop() 318 } 319 l.Position.BeforeEnd = !atEnd 320 if pos < mainMin { 321 pos = mainMin 322 } 323 if pos > mainMax { 324 pos = mainMax 325 } 326 if crossMin, crossMax := l.Axis.crossConstraint(l.cs); maxCross < crossMin { 327 maxCross = crossMin 328 } else if maxCross > crossMax { 329 maxCross = crossMax 330 } 331 dims := l.Axis.Convert(image.Pt(pos, maxCross)) 332 call := macro.Stop() 333 defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() 334 335 min, max := int(-inf), int(inf) 336 if l.Position.First == 0 { 337 // Use the size of the invisible part as scroll boundary. 338 min = -l.Position.Offset 339 if min > 0 { 340 min = 0 341 } 342 } 343 if l.Position.First+l.Position.Count == l.len { 344 max = -l.Position.OffsetLast 345 if max < 0 { 346 max = 0 347 } 348 } 349 scrollRange := image.Rectangle{ 350 Min: l.Axis.Convert(image.Pt(min, 0)), 351 Max: l.Axis.Convert(image.Pt(max, 0)), 352 } 353 l.scroll.Add(ops, scrollRange) 354 355 call.Add(ops) 356 return Dimensions{Size: dims} 357 } 358 359 // ScrollBy scrolls the list by a relative amount of items. 360 // 361 // Fractional scrolling may be inaccurate for items of differing 362 // dimensions. This includes scrolling by integer amounts if the current 363 // l.Position.Offset is non-zero. 364 func (l *List) ScrollBy(num float32) { 365 // Split number of items into integer and fractional parts 366 i, f := math.Modf(float64(num)) 367 368 // Scroll by integer amount of items 369 l.Position.First += int(i) 370 371 // Adjust Offset to account for fractional items. If Offset gets so large that it amounts to an entire item, then 372 // the layout code will handle that for us and adjust First and Offset accordingly. 373 itemHeight := float64(l.Position.Length) / float64(l.len) 374 l.Position.Offset += int(math.Round(itemHeight * f)) 375 376 // First and Offset can go out of bounds, but the layout code knows how to handle that. 377 378 // Ensure that the list pays attention to the Offset field when the scrollbar drag 379 // is started while the bar is at the end of the list. Without this, the scrollbar 380 // cannot be dragged away from the end. 381 l.Position.BeforeEnd = true 382 } 383 384 // ScrollTo scrolls to the specified item. 385 func (l *List) ScrollTo(n int) { 386 l.Position.First = n 387 l.Position.Offset = 0 388 l.Position.BeforeEnd = true 389 }