github.com/utopiagio/gio@v0.0.8/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/utopiagio/gio/gesture" 10 "github.com/utopiagio/gio/op" 11 "github.com/utopiagio/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 min, max := int(-inf), int(inf) 148 if l.Position.First == 0 { 149 // Use the size of the invisible part as scroll boundary. 150 min = -l.Position.Offset 151 if min > 0 { 152 min = 0 153 } 154 } 155 if l.Position.First+l.Position.Count == l.len { 156 max = -l.Position.OffsetLast 157 if max < 0 { 158 max = 0 159 } 160 } 161 scrollRange := image.Rectangle{ 162 Min: l.Axis.Convert(image.Pt(min, 0)), 163 Max: l.Axis.Convert(image.Pt(max, 0)), 164 } 165 d := l.scroll.Update(gtx.Metric, gtx.Source, gtx.Now, gesture.Axis(l.Axis), scrollRange) 166 l.scrollDelta = d 167 l.Position.Offset += d 168 } 169 170 // next advances to the next child. 171 func (l *List) next() { 172 l.dir = l.nextDir() 173 // The user scroll offset is applied after scrolling to 174 // list end. 175 if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 { 176 l.Position.BeforeEnd = true 177 l.Position.Offset += l.scrollDelta 178 l.dir = l.nextDir() 179 } 180 } 181 182 // index is current child's position in the underlying list. 183 func (l *List) index() int { 184 switch l.dir { 185 case iterateBackward: 186 return l.Position.First - 1 187 case iterateForward: 188 return l.Position.First + len(l.children) 189 default: 190 panic("Index called before Next") 191 } 192 } 193 194 // more reports whether more children are needed. 195 func (l *List) more() bool { 196 return l.dir != iterateNone 197 } 198 199 func (l *List) nextDir() iterationDir { 200 _, vsize := l.Axis.mainConstraint(l.cs) 201 last := l.Position.First + len(l.children) 202 // Clamp offset. 203 if l.maxSize-l.Position.Offset < vsize && last == l.len { 204 l.Position.Offset = l.maxSize - vsize 205 } 206 if l.Position.Offset < 0 && l.Position.First == 0 { 207 l.Position.Offset = 0 208 } 209 // Lay out an extra (invisible) child at each end to enable focus to 210 // move to them, triggering automatic scroll. 211 firstSize, lastSize := 0, 0 212 if len(l.children) > 0 { 213 if l.Position.First > 0 { 214 firstChild := l.children[0] 215 firstSize = l.Axis.Convert(firstChild.size).X 216 } 217 if last < l.len { 218 lastChild := l.children[len(l.children)-1] 219 lastSize = l.Axis.Convert(lastChild.size).X 220 } 221 } 222 switch { 223 case len(l.children) == l.len: 224 return iterateNone 225 case l.maxSize-l.Position.Offset-lastSize < vsize: 226 return iterateForward 227 case l.Position.Offset-firstSize < 0: 228 return iterateBackward 229 } 230 return iterateNone 231 } 232 233 // End the current child by specifying its dimensions. 234 func (l *List) end(dims Dimensions, call op.CallOp) { 235 child := scrollChild{dims.Size, call} 236 mainSize := l.Axis.Convert(child.size).X 237 l.maxSize += mainSize 238 switch l.dir { 239 case iterateForward: 240 l.children = append(l.children, child) 241 case iterateBackward: 242 l.children = append(l.children, scrollChild{}) 243 copy(l.children[1:], l.children) 244 l.children[0] = child 245 l.Position.First-- 246 l.Position.Offset += mainSize 247 default: 248 panic("call Next before End") 249 } 250 l.dir = iterateNone 251 } 252 253 // Layout the List and return its dimensions. 254 func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions { 255 if l.more() { 256 panic("unfinished child") 257 } 258 mainMin, mainMax := l.Axis.mainConstraint(l.cs) 259 children := l.children 260 var first scrollChild 261 // Skip invisible children. 262 for len(children) > 0 { 263 child := children[0] 264 sz := child.size 265 mainSize := l.Axis.Convert(sz).X 266 if l.Position.Offset < mainSize { 267 // First child is partially visible. 268 break 269 } 270 l.Position.First++ 271 l.Position.Offset -= mainSize 272 first = child 273 children = children[1:] 274 } 275 size := -l.Position.Offset 276 var maxCross int 277 var last scrollChild 278 for i, child := range children { 279 sz := l.Axis.Convert(child.size) 280 if c := sz.Y; c > maxCross { 281 maxCross = c 282 } 283 size += sz.X 284 if size >= mainMax { 285 if i < len(children)-1 { 286 last = children[i+1] 287 } 288 children = children[:i+1] 289 break 290 } 291 } 292 l.Position.Count = len(children) 293 l.Position.OffsetLast = mainMax - size 294 // ScrollToEnd lists are end aligned. 295 if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 { 296 l.Position.Offset -= space 297 } 298 pos := -l.Position.Offset 299 layout := func(child scrollChild) { 300 sz := l.Axis.Convert(child.size) 301 var cross int 302 switch l.Alignment { 303 case End: 304 cross = maxCross - sz.Y 305 case Middle: 306 cross = (maxCross - sz.Y) / 2 307 } 308 childSize := sz.X 309 min := pos 310 if min < 0 { 311 min = 0 312 } 313 pt := l.Axis.Convert(image.Pt(pos, cross)) 314 trans := op.Offset(pt).Push(ops) 315 child.call.Add(ops) 316 trans.Pop() 317 pos += childSize 318 } 319 // Lay out leading invisible child. 320 if first != (scrollChild{}) { 321 sz := l.Axis.Convert(first.size) 322 pos -= sz.X 323 layout(first) 324 } 325 for _, child := range children { 326 layout(child) 327 } 328 // Lay out trailing invisible child. 329 if last != (scrollChild{}) { 330 layout(last) 331 } 332 atStart := l.Position.First == 0 && l.Position.Offset <= 0 333 atEnd := l.Position.First+len(children) == l.len && mainMax >= pos 334 if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 { 335 l.scroll.Stop() 336 } 337 l.Position.BeforeEnd = !atEnd 338 if pos < mainMin { 339 pos = mainMin 340 } 341 if pos > mainMax { 342 pos = mainMax 343 } 344 if crossMin, crossMax := l.Axis.crossConstraint(l.cs); maxCross < crossMin { 345 maxCross = crossMin 346 } else if maxCross > crossMax { 347 maxCross = crossMax 348 } 349 dims := l.Axis.Convert(image.Pt(pos, maxCross)) 350 call := macro.Stop() 351 defer clip.Rect(image.Rectangle{Max: dims}).Push(ops).Pop() 352 353 l.scroll.Add(ops) 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 // ************************************************************************** 385 // *************** RNW Added ScrollOffsetBy (dx) 12.03.2024 ***************** 386 // ScrollOffsetBy scrolls by the specified offset. dx - pixels 387 func (l *List) ScrollOffsetBy(dx int) { 388 l.Position.First = 0 389 // Adjust Offset to account for fractional items. If Offset gets so large that it amounts to an entire item, then 390 // the layout code will handle that for us and adjust First and Offset accordingly. 391 l.Position.Offset += dx 392 // First and Offset can go out of bounds, but the layout code knows how to handle that. 393 394 // Ensure that the list pays attention to the Offset field when the scrollbar drag 395 // is started while the bar is at the end of the list. Without this, the scrollbar 396 // cannot be dragged away from the end. 397 l.Position.BeforeEnd = true 398 } 399 // ************************************************************************** 400 401 // ************************************************************************** 402 // *************** RNW Added ScrollToOffset (dx) 12.03.2024 ***************** 403 // ScrollToOffset scrolls to the specified offset. dx - pixels 404 func (l *List) ScrollToOffset(dx int) { 405 l.Position.First = 0 406 l.Position.Offset = dx 407 l.Position.BeforeEnd = true 408 } 409 // ************************************************************************** 410 411 412 // ScrollTo scrolls to the specified item. 413 func (l *List) ScrollTo(n int) { 414 l.Position.First = n 415 l.Position.Offset = 0 416 l.Position.BeforeEnd = true 417 }