1 // SPDX-License-Identifier: Unlicense OR MIT
2 3 package layout
4 5 import (
6 "image"
7 8 "github.com/p9c/p9/pkg/gel/gio/gesture"
9 "github.com/p9c/p9/pkg/gel/gio/io/pointer"
10 "github.com/p9c/p9/pkg/gel/gio/op"
11 "github.com/p9c/p9/pkg/gel/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 top edge to the child at index
70 // First.
71 Offset int
72 // OffsetLast is the signed distance in pixels from the bottom 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 }
78 79 const (
80 iterateNone iterationDir = iota
81 iterateForward
82 iterateBackward
83 )
84 85 const inf = 1e6
86 87 // init prepares the list for iterating through its children with next.
88 func (l *List) init(gtx Context, len int) {
89 if l.more() {
90 panic("unfinished child")
91 }
92 l.cs = gtx.Constraints
93 l.maxSize = 0
94 l.children = l.children[:0]
95 l.len = len
96 l.update(gtx)
97 if l.scrollToEnd() || l.Position.First > len {
98 l.Position.Offset = 0
99 l.Position.First = len
100 }
101 }
102 103 // Layout the List.
104 func (l *List) Layout(gtx Context, len int, w ListElement) Dimensions {
105 l.init(gtx, len)
106 crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints)
107 gtx.Constraints = l.Axis.constraints(0, inf, crossMin, crossMax)
108 macro := op.Record(gtx.Ops)
109 for l.next(); l.more(); l.next() {
110 child := op.Record(gtx.Ops)
111 dims := w(gtx, l.index())
112 call := child.Stop()
113 l.end(dims, call)
114 }
115 return l.layout(gtx.Ops, macro)
116 }
117 118 func (l *List) scrollToEnd() bool {
119 return l.ScrollToEnd && !l.Position.BeforeEnd
120 }
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 127 func (l *List) update(gtx Context) {
128 d := l.scroll.Scroll(gtx.Metric, gtx, gtx.Now, gesture.Axis(l.Axis))
129 l.scrollDelta = d
130 l.Position.Offset += d
131 }
132 133 // next advances to the next child.
134 func (l *List) next() {
135 l.dir = l.nextDir()
136 // The user scroll offset is applied after scrolling to
137 // list end.
138 if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 {
139 l.Position.BeforeEnd = true
140 l.Position.Offset += l.scrollDelta
141 l.dir = l.nextDir()
142 }
143 }
144 145 // index is current child's position in the underlying list.
146 func (l *List) index() int {
147 switch l.dir {
148 case iterateBackward:
149 return l.Position.First - 1
150 case iterateForward:
151 return l.Position.First + len(l.children)
152 default:
153 panic("Index called before Next")
154 }
155 }
156 157 // more reports whether more children are needed.
158 func (l *List) more() bool {
159 return l.dir != iterateNone
160 }
161 162 func (l *List) nextDir() iterationDir {
163 _, vsize := l.Axis.mainConstraint(l.cs)
164 last := l.Position.First + len(l.children)
165 // Clamp offset.
166 if l.maxSize-l.Position.Offset < vsize && last == l.len {
167 l.Position.Offset = l.maxSize - vsize
168 }
169 if l.Position.Offset < 0 && l.Position.First == 0 {
170 l.Position.Offset = 0
171 }
172 switch {
173 case len(l.children) == l.len:
174 return iterateNone
175 case l.maxSize-l.Position.Offset < vsize:
176 return iterateForward
177 case l.Position.Offset < 0:
178 return iterateBackward
179 }
180 return iterateNone
181 }
182 183 // End the current child by specifying its dimensions.
184 func (l *List) end(dims Dimensions, call op.CallOp) {
185 child := scrollChild{dims.Size, call}
186 mainSize := l.Axis.Convert(child.size).X
187 l.maxSize += mainSize
188 switch l.dir {
189 case iterateForward:
190 l.children = append(l.children, child)
191 case iterateBackward:
192 l.children = append(l.children, scrollChild{})
193 copy(l.children[1:], l.children)
194 l.children[0] = child
195 l.Position.First--
196 l.Position.Offset += mainSize
197 default:
198 panic("call Next before End")
199 }
200 l.dir = iterateNone
201 }
202 203 // Layout the List and return its dimensions.
204 func (l *List) layout(ops *op.Ops, macro op.MacroOp) Dimensions {
205 if l.more() {
206 panic("unfinished child")
207 }
208 mainMin, mainMax := l.Axis.mainConstraint(l.cs)
209 children := l.children
210 // Skip invisible children
211 for len(children) > 0 {
212 sz := children[0].size
213 mainSize := l.Axis.Convert(sz).X
214 if l.Position.Offset < mainSize {
215 // First child is partially visible.
216 break
217 }
218 l.Position.First++
219 l.Position.Offset -= mainSize
220 children = children[1:]
221 }
222 size := -l.Position.Offset
223 var maxCross int
224 for i, child := range children {
225 sz := l.Axis.Convert(child.size)
226 if c := sz.Y; c > maxCross {
227 maxCross = c
228 }
229 size += sz.X
230 if size >= mainMax {
231 children = children[:i+1]
232 break
233 }
234 }
235 l.Position.Count = len(children)
236 l.Position.OffsetLast = mainMax - size
237 pos := -l.Position.Offset
238 // ScrollToEnd lists are end aligned.
239 if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 {
240 pos += space
241 }
242 for _, child := range children {
243 sz := l.Axis.Convert(child.size)
244 var cross int
245 switch l.Alignment {
246 case End:
247 cross = maxCross - sz.Y
248 case Middle:
249 cross = (maxCross - sz.Y) / 2
250 }
251 childSize := sz.X
252 max := childSize + pos
253 if max > mainMax {
254 max = mainMax
255 }
256 min := pos
257 if min < 0 {
258 min = 0
259 }
260 r := image.Rectangle{
261 Min: l.Axis.Convert(image.Pt(min, -inf)),
262 Max: l.Axis.Convert(image.Pt(max, inf)),
263 }
264 stack := op.Save(ops)
265 clip.Rect(r).Add(ops)
266 pt := l.Axis.Convert(image.Pt(pos, cross))
267 op.Offset(FPt(pt)).Add(ops)
268 child.call.Add(ops)
269 stack.Load()
270 pos += childSize
271 }
272 atStart := l.Position.First == 0 && l.Position.Offset <= 0
273 atEnd := l.Position.First+len(children) == l.len && mainMax >= pos
274 if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
275 l.scroll.Stop()
276 }
277 l.Position.BeforeEnd = !atEnd
278 if pos < mainMin {
279 pos = mainMin
280 }
281 if pos > mainMax {
282 pos = mainMax
283 }
284 dims := l.Axis.Convert(image.Pt(pos, maxCross))
285 call := macro.Stop()
286 defer op.Save(ops).Load()
287 pointer.Rect(image.Rectangle{Max: dims}).Add(ops)
288 289 var min, max int
290 if o := l.Position.Offset; o > 0 {
291 // Use the size of the invisible part as scroll boundary.
292 min = -o
293 } else if l.Position.First > 0 {
294 min = -inf
295 }
296 if o := l.Position.OffsetLast; o < 0 {
297 max = -o
298 } else if l.Position.First+l.Position.Count < l.len {
299 max = inf
300 }
301 scrollRange := image.Rectangle{
302 Min: l.Axis.Convert(image.Pt(min, 0)),
303 Max: l.Axis.Convert(image.Pt(max, 0)),
304 }
305 l.scroll.Add(ops, scrollRange)
306 307 call.Add(ops)
308 return Dimensions{Size: dims}
309 }
310