gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/layout/list.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package layout
     4  
     5  import (
     6  	"image"
     7  
     8  	"gioui.org/ui"
     9  	"gioui.org/ui/gesture"
    10  	"gioui.org/ui/paint"
    11  	"gioui.org/ui/pointer"
    12  )
    13  
    14  type scrollChild struct {
    15  	size  image.Point
    16  	macro ui.MacroOp
    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 reahed. A List with ScrollToEnd enabled also align its content to
    26  	// the end.
    27  	ScrollToEnd bool
    28  	// Alignment is the cross axis alignment of list elements.
    29  	Alignment Alignment
    30  
    31  	// beforeEnd tracks whether the List position is before
    32  	// the very end.
    33  	beforeEnd bool
    34  
    35  	ctx         *Context
    36  	macro       ui.MacroOp
    37  	child       ui.MacroOp
    38  	scroll      gesture.Scroll
    39  	scrollDelta int
    40  
    41  	// first is the index of the first visible child.
    42  	first int
    43  	// offset is the signed distance from the top edge
    44  	// to the child with index first.
    45  	offset int
    46  
    47  	len int
    48  
    49  	// maxSize is the total size of visible children.
    50  	maxSize  int
    51  	children []scrollChild
    52  	dir      iterationDir
    53  }
    54  
    55  // ListElement is a function that computes the dimensions of
    56  // a list element.
    57  type ListElement func(index int)
    58  
    59  type iterationDir uint8
    60  
    61  const (
    62  	iterateNone iterationDir = iota
    63  	iterateForward
    64  	iterateBackward
    65  )
    66  
    67  const inf = 1e6
    68  
    69  // init prepares the list for iterating through its children with next.
    70  func (l *List) init(gtx *Context, len int) {
    71  	if l.more() {
    72  		panic("unfinished child")
    73  	}
    74  	l.ctx = gtx
    75  	l.maxSize = 0
    76  	l.children = l.children[:0]
    77  	l.len = len
    78  	l.update()
    79  	if l.scrollToEnd() {
    80  		l.offset = 0
    81  		l.first = len
    82  	}
    83  	if l.first > len {
    84  		l.offset = 0
    85  		l.first = len
    86  	}
    87  	l.macro.Record(gtx.Ops)
    88  	l.next()
    89  }
    90  
    91  // Layout the List and return its dimensions.
    92  func (l *List) Layout(gtx *Context, len int, w ListElement) {
    93  	for l.init(gtx, len); l.more(); l.next() {
    94  		cs := axisConstraints(l.Axis, Constraint{Max: inf}, axisCrossConstraint(l.Axis, l.ctx.Constraints))
    95  		i := l.index()
    96  		l.end(gtx.Layout(cs, func() {
    97  			w(i)
    98  		}))
    99  	}
   100  	gtx.Dimensions = l.layout()
   101  }
   102  
   103  func (l *List) scrollToEnd() bool {
   104  	return l.ScrollToEnd && !l.beforeEnd
   105  }
   106  
   107  // Dragging reports whether the List is being dragged.
   108  func (l *List) Dragging() bool {
   109  	return l.scroll.State() == gesture.StateDragging
   110  }
   111  
   112  func (l *List) update() {
   113  	d := l.scroll.Scroll(l.ctx.Config, l.ctx.Queue, gesture.Axis(l.Axis))
   114  	l.scrollDelta = d
   115  	l.offset += d
   116  }
   117  
   118  // next advances to the next child.
   119  func (l *List) next() {
   120  	l.dir = l.nextDir()
   121  	// The user scroll offset is applied after scrolling to
   122  	// list end.
   123  	if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 {
   124  		l.beforeEnd = true
   125  		l.offset += l.scrollDelta
   126  		l.dir = l.nextDir()
   127  	}
   128  	if l.more() {
   129  		l.child.Record(l.ctx.Ops)
   130  	}
   131  }
   132  
   133  // index is current child's position in the underlying list.
   134  func (l *List) index() int {
   135  	switch l.dir {
   136  	case iterateBackward:
   137  		return l.first - 1
   138  	case iterateForward:
   139  		return l.first + len(l.children)
   140  	default:
   141  		panic("Index called before Next")
   142  	}
   143  }
   144  
   145  // more reports whether more children are needed.
   146  func (l *List) more() bool {
   147  	return l.dir != iterateNone
   148  }
   149  
   150  func (l *List) nextDir() iterationDir {
   151  	vsize := axisMainConstraint(l.Axis, l.ctx.Constraints).Max
   152  	last := l.first + len(l.children)
   153  	// Clamp offset.
   154  	if l.maxSize-l.offset < vsize && last == l.len {
   155  		l.offset = l.maxSize - vsize
   156  	}
   157  	if l.offset < 0 && l.first == 0 {
   158  		l.offset = 0
   159  	}
   160  	switch {
   161  	case len(l.children) == l.len:
   162  		return iterateNone
   163  	case l.maxSize-l.offset < vsize:
   164  		return iterateForward
   165  	case l.offset < 0:
   166  		return iterateBackward
   167  	}
   168  	return iterateNone
   169  }
   170  
   171  // End the current child by specifying its dimensions.
   172  func (l *List) end(dims Dimensions) {
   173  	l.child.Stop()
   174  	child := scrollChild{dims.Size, l.child}
   175  	mainSize := axisMain(l.Axis, child.size)
   176  	l.maxSize += mainSize
   177  	switch l.dir {
   178  	case iterateForward:
   179  		l.children = append(l.children, child)
   180  	case iterateBackward:
   181  		l.children = append([]scrollChild{child}, l.children...)
   182  		l.first--
   183  		l.offset += mainSize
   184  	default:
   185  		panic("call Next before End")
   186  	}
   187  	l.dir = iterateNone
   188  }
   189  
   190  // Layout the List and return its dimensions.
   191  func (l *List) layout() Dimensions {
   192  	if l.more() {
   193  		panic("unfinished child")
   194  	}
   195  	mainc := axisMainConstraint(l.Axis, l.ctx.Constraints)
   196  	children := l.children
   197  	// Skip invisible children
   198  	for len(children) > 0 {
   199  		sz := children[0].size
   200  		mainSize := axisMain(l.Axis, sz)
   201  		if l.offset <= mainSize {
   202  			break
   203  		}
   204  		l.first++
   205  		l.offset -= mainSize
   206  		children = children[1:]
   207  	}
   208  	size := -l.offset
   209  	var maxCross int
   210  	for i, child := range children {
   211  		sz := child.size
   212  		if c := axisCross(l.Axis, sz); c > maxCross {
   213  			maxCross = c
   214  		}
   215  		size += axisMain(l.Axis, sz)
   216  		if size >= mainc.Max {
   217  			children = children[:i+1]
   218  			break
   219  		}
   220  	}
   221  	ops := l.ctx.Ops
   222  	pos := -l.offset
   223  	// ScrollToEnd lists lists are end aligned.
   224  	if space := mainc.Max - size; l.ScrollToEnd && space > 0 {
   225  		pos += space
   226  	}
   227  	for _, child := range children {
   228  		sz := child.size
   229  		var cross int
   230  		switch l.Alignment {
   231  		case End:
   232  			cross = maxCross - axisCross(l.Axis, sz)
   233  		case Middle:
   234  			cross = (maxCross - axisCross(l.Axis, sz)) / 2
   235  		}
   236  		childSize := axisMain(l.Axis, sz)
   237  		max := childSize + pos
   238  		if max > mainc.Max {
   239  			max = mainc.Max
   240  		}
   241  		min := pos
   242  		if min < 0 {
   243  			min = 0
   244  		}
   245  		r := image.Rectangle{
   246  			Min: axisPoint(l.Axis, min, -inf),
   247  			Max: axisPoint(l.Axis, max, inf),
   248  		}
   249  		var stack ui.StackOp
   250  		stack.Push(ops)
   251  		paint.RectClip(r).Add(ops)
   252  		ui.TransformOp{}.Offset(toPointF(axisPoint(l.Axis, pos, cross))).Add(ops)
   253  		child.macro.Add(ops)
   254  		stack.Pop()
   255  		pos += childSize
   256  	}
   257  	atStart := l.first == 0 && l.offset <= 0
   258  	atEnd := l.first+len(children) == l.len && mainc.Max >= pos
   259  	if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
   260  		l.scroll.Stop()
   261  	}
   262  	l.beforeEnd = !atEnd
   263  	dims := axisPoint(l.Axis, mainc.Constrain(pos), maxCross)
   264  	l.macro.Stop()
   265  	pointer.RectAreaOp{Rect: image.Rectangle{Max: dims}}.Add(ops)
   266  	l.scroll.Add(ops)
   267  	l.macro.Add(ops)
   268  	return Dimensions{Size: dims}
   269  }