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