list.go raw

   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