list.go 10 KB

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