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