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