list.go raw
1 package gel
2
3 import (
4 "image"
5 "time"
6
7 "github.com/p9c/gio/gesture"
8 "github.com/p9c/gio/io/pointer"
9 l "github.com/p9c/gio/layout"
10 "github.com/p9c/gio/op"
11 "github.com/p9c/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 large underlying list. List accepts user input to scroll the
20 // subsection.
21 type List struct {
22 axis l.Axis
23 // ScrollToEnd instructs the list to stay scrolled to the far end position once reached. A List with ScrollToEnd ==
24 // true and Position.BeforeEnd == false draws its content with the last item at the bottom of the list area.
25 scrollToEnd bool
26 // Alignment is the cross axis alignment of list elements.
27 alignment l.Alignment
28 scroll gesture.Scroll
29 scrollDelta int
30 // position is updated during Layout. To save the list scroll position, just save Position after Layout finishes. To
31 // scroll the list programmatically, update Position (e.g. restore it from a saved value) before calling Layout.
32 // nextUp, nextDown Position
33 position Position
34 Len int
35 // maxSize is the total size of visible children.
36 maxSize int
37 children []scrollChild
38 dir iterationDir
39
40 // all below are additional fields to implement the scrollbar
41 *Window
42 // we store the constraints here instead of in the `cs` field
43 ctx l.Context
44 sideScroll gesture.Scroll
45 disableScroll bool
46 drag gesture.Drag
47 recentPageClick time.Time
48 color string
49 active string
50 background string
51 currentColor string
52 scrollWidth int
53 setScrollWidth int
54 length int
55 prevLength int
56 w ListElement
57 pageUp, pageDown *Clickable
58 dims DimensionList
59 cross int
60 view, total, before int
61 top, middle, bottom int
62 lastWidth int
63 recalculateTime time.Time
64 recalculate bool
65 notFirst bool
66 leftSide bool
67 }
68
69 // List returns a new scrollable List widget
70 func (w *Window) List() (li *List) {
71 li = &List{
72 Window: w,
73 pageUp: w.WidgetPool.GetClickable(),
74 pageDown: w.WidgetPool.GetClickable(),
75 color: "DocText",
76 background: "Transparent",
77 active: "Primary",
78 scrollWidth: int(w.TextSize.Scale(0.75).V),
79 setScrollWidth: int(w.TextSize.Scale(0.75).V),
80 recalculateTime: time.Now().Add(-time.Second),
81 recalculate: true,
82 }
83 li.currentColor = li.color
84 return
85 }
86
87 // ListElement is a function that computes the dimensions of a list element.
88 type ListElement func(gtx l.Context, index int) l.Dimensions
89
90 type iterationDir uint8
91
92 // Position is a List scroll offset represented as an offset from the top edge of a child element.
93 type Position struct {
94 // BeforeEnd tracks whether the List position is before the very end. We use "before end" instead of "at end" so
95 // that the zero value of a Position struct is useful.
96 //
97 // When laying out a list, if ScrollToEnd is true and BeforeEnd is false, then First and Offset are ignored, and the
98 // list is drawn with the last item at the bottom. If ScrollToEnd is false then BeforeEnd is ignored.
99 BeforeEnd bool
100 // First is the index of the first visible child.
101 First int
102 // Offset is the distance in pixels from the top edge to the child at index First.
103 Offset int
104 // OffsetLast is the signed distance in pixels from the bottom edge to the
105 // bottom edge of the child at index First+Count.
106 OffsetLast int
107 // Count is the number of visible children.
108 Count int
109 }
110
111 const (
112 iterateNone iterationDir = iota
113 iterateForward
114 iterateBackward
115 )
116
117 // init prepares the list for iterating through its children with next.
118 func (li *List) init(gtx l.Context, length int) {
119 if li.more() {
120 panic("unfinished child")
121 }
122 li.ctx = gtx
123 li.maxSize = 0
124 li.children = li.children[:0]
125 li.Len = length
126 li.update()
127 if li.canScrollToEnd() || li.position.First > length {
128 li.position.Offset = 0
129 li.position.First = length
130 }
131 }
132
133 // Layout the List.
134 func (li *List) Layout(gtx l.Context, len int, w ListElement) l.Dimensions {
135 li.init(gtx, len)
136 crossMin, crossMax := axisCrossConstraint(li.axis, gtx.Constraints)
137 gtx.Constraints = axisConstraints(li.axis, 0, Inf, crossMin, crossMax)
138 macro := op.Record(gtx.Ops)
139 for li.next(); li.more(); li.next() {
140 child := op.Record(gtx.Ops)
141 dims := w(gtx, li.index())
142 call := child.Stop()
143 li.end(dims, call)
144 }
145 return li.layout(macro)
146 }
147
148 // canScrollToEnd returns true if there is room to scroll further towards the end
149 func (li *List) canScrollToEnd() bool {
150 return li.scrollToEnd && !li.position.BeforeEnd
151 }
152
153 // Dragging reports whether the List is being dragged.
154 func (li *List) Dragging() bool {
155 return li.scroll.State() == gesture.StateDragging ||
156 li.sideScroll.State() == gesture.StateDragging
157 }
158
159 // update the scrolling
160 func (li *List) update() {
161 d := li.scroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, gesture.Axis(li.axis))
162 d += li.sideScroll.Scroll(li.ctx.Metric, li.ctx, li.ctx.Now, gesture.Axis(li.axis))
163 li.scrollDelta = d
164 li.position.Offset += d
165 }
166
167 // next advances to the next child.
168 func (li *List) next() {
169 li.dir = li.nextDir()
170 // The user scroll offset is applied after scrolling to list end.
171 if li.canScrollToEnd() && !li.more() && li.scrollDelta < 0 {
172 li.position.BeforeEnd = true
173 li.position.Offset += li.scrollDelta
174 li.dir = li.nextDir()
175 }
176 }
177
178 // index is current child's position in the underlying list.
179 func (li *List) index() int {
180 switch li.dir {
181 case iterateBackward:
182 return li.position.First - 1
183 case iterateForward:
184 return li.position.First + len(li.children)
185 default:
186 panic("Index called before Next")
187 }
188 }
189
190 // more reports whether more children are needed.
191 func (li *List) more() bool {
192 return li.dir != iterateNone
193 }
194
195 func (li *List) nextDir() iterationDir {
196 _, vSize := axisMainConstraint(li.axis, li.ctx.Constraints)
197 last := li.position.First + len(li.children)
198 // Clamp offset.
199 if li.maxSize-li.position.Offset < vSize && last == li.Len {
200 li.position.Offset = li.maxSize - vSize
201 }
202 if li.position.Offset < 0 && li.position.First == 0 {
203 li.position.Offset = 0
204 }
205 switch {
206 case len(li.children) == li.Len:
207 return iterateNone
208 case li.maxSize-li.position.Offset < vSize:
209 return iterateForward
210 case li.position.Offset < 0:
211 return iterateBackward
212 }
213 return iterateNone
214 }
215
216 // End the current child by specifying its dimensions.
217 func (li *List) end(dims l.Dimensions, call op.CallOp) {
218 child := scrollChild{dims.Size, call}
219 mainSize := axisConvert(li.axis, child.size).X
220 li.maxSize += mainSize
221 switch li.dir {
222 case iterateForward:
223 li.children = append(li.children, child)
224 case iterateBackward:
225 li.children = append(li.children, scrollChild{})
226 copy(li.children[1:], li.children)
227 li.children[0] = child
228 li.position.First--
229 li.position.Offset += mainSize
230 default:
231 panic("call Next before End")
232 }
233 li.dir = iterateNone
234 }
235
236 // layout the List and return its dimensions.
237 func (li *List) layout(macro op.MacroOp) l.Dimensions {
238 if li.more() {
239 panic("unfinished child")
240 }
241 mainMin, mainMax := axisMainConstraint(li.axis, li.ctx.Constraints)
242 children := li.children
243 // Skip invisible children
244 for len(children) > 0 {
245 sz := children[0].size
246 mainSize := axisConvert(li.axis, sz).X
247 if li.position.Offset < mainSize {
248 // First child is partially visible.
249 break
250 }
251 li.position.First++
252 li.position.Offset -= mainSize
253 children = children[1:]
254 }
255 size := -li.position.Offset
256 var maxCross int
257 for i, child := range children {
258 sz := axisConvert(li.axis, child.size)
259 if c := sz.Y; c > maxCross {
260 maxCross = c
261 }
262 size += sz.X
263 if size >= mainMax {
264 children = children[:i+1]
265 break
266 }
267 }
268 li.position.Count = len(children)
269 li.position.OffsetLast = mainMax - size
270 ops := li.ctx.Ops
271 pos := -li.position.Offset
272 // ScrollToEnd lists are end aligned.
273 if space := li.position.OffsetLast; li.scrollToEnd && space > 0 {
274 pos += space
275 }
276 for _, child := range children {
277 sz := axisConvert(li.axis, child.size)
278 var cross int
279 switch li.alignment {
280 case l.End:
281 cross = maxCross - sz.Y
282 case l.Middle:
283 cross = (maxCross - sz.Y) / 2
284 }
285 childSize := sz.X
286 max := childSize + pos
287 if max > mainMax {
288 max = mainMax
289 }
290 min := pos
291 if min < 0 {
292 min = 0
293 }
294 r := image.Rectangle{
295 Min: axisConvert(li.axis, image.Pt(min, -Inf)),
296 Max: axisConvert(li.axis, image.Pt(max, Inf)),
297 }
298 stack := op.Save(ops)
299 clip.Rect(r).Add(ops)
300 pt := axisConvert(li.axis, image.Pt(pos, cross))
301 op.Offset(Fpt(pt)).Add(ops)
302 child.call.Add(ops)
303 stack.Load()
304 pos += childSize
305 }
306 atStart := li.position.First == 0 && li.position.Offset <= 0
307 atEnd := li.position.First+len(children) == li.Len && mainMax >= pos
308 if atStart && li.scrollDelta < 0 || atEnd && li.scrollDelta > 0 {
309 li.scroll.Stop()
310 li.sideScroll.Stop()
311 }
312 li.position.BeforeEnd = !atEnd
313 if pos < mainMin {
314 pos = mainMin
315 }
316 if pos > mainMax {
317 pos = mainMax
318 }
319 dims := axisConvert(li.axis, image.Pt(pos, maxCross))
320 call := macro.Stop()
321 defer op.Save(ops).Load()
322 bounds := image.Rectangle{Max: dims}
323 pointer.Rect(bounds).Add(ops)
324 // li.sideScroll.Add(ops, bounds)
325 // li.scroll.Add(ops, bounds)
326
327 var min, max int
328 if o := li.position.Offset; o > 0 {
329 // Use the size of the invisible part as scroll boundary.
330 min = -o
331 } else if li.position.First > 0 {
332 min = -Inf
333 }
334 if o := li.position.OffsetLast; o < 0 {
335 max = -o
336 } else if li.position.First+li.position.Count < li.Len {
337 max = Inf
338 }
339 scrollRange := image.Rectangle{
340 Min: axisConvert(li.axis, image.Pt(min, 0)),
341 Max: axisConvert(li.axis, image.Pt(max, 0)),
342 }
343 li.scroll.Add(ops, scrollRange)
344 li.sideScroll.Add(ops, scrollRange)
345
346 call.Add(ops)
347 return l.Dimensions{Size: dims}
348 }
349
350 // Everything below is extensions on the original from gioui.org/layout
351
352 // Position returns the current position of the scroller
353 func (li *List) Position() Position {
354 return li.position
355 }
356
357 // SetPosition sets the position of the scroller
358 func (li *List) SetPosition(position Position) {
359 li.position = position
360 }
361
362 // JumpToStart moves the position to the start
363 func (li *List) JumpToStart() {
364 li.position = Position{}
365 }
366
367 // JumpToEnd moves the position to the end
368 func (li *List) JumpToEnd() {
369 li.position = Position{
370 BeforeEnd: false,
371 First: len(li.dims),
372 Offset: axisMain(li.axis, li.dims[len(li.dims)-1].Size),
373 }
374 }
375
376 // Vertical sets the axis to vertical (default implicit is horizontal)
377 func (li *List) Vertical() (out *List) {
378 li.axis = l.Vertical
379 return li
380 }
381
382 // Start sets the alignment to start
383 func (li *List) Start() *List {
384 li.alignment = l.Start
385 return li
386 }
387
388 // End sets the alignment to end
389 func (li *List) End() *List {
390 li.alignment = l.End
391 return li
392 }
393
394 // Middle sets the alignment to middle
395 func (li *List) Middle() *List {
396 li.alignment = l.Middle
397 return li
398 }
399
400 // Baseline sets the alignment to baseline
401 func (li *List) Baseline() *List {
402 li.alignment = l.Baseline
403 return li
404 }
405
406 // ScrollToEnd sets the List to add new items to the end and push older ones up/left and initial render has scroll
407 // to the end (or bottom) of the List
408 func (li *List) ScrollToEnd() (out *List) {
409 li.scrollToEnd = true
410 return li
411 }
412
413 // LeftSide sets the scroller to be on the opposite side from usual
414 func (li *List) LeftSide(b bool) (out *List) {
415 li.leftSide = b
416 return li
417 }
418
419 // Length sets the new length for the list
420 func (li *List) Length(length int) *List {
421 li.prevLength = li.length
422 li.length = length
423 return li
424 }
425
426 // DisableScroll turns off the scrollbar
427 func (li *List) DisableScroll(disable bool) *List {
428 li.disableScroll = disable
429 if disable {
430 li.scrollWidth = 0
431 } else {
432 li.scrollWidth = li.setScrollWidth
433 }
434 return li
435 }
436
437 // ListElement defines the function that returns list elements
438 func (li *List) ListElement(w ListElement) *List {
439 li.w = w
440 return li
441 }
442
443 // ScrollWidth sets the width of the scrollbar
444 func (li *List) ScrollWidth(width int) *List {
445 li.scrollWidth = width
446 li.setScrollWidth = width
447 return li
448 }
449
450 // Color sets the primary color of the scrollbar grabber
451 func (li *List) Color(color string) *List {
452 li.color = color
453 li.currentColor = li.color
454 return li
455 }
456
457 // Background sets the background color of the scrollbar
458 func (li *List) Background(color string) *List {
459 li.background = color
460 return li
461 }
462
463 // Active sets the color of the scrollbar grabber when it is being operated
464 func (li *List) Active(color string) *List {
465 li.active = color
466 return li
467 }
468
469 func (li *List) Slice(gtx l.Context, widgets ...l.Widget) l.Widget {
470 return li.Length(len(widgets)).Vertical().ListElement(func(gtx l.Context, index int) l.Dimensions {
471 return widgets[index](gtx)
472 },
473 ).Fn
474 }
475
476 // Fn runs the layout in the configured context. The ListElement function returns the widget at the given index
477 func (li *List) Fn(gtx l.Context) l.Dimensions {
478 if li.length == 0 {
479 // if there is no children just return a big empty box
480 return EmptyFromSize(gtx.Constraints.Max)(gtx)
481 }
482 if li.disableScroll {
483 return li.embedWidget(0)(gtx)
484 }
485 if li.length != li.prevLength {
486 li.recalculate = true
487 li.recalculateTime = time.Now().Add(time.Millisecond * 100)
488 } else if li.lastWidth != gtx.Constraints.Max.X && li.notFirst {
489 li.recalculateTime = time.Now().Add(time.Millisecond * 100)
490 li.recalculate = true
491 }
492 if !li.notFirst {
493 li.recalculateTime = time.Now().Add(-time.Millisecond * 100)
494 li.notFirst = true
495 }
496 li.lastWidth = gtx.Constraints.Max.X
497 if li.recalculateTime.Sub(time.Now()) < 0 && li.recalculate {
498 li.scrollBarSize = li.scrollWidth // + li.scrollBarPad
499 gtx1 := CopyContextDimensionsWithMaxAxis(gtx, li.axis)
500 // generate the dimensions for all the list elements
501 li.dims = GetDimensionList(gtx1, li.length, li.w)
502 li.recalculateTime = time.Time{}
503 li.recalculate = false
504 }
505 _, li.view = axisMainConstraint(li.axis, gtx.Constraints)
506 _, li.cross = axisCrossConstraint(li.axis, gtx.Constraints)
507 li.total, li.before = li.dims.GetSizes(li.position, li.axis)
508 if li.total == 0 {
509 // if there is no children just return a big empty box
510 return EmptyFromSize(gtx.Constraints.Max)(gtx)
511 }
512 if li.total < li.view {
513 // if the contents fit the view, don't show the scrollbar
514 li.top, li.middle, li.bottom = 0, 0, 0
515 li.scrollWidth = 0
516 } else {
517 li.scrollWidth = li.setScrollWidth
518 li.top = li.before * (li.view - li.scrollWidth) / li.total
519 li.middle = li.view * (li.view - li.scrollWidth) / li.total
520 li.bottom = (li.total - li.before - li.view) * (li.view - li.scrollWidth) / li.total
521 if li.view < li.scrollWidth {
522 li.middle = li.view
523 li.top, li.bottom = 0, 0
524 } else {
525 li.middle += li.scrollWidth
526 }
527 }
528 // now lay it all out and draw the list and scrollbar
529 var container l.Widget
530 if li.axis == l.Horizontal {
531 containerFlex := li.Theme.VFlex()
532 if !li.leftSide {
533 containerFlex.Rigid(li.embedWidget(li.scrollWidth /* + int(li.TextSize.True)/4)*/))
534 containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, int(li.TextSize.V)/4))
535 }
536 containerFlex.Rigid(
537 li.VFlex().
538 Rigid(
539 func(gtx l.Context) l.Dimensions {
540 pointer.Rect(image.Rectangle{Max: image.Point{X: gtx.Constraints.Max.X,
541 Y: gtx.Constraints.Max.Y,
542 },
543 },
544 ).Add(gtx.Ops)
545 li.drag.Add(gtx.Ops)
546 return li.Theme.Flex().
547 Rigid(li.pageUpDown(li.dims, li.view, li.total,
548 // li.scrollBarPad+
549 li.scrollWidth, li.top, false,
550 ),
551 ).
552 Rigid(li.grabber(li.dims, li.scrollWidth, li.middle,
553 li.view, gtx.Constraints.Max.X,
554 ),
555 ).
556 Rigid(li.pageUpDown(li.dims, li.view, li.total,
557 // li.scrollBarPad+
558 li.scrollWidth, li.bottom, true,
559 ),
560 ).
561 Fn(gtx)
562 },
563 ).
564 Fn,
565 )
566 if li.leftSide {
567 containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/4, int(li.TextSize.V)/4))
568 containerFlex.Rigid(li.embedWidget(li.scrollWidth)) // li.scrollWidth)) // + li.scrollBarPad))
569 }
570 container = containerFlex.Fn
571 } else {
572 containerFlex := li.Theme.Flex()
573 if !li.leftSide {
574 containerFlex.Rigid(li.embedWidget(li.scrollWidth + int(li.TextSize.V)/2)) // + li.scrollBarPad))
575 containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, int(li.TextSize.V)/2))
576 }
577 containerFlex.Rigid(
578 li.Fill(li.background, l.Center, li.TextSize.V/4, 0, li.Flex().
579 Rigid(
580 func(gtx l.Context) l.Dimensions {
581 pointer.Rect(image.Rectangle{Max: image.Point{X: gtx.Constraints.Max.X,
582 Y: gtx.Constraints.Max.Y,
583 },
584 },
585 ).Add(gtx.Ops)
586 li.drag.Add(gtx.Ops)
587 return li.Theme.Flex().Vertical().
588 Rigid(li.pageUpDown(li.dims, li.view, li.total,
589 li.scrollWidth, li.top, false,
590 ),
591 ).
592 Rigid(li.grabber(li.dims,
593 li.scrollWidth, li.middle,
594 li.view, gtx.Constraints.Max.X,
595 ),
596 ).
597 Rigid(li.pageUpDown(li.dims, li.view, li.total,
598 li.scrollWidth, li.bottom, true,
599 ),
600 ).
601 Fn(gtx)
602 },
603 ).
604 Fn,
605 ).Fn,
606 )
607 if li.leftSide {
608 containerFlex.Rigid(EmptySpace(int(li.TextSize.V)/2, int(li.TextSize.V)/2))
609 containerFlex.Rigid(li.embedWidget(li.scrollWidth + int(li.TextSize.V)/2))
610 }
611 container = li.Fill(li.background, l.Center, li.TextSize.V/4, 0, containerFlex.Fn).Fn
612 }
613 return container(gtx)
614 }
615
616 // EmbedWidget places the scrollable content
617 func (li *List) embedWidget(scrollWidth int) func(l.Context) l.Dimensions {
618 return func(gtx l.Context) l.Dimensions {
619 if li.axis == l.Horizontal {
620 gtx.Constraints.Min.Y = gtx.Constraints.Max.Y - scrollWidth
621 gtx.Constraints.Max.Y = gtx.Constraints.Min.Y
622 } else {
623 gtx.Constraints.Min.X = gtx.Constraints.Max.X - scrollWidth
624 gtx.Constraints.Max.X = gtx.Constraints.Min.X
625 }
626 return li.Layout(gtx, li.length, li.w)
627 }
628 }
629
630 // pageUpDown creates the clickable areas either side of the grabber that trigger a page up/page down action
631 func (li *List) pageUpDown(dims DimensionList, view, total, x, y int, down bool) func(l.Context) l.Dimensions {
632 button := li.pageUp
633 if down {
634 button = li.pageDown
635 }
636 return func(gtx l.Context) l.Dimensions {
637 bounds := image.Rectangle{Max: gtx.Constraints.Max}
638 pointer.Rect(bounds).Add(gtx.Ops)
639 li.sideScroll.Add(gtx.Ops, bounds)
640 return li.ButtonLayout(button.SetClick(func() {
641 current := dims.PositionToCoordinate(li.position, li.axis)
642 var newPos int
643 if down {
644 if current+view > total {
645 newPos = total - view
646 } else {
647 newPos = current + view
648 }
649 } else {
650 newPos = current - view
651 if newPos < 0 {
652 newPos = 0
653 }
654 }
655 li.position = dims.CoordinateToPosition(newPos, li.axis)
656 },
657 ).
658 SetPress(func() { li.recentPageClick = time.Now() }),
659 ).Embed(
660 li.Flex().
661 Rigid(EmptySpace(x/4, y)).
662 Rigid(
663 li.Fill("scrim", l.Center, li.TextSize.V/4, 0, EmptySpace(x/2, y)).Fn,
664 ).
665 Rigid(EmptySpace(x/4, y)).
666 Fn,
667 ).Background("Transparent").CornerRadius(0).Fn(gtx)
668 }
669 }
670
671 // grabber renders the grabber
672 func (li *List) grabber(dims DimensionList, x, y, viewAxis, viewCross int) func(l.Context) l.Dimensions {
673 return func(gtx l.Context) l.Dimensions {
674 ax := gesture.Vertical
675 if li.axis == l.Horizontal {
676 ax = gesture.Horizontal
677 }
678 var de *pointer.Event
679 for _, ev := range li.drag.Events(gtx.Metric, gtx, ax) {
680 if ev.Type == pointer.Press ||
681 ev.Type == pointer.Release ||
682 ev.Type == pointer.Drag {
683 de = &ev
684 }
685 }
686 if de != nil {
687 if de.Type == pointer.Press { // || de.Type == pointer.Drag {
688 }
689 if de.Type == pointer.Release {
690 }
691 if de.Type == pointer.Drag {
692 // D.Ln("drag position", de.Position)
693 if time.Now().Sub(li.recentPageClick) > time.Second/2 {
694 total := dims.GetTotal(li.axis)
695 var d int
696 if li.axis == l.Horizontal {
697 deltaX := int(de.Position.X)
698 if deltaX > 8 || deltaX < -8 {
699 d = deltaX * (total / viewAxis)
700 li.SetPosition(dims.CoordinateToPosition(d, li.axis))
701 }
702 } else {
703 deltaY := int(de.Position.Y)
704 if deltaY > 8 || deltaY < -8 {
705 d = deltaY * (total / viewAxis)
706 li.SetPosition(dims.CoordinateToPosition(d, li.axis))
707 }
708 }
709 }
710 li.Window.Invalidate()
711 }
712 }
713 defer op.Save(gtx.Ops).Load()
714 bounds := image.Rectangle{Max: image.Point{X: x, Y: y}}
715 pointer.Rect(bounds).Add(gtx.Ops)
716 li.sideScroll.Add(gtx.Ops, bounds)
717 return li.Flex().
718 Rigid(
719 li.Fill(li.currentColor, l.Center, 0, 0, EmptySpace(x, y)).
720 Fn,
721 ).
722 Fn(gtx)
723 }
724 }
725