layout.mx raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  // Layout system: constraints, dimensions, flex, stack, list, inset, direction.
   4  // Ported from gioui.org/layout and gioui.org/op (public wrappers).
   5  
   6  package gio
   7  
   8  import (
   9  	"image"
  10  	"math"
  11  	"time"
  12  )
  13  
  14  // OpOffset converts an integer offset to a TransformOp.
  15  func OpOffset(off image.Point) TransformOp {
  16  	return AffineTransform(AffineId().Offset(Pt(float32(off.X), float32(off.Y))))
  17  }
  18  
  19  // InputSource is a stub for input.Source (Phase 3).
  20  type InputSource struct{}
  21  
  22  func (s InputSource) Disabled() InputSource {
  23  	return InputSource{}
  24  }
  25  
  26  // --- Layout types (from layout/layout.go) ---
  27  
  28  // Constraints represent the minimum and maximum size of a widget.
  29  type Constraints struct {
  30  	Min, Max image.Point
  31  }
  32  
  33  // Dimensions are the resolved size and baseline for a widget.
  34  type Dimensions struct {
  35  	Size     image.Point
  36  	Baseline int
  37  }
  38  
  39  // Axis is the layout axis (horizontal or vertical).
  40  type Axis uint8
  41  
  42  // Alignment is the mutual alignment of a list of widgets.
  43  type Alignment uint8
  44  
  45  // Direction is the alignment of widgets relative to a containing space.
  46  type Direction uint8
  47  
  48  // Widget is a function scope for drawing, processing events and
  49  // computing dimensions for a user interface element.
  50  type Widget func(gtx LayoutContext) Dimensions
  51  
  52  const (
  53  	Start Alignment = iota
  54  	End
  55  	Middle
  56  	Baseline
  57  )
  58  
  59  const (
  60  	NW Direction = iota
  61  	N
  62  	NE
  63  	E
  64  	SE
  65  	S
  66  	SW
  67  	W
  68  	Center
  69  )
  70  
  71  const (
  72  	AxisHorizontal Axis = iota
  73  	AxisVertical
  74  )
  75  
  76  // Exact returns Constraints with min and max set to size.
  77  func Exact(size image.Point) Constraints {
  78  	return Constraints{
  79  		Min: size, Max: size,
  80  	}
  81  }
  82  
  83  // LayoutFPt converts an image.Point to a Point (f32).
  84  // Note: FPt already exists in f32.mx, this is the layout-specific alias.
  85  func LayoutFPt(p image.Point) Point {
  86  	return Point{
  87  		X: float32(p.X), Y: float32(p.Y),
  88  	}
  89  }
  90  
  91  // Constrain a size so each dimension is in the range [min;max].
  92  func (c Constraints) Constrain(size image.Point) image.Point {
  93  	if min := c.Min.X; size.X < min {
  94  		size.X = min
  95  	}
  96  	if min := c.Min.Y; size.Y < min {
  97  		size.Y = min
  98  	}
  99  	if max := c.Max.X; size.X > max {
 100  		size.X = max
 101  	}
 102  	if max := c.Max.Y; size.Y > max {
 103  		size.Y = max
 104  	}
 105  	return size
 106  }
 107  
 108  // AddMin returns a copy of Constraints with the Min enlarged by up to delta
 109  // while still fitting within Max.
 110  func (c Constraints) AddMin(delta image.Point) Constraints {
 111  	c.Min = c.Min.Add(delta)
 112  	if c.Min.X < 0 {
 113  		c.Min.X = 0
 114  	}
 115  	if c.Min.Y < 0 {
 116  		c.Min.Y = 0
 117  	}
 118  	c.Min = c.Constrain(c.Min)
 119  	return c
 120  }
 121  
 122  // SubMax returns a copy of Constraints with the Max shrunk by up to delta.
 123  func (c Constraints) SubMax(delta image.Point) Constraints {
 124  	c.Max = c.Max.Sub(delta)
 125  	if c.Max.X < 0 {
 126  		c.Max.X = 0
 127  	}
 128  	if c.Max.Y < 0 {
 129  		c.Max.Y = 0
 130  	}
 131  	c.Min = c.Constrain(c.Min)
 132  	return c
 133  }
 134  
 135  // --- Context (from layout/context.go) ---
 136  
 137  // LayoutContext carries the state needed by almost all layouts and widgets.
 138  type LayoutContext struct {
 139  	Constraints Constraints
 140  	Metric      Metric
 141  	Now         time.Time
 142  	Locale      Locale
 143  	Values      map[string]any
 144  	Source      InputSource
 145  	Ops         *OpList
 146  }
 147  
 148  // Dp converts v to pixels.
 149  func (c LayoutContext) Dp(v Dp) int {
 150  	return c.Metric.Dp(v)
 151  }
 152  
 153  // Sp converts v to pixels.
 154  func (c LayoutContext) Sp(v Sp) int {
 155  	return c.Metric.Sp(v)
 156  }
 157  
 158  // Disabled returns a copy of this context with no event delivery.
 159  func (c LayoutContext) Disabled() LayoutContext {
 160  	c.Source = c.Source.Disabled()
 161  	return c
 162  }
 163  
 164  // --- Inset (from layout/layout.go) ---
 165  
 166  // Inset adds space around a widget by decreasing its maximum constraints.
 167  type Inset struct {
 168  	Top, Bottom, Left, Right Dp
 169  }
 170  
 171  // Layout a widget with inset.
 172  func (in Inset) Layout(gtx LayoutContext, w Widget) Dimensions {
 173  	top := gtx.Dp(in.Top)
 174  	right := gtx.Dp(in.Right)
 175  	bottom := gtx.Dp(in.Bottom)
 176  	left := gtx.Dp(in.Left)
 177  	mcs := gtx.Constraints
 178  	mcs.Max.X -= left + right
 179  	if mcs.Max.X < 0 {
 180  		left = 0
 181  		right = 0
 182  		mcs.Max.X = 0
 183  	}
 184  	if mcs.Min.X > mcs.Max.X {
 185  		mcs.Min.X = mcs.Max.X
 186  	}
 187  	mcs.Max.Y -= top + bottom
 188  	if mcs.Max.Y < 0 {
 189  		bottom = 0
 190  		top = 0
 191  		mcs.Max.Y = 0
 192  	}
 193  	if mcs.Min.Y > mcs.Max.Y {
 194  		mcs.Min.Y = mcs.Max.Y
 195  	}
 196  	gtx.Constraints = mcs
 197  	trans := OpOffset(image.Pt(left, top)).Push(gtx.Ops)
 198  	dims := w(gtx)
 199  	trans.Pop()
 200  	return Dimensions{
 201  		Size:     dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
 202  		Baseline: dims.Baseline + bottom,
 203  	}
 204  }
 205  
 206  // UniformInset returns an Inset with a single value applied to all edges.
 207  func UniformInset(v Dp) Inset {
 208  	return Inset{Top: v, Right: v, Bottom: v, Left: v}
 209  }
 210  
 211  // --- Direction layout (from layout/layout.go) ---
 212  
 213  // Layout a widget according to the direction.
 214  func (d Direction) Layout(gtx LayoutContext, w Widget) Dimensions {
 215  	macro := Record(gtx.Ops)
 216  	csn := gtx.Constraints.Min
 217  	switch d {
 218  	case N, S:
 219  		gtx.Constraints.Min.Y = 0
 220  	case E, W:
 221  		gtx.Constraints.Min.X = 0
 222  	default:
 223  		gtx.Constraints.Min = image.Point{}
 224  	}
 225  	dims := w(gtx)
 226  	call := macro.Stop()
 227  	sz := dims.Size
 228  	if sz.X < csn.X {
 229  		sz.X = csn.X
 230  	}
 231  	if sz.Y < csn.Y {
 232  		sz.Y = csn.Y
 233  	}
 234  
 235  	p := d.Position(dims.Size, sz)
 236  	defer OpOffset(p).Push(gtx.Ops).Pop()
 237  	call.Add(gtx.Ops)
 238  
 239  	return Dimensions{
 240  		Size:     sz,
 241  		Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
 242  	}
 243  }
 244  
 245  // Position calculates widget position according to the direction.
 246  func (d Direction) Position(widget, bounds image.Point) image.Point {
 247  	var p image.Point
 248  
 249  	switch d {
 250  	case N, S, Center:
 251  		p.X = (bounds.X - widget.X) / 2
 252  	case NE, SE, E:
 253  		p.X = bounds.X - widget.X
 254  	}
 255  
 256  	switch d {
 257  	case W, Center, E:
 258  		p.Y = (bounds.Y - widget.Y) / 2
 259  	case SW, S, SE:
 260  		p.Y = bounds.Y - widget.Y
 261  	}
 262  
 263  	return p
 264  }
 265  
 266  // --- Spacer (from layout/layout.go) ---
 267  
 268  // Spacer adds space between widgets.
 269  type Spacer struct {
 270  	Width, Height Dp
 271  }
 272  
 273  func (s Spacer) Layout(gtx LayoutContext) Dimensions {
 274  	return Dimensions{
 275  		Size: gtx.Constraints.Constrain(image.Point{
 276  			X: gtx.Dp(s.Width),
 277  			Y: gtx.Dp(s.Height),
 278  		}),
 279  	}
 280  }
 281  
 282  // --- Axis utilities (from layout/layout.go) ---
 283  
 284  // Convert a point in (x, y) coordinates to (main, cross) coordinates,
 285  // or vice versa.
 286  func (a Axis) Convert(pt image.Point) image.Point {
 287  	if a == AxisHorizontal {
 288  		return pt
 289  	}
 290  	return image.Pt(pt.Y, pt.X)
 291  }
 292  
 293  // FConvert a point in (x, y) coordinates to (main, cross) coordinates,
 294  // or vice versa, for f32 points.
 295  func (a Axis) FConvert(pt Point) Point {
 296  	if a == AxisHorizontal {
 297  		return pt
 298  	}
 299  	return Pt(pt.Y, pt.X)
 300  }
 301  
 302  func (a Axis) mainConstraint(cs Constraints) (int, int) {
 303  	if a == AxisHorizontal {
 304  		return cs.Min.X, cs.Max.X
 305  	}
 306  	return cs.Min.Y, cs.Max.Y
 307  }
 308  
 309  func (a Axis) crossConstraint(cs Constraints) (int, int) {
 310  	if a == AxisHorizontal {
 311  		return cs.Min.Y, cs.Max.Y
 312  	}
 313  	return cs.Min.X, cs.Max.X
 314  }
 315  
 316  func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
 317  	if a == AxisHorizontal {
 318  		return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
 319  	}
 320  	return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
 321  }
 322  
 323  func (a Axis) String() string {
 324  	switch a {
 325  	case AxisHorizontal:
 326  		return "Horizontal"
 327  	case AxisVertical:
 328  		return "Vertical"
 329  	default:
 330  		panic("unreachable")
 331  	}
 332  }
 333  
 334  func (al Alignment) String() string {
 335  	switch al {
 336  	case Start:
 337  		return "Start"
 338  	case End:
 339  		return "End"
 340  	case Middle:
 341  		return "Middle"
 342  	case Baseline:
 343  		return "Baseline"
 344  	default:
 345  		panic("unreachable")
 346  	}
 347  }
 348  
 349  func (d Direction) String() string {
 350  	switch d {
 351  	case NW:
 352  		return "NW"
 353  	case N:
 354  		return "N"
 355  	case NE:
 356  		return "NE"
 357  	case E:
 358  		return "E"
 359  	case SE:
 360  		return "SE"
 361  	case S:
 362  		return "S"
 363  	case SW:
 364  		return "SW"
 365  	case W:
 366  		return "W"
 367  	case Center:
 368  		return "Center"
 369  	default:
 370  		panic("unreachable")
 371  	}
 372  }
 373  
 374  // --- Flex (from layout/flex.go) ---
 375  
 376  // Flex lays out child elements along an axis,
 377  // according to alignment and weights.
 378  type Flex struct {
 379  	Axis      Axis
 380  	Spacing   Spacing
 381  	Alignment Alignment
 382  	WeightSum float32
 383  	Gap       int
 384  }
 385  
 386  // FlexChild is the descriptor for a Flex child.
 387  type FlexChild struct {
 388  	flex   bool
 389  	weight float32
 390  	widget Widget
 391  }
 392  
 393  // Spacing determines the spacing mode for a Flex.
 394  type Spacing uint8
 395  
 396  const (
 397  	SpaceEnd Spacing = iota
 398  	SpaceStart
 399  	SpaceSides
 400  	SpaceAround
 401  	SpaceBetween
 402  	SpaceEvenly
 403  )
 404  
 405  // Rigid returns a Flex child with a maximal constraint of the
 406  // remaining space.
 407  func Rigid(widget Widget) FlexChild {
 408  	return FlexChild{
 409  		widget: widget,
 410  	}
 411  }
 412  
 413  // Flexed returns a Flex child forced to take up weight fraction of the
 414  // space left over from Rigid children.
 415  func Flexed(weight float32, widget Widget) FlexChild {
 416  	return FlexChild{
 417  		flex:   true,
 418  		weight: weight,
 419  		widget: widget,
 420  	}
 421  }
 422  
 423  // Layout a list of children. Rigid children are laid out before Flexed children.
 424  func (f Flex) Layout(gtx LayoutContext, children ...FlexChild) Dimensions {
 425  	size := 0
 426  	cs := gtx.Constraints
 427  	mainMin, mainMax := f.Axis.mainConstraint(cs)
 428  	crossMin, crossMax := f.Axis.crossConstraint(cs)
 429  	remaining := mainMax
 430  	if len(children) > 1 && f.Gap > 0 {
 431  		totalGap := f.Gap * (len(children) - 1)
 432  		remaining -= totalGap
 433  		if remaining < 0 {
 434  			remaining = 0
 435  		}
 436  	}
 437  	var totalWeight float32
 438  	cgtx := gtx
 439  	type scratchSpace struct {
 440  		call CallOp
 441  		dims Dimensions
 442  	}
 443  	var scratchArray [32]scratchSpace
 444  	scratch := scratchArray[:0]
 445  	scratch = append(scratch, []scratchSpace{:len(children)}...)
 446  	// Lay out Rigid children.
 447  	for i, child := range children {
 448  		if child.flex {
 449  			totalWeight += child.weight
 450  			continue
 451  		}
 452  		macro := Record(gtx.Ops)
 453  		cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax)
 454  		dims := child.widget(cgtx)
 455  		c := macro.Stop()
 456  		sz := f.Axis.Convert(dims.Size).X
 457  		size += sz
 458  		remaining -= sz
 459  		if remaining < 0 {
 460  			remaining = 0
 461  		}
 462  		scratch[i].call = c
 463  		scratch[i].dims = dims
 464  	}
 465  	if w := f.WeightSum; w != 0 {
 466  		totalWeight = w
 467  	}
 468  	var fraction float32
 469  	flexTotal := remaining
 470  	// Lay out Flexed children.
 471  	for i, child := range children {
 472  		if !child.flex {
 473  			continue
 474  		}
 475  		var flexSize int
 476  		if remaining > 0 && totalWeight > 0 {
 477  			childSize := float32(flexTotal) * child.weight / totalWeight
 478  			flexSize = int(childSize + fraction + .5)
 479  			fraction = childSize - float32(flexSize)
 480  			if flexSize > remaining {
 481  				flexSize = remaining
 482  			}
 483  		}
 484  		macro := Record(gtx.Ops)
 485  		cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, crossMax)
 486  		dims := child.widget(cgtx)
 487  		c := macro.Stop()
 488  		sz := f.Axis.Convert(dims.Size).X
 489  		size += sz
 490  		remaining -= sz
 491  		if remaining < 0 {
 492  			remaining = 0
 493  		}
 494  		scratch[i].call = c
 495  		scratch[i].dims = dims
 496  	}
 497  	maxCross := crossMin
 498  	var maxBaseline int
 499  	for _, scratchChild := range scratch {
 500  		if c := f.Axis.Convert(scratchChild.dims.Size).Y; c > maxCross {
 501  			maxCross = c
 502  		}
 503  		if b := scratchChild.dims.Size.Y - scratchChild.dims.Baseline; b > maxBaseline {
 504  			maxBaseline = b
 505  		}
 506  	}
 507  	if len(children) > 1 && f.Gap > 0 {
 508  		size += f.Gap * (len(children) - 1)
 509  	}
 510  	var space int
 511  	if mainMin > size {
 512  		space = mainMin - size
 513  	}
 514  	var mainSize int
 515  	switch f.Spacing {
 516  	case SpaceSides:
 517  		mainSize += space / 2
 518  	case SpaceStart:
 519  		mainSize += space
 520  	case SpaceEvenly:
 521  		mainSize += space / (1 + len(children))
 522  	case SpaceAround:
 523  		if len(children) > 0 {
 524  			mainSize += space / (len(children) * 2)
 525  		}
 526  	}
 527  	for i, scratchChild := range scratch {
 528  		dims := scratchChild.dims
 529  		b := dims.Size.Y - dims.Baseline
 530  		var cross int
 531  		switch f.Alignment {
 532  		case End:
 533  			cross = maxCross - f.Axis.Convert(dims.Size).Y
 534  		case Middle:
 535  			cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2
 536  		case Baseline:
 537  			if f.Axis == AxisHorizontal {
 538  				cross = maxBaseline - b
 539  			}
 540  		}
 541  		pt := f.Axis.Convert(image.Pt(mainSize, cross))
 542  		trans := OpOffset(pt).Push(gtx.Ops)
 543  		scratchChild.call.Add(gtx.Ops)
 544  		trans.Pop()
 545  		mainSize += f.Axis.Convert(dims.Size).X
 546  		if i < len(children)-1 {
 547  			mainSize += f.Gap
 548  			switch f.Spacing {
 549  			case SpaceEvenly:
 550  				mainSize += space / (1 + len(children))
 551  			case SpaceAround:
 552  				if len(children) > 0 {
 553  					mainSize += space / len(children)
 554  				}
 555  			case SpaceBetween:
 556  				if len(children) > 1 {
 557  					mainSize += space / (len(children) - 1)
 558  				}
 559  			}
 560  		}
 561  	}
 562  	switch f.Spacing {
 563  	case SpaceSides:
 564  		mainSize += space / 2
 565  	case SpaceEnd:
 566  		mainSize += space
 567  	case SpaceEvenly:
 568  		mainSize += space / (1 + len(children))
 569  	case SpaceAround:
 570  		if len(children) > 0 {
 571  			mainSize += space / (len(children) * 2)
 572  		}
 573  	}
 574  	sz := f.Axis.Convert(image.Pt(mainSize, maxCross))
 575  	sz = cs.Constrain(sz)
 576  	return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
 577  }
 578  
 579  func (s Spacing) String() string {
 580  	switch s {
 581  	case SpaceEnd:
 582  		return "SpaceEnd"
 583  	case SpaceStart:
 584  		return "SpaceStart"
 585  	case SpaceSides:
 586  		return "SpaceSides"
 587  	case SpaceAround:
 588  		return "SpaceAround"
 589  	case SpaceBetween:
 590  		return "SpaceBetween"
 591  	case SpaceEvenly:
 592  		return "SpaceEvenly"
 593  	default:
 594  		panic("unreachable")
 595  	}
 596  }
 597  
 598  // --- Stack (from layout/stack.go) ---
 599  
 600  // LayoutStack lays out child elements on top of each other,
 601  // according to an alignment direction.
 602  type LayoutStack struct {
 603  	Alignment Direction
 604  }
 605  
 606  // StackChild represents a child for a LayoutStack layout.
 607  type StackChild struct {
 608  	expanded bool
 609  	widget   Widget
 610  }
 611  
 612  // Stacked returns a StackChild laid out with no minimum constraints.
 613  func Stacked(w Widget) StackChild {
 614  	return StackChild{
 615  		widget: w,
 616  	}
 617  }
 618  
 619  // Expanded returns a StackChild with min constraints set
 620  // to the largest Stacked child.
 621  func Expanded(w Widget) StackChild {
 622  	return StackChild{
 623  		expanded: true,
 624  		widget:   w,
 625  	}
 626  }
 627  
 628  // Layout a stack of children. Stacked children are laid out before Expanded children.
 629  func (s LayoutStack) Layout(gtx LayoutContext, children ...StackChild) Dimensions {
 630  	var maxSZ image.Point
 631  	cgtx := gtx
 632  	cgtx.Constraints.Min = image.Point{}
 633  	type scratchSpace struct {
 634  		call CallOp
 635  		dims Dimensions
 636  	}
 637  	var scratchArray [32]scratchSpace
 638  	scratch := scratchArray[:0]
 639  	scratch = append(scratch, []scratchSpace{:len(children)}...)
 640  	for i, w := range children {
 641  		if w.expanded {
 642  			continue
 643  		}
 644  		macro := Record(gtx.Ops)
 645  		dims := w.widget(cgtx)
 646  		call := macro.Stop()
 647  		if w := dims.Size.X; w > maxSZ.X {
 648  			maxSZ.X = w
 649  		}
 650  		if h := dims.Size.Y; h > maxSZ.Y {
 651  			maxSZ.Y = h
 652  		}
 653  		scratch[i].call = call
 654  		scratch[i].dims = dims
 655  	}
 656  	for i, w := range children {
 657  		if !w.expanded {
 658  			continue
 659  		}
 660  		macro := Record(gtx.Ops)
 661  		cgtx.Constraints.Min = maxSZ
 662  		dims := w.widget(cgtx)
 663  		call := macro.Stop()
 664  		if w := dims.Size.X; w > maxSZ.X {
 665  			maxSZ.X = w
 666  		}
 667  		if h := dims.Size.Y; h > maxSZ.Y {
 668  			maxSZ.Y = h
 669  		}
 670  		scratch[i].call = call
 671  		scratch[i].dims = dims
 672  	}
 673  
 674  	maxSZ = gtx.Constraints.Constrain(maxSZ)
 675  	var baseline int
 676  	for _, scratchChild := range scratch {
 677  		sz := scratchChild.dims.Size
 678  		var p image.Point
 679  		switch s.Alignment {
 680  		case N, S, Center:
 681  			p.X = (maxSZ.X - sz.X) / 2
 682  		case NE, SE, E:
 683  			p.X = maxSZ.X - sz.X
 684  		}
 685  		switch s.Alignment {
 686  		case W, Center, E:
 687  			p.Y = (maxSZ.Y - sz.Y) / 2
 688  		case SW, S, SE:
 689  			p.Y = maxSZ.Y - sz.Y
 690  		}
 691  		trans := OpOffset(p).Push(gtx.Ops)
 692  		scratchChild.call.Add(gtx.Ops)
 693  		trans.Pop()
 694  		if baseline == 0 {
 695  			if b := scratchChild.dims.Baseline; b != 0 {
 696  				baseline = b + maxSZ.Y - sz.Y - p.Y
 697  			}
 698  		}
 699  	}
 700  	return Dimensions{
 701  		Size:     maxSZ,
 702  		Baseline: baseline,
 703  	}
 704  }
 705  
 706  // Background lays out a single child widget on top of a background.
 707  type Background struct{}
 708  
 709  // Layout a widget and then add a background to it.
 710  func (Background) Layout(gtx LayoutContext, background, widget Widget) Dimensions {
 711  	macro := Record(gtx.Ops)
 712  	wdims := widget(gtx)
 713  	baseline := wdims.Baseline
 714  	call := macro.Stop()
 715  
 716  	cgtx := gtx
 717  	cgtx.Constraints.Min = gtx.Constraints.Constrain(wdims.Size)
 718  	bdims := background(cgtx)
 719  
 720  	if bdims.Size != wdims.Size {
 721  		p := image.Point{
 722  			X: (bdims.Size.X - wdims.Size.X) / 2,
 723  			Y: (bdims.Size.Y - wdims.Size.Y) / 2,
 724  		}
 725  		baseline += (bdims.Size.Y - wdims.Size.Y) / 2
 726  		trans := OpOffset(p).Push(gtx.Ops)
 727  		defer trans.Pop()
 728  	}
 729  
 730  	call.Add(gtx.Ops)
 731  
 732  	return Dimensions{
 733  		Size:     bdims.Size,
 734  		Baseline: baseline,
 735  	}
 736  }
 737  
 738  // --- List (from layout/list.go) ---
 739  
 740  type scrollChild struct {
 741  	size image.Point
 742  	call CallOp
 743  }
 744  
 745  // LayoutList displays a subsection of a potentially infinitely
 746  // large underlying list. Accepts user input to scroll the subsection.
 747  type LayoutList struct {
 748  	Axis          Axis
 749  	ScrollToEnd   bool
 750  	Alignment     Alignment
 751  	ScrollAnyAxis bool
 752  	Gap           int
 753  
 754  	cs          Constraints
 755  	scroll      GestureScroll
 756  	scrollDelta int
 757  
 758  	Position Position
 759  
 760  	len int
 761  
 762  	maxSize  int
 763  	children []scrollChild
 764  	dir      iterationDir
 765  }
 766  
 767  // ListElement computes the dimensions of a list element.
 768  type ListElement func(gtx LayoutContext, index int) Dimensions
 769  
 770  type iterationDir uint8
 771  
 772  // Position is a List scroll offset.
 773  type Position struct {
 774  	BeforeEnd  bool
 775  	First      int
 776  	Offset     int
 777  	OffsetLast int
 778  	Count      int
 779  	Length     int
 780  }
 781  
 782  const (
 783  	iterateNone iterationDir = iota
 784  	iterateForward
 785  	iterateBackward
 786  )
 787  
 788  const listInf = 1e6
 789  
 790  func (l *LayoutList) init(gtx LayoutContext, length int) {
 791  	if l.more() {
 792  		panic("unfinished child")
 793  	}
 794  	l.cs = gtx.Constraints
 795  	l.maxSize = 0
 796  	l.children = l.children[:0]
 797  	l.len = length
 798  	l.update(gtx)
 799  	if l.Position.First < 0 {
 800  		l.Position.Offset = 0
 801  		l.Position.First = 0
 802  	}
 803  	if l.scrollToEnd() || l.Position.First > length {
 804  		l.Position.Offset = 0
 805  		l.Position.First = length
 806  	}
 807  }
 808  
 809  // Layout a List of length items, where each item is implicitly defined
 810  // by the callback w.
 811  func (l *LayoutList) Layout(gtx LayoutContext, length int, w ListElement) Dimensions {
 812  	l.init(gtx, length)
 813  	crossMin, crossMax := l.Axis.crossConstraint(gtx.Constraints)
 814  	gtx.Constraints = l.Axis.constraints(0, int(listInf), crossMin, crossMax)
 815  	macro := Record(gtx.Ops)
 816  	laidOutTotalLength := 0
 817  	numLaidOut := 0
 818  
 819  	for l.next(); l.more(); l.next() {
 820  		child := Record(gtx.Ops)
 821  		dims := w(gtx, l.index())
 822  		call := child.Stop()
 823  		l.end(dims, call)
 824  		laidOutTotalLength += l.Axis.Convert(dims.Size).X
 825  		numLaidOut++
 826  	}
 827  
 828  	if numLaidOut > 0 {
 829  		l.Position.Length = laidOutTotalLength*length/numLaidOut + l.Gap*(length-1)
 830  	} else {
 831  		l.Position.Length = 0
 832  	}
 833  	return l.layout(gtx.Ops, macro)
 834  }
 835  
 836  func (l *LayoutList) scrollToEnd() bool {
 837  	return l.ScrollToEnd && !l.Position.BeforeEnd
 838  }
 839  
 840  // Dragging reports whether the List is being dragged.
 841  func (l *LayoutList) Dragging() bool {
 842  	return l.scroll.ScrollState() == GestureScrollDragging
 843  }
 844  
 845  func (l *LayoutList) update(gtx LayoutContext) {
 846  	axis := GestureAxisHorizontal
 847  	if l.Axis == AxisVertical {
 848  		axis = GestureAxisVertical
 849  	}
 850  	if l.ScrollAnyAxis {
 851  		axis = GestureAxisBoth
 852  	}
 853  	d := l.scroll.Update(Source{}, gtx.Metric, gtx.Now, axis)
 854  
 855  	l.scrollDelta = d
 856  	l.Position.Offset += d
 857  }
 858  
 859  func (l *LayoutList) next() {
 860  	l.dir = l.nextDir()
 861  	if l.scrollToEnd() && !l.more() && l.scrollDelta < 0 {
 862  		l.Position.BeforeEnd = true
 863  		l.Position.Offset += l.scrollDelta
 864  		l.dir = l.nextDir()
 865  	}
 866  }
 867  
 868  func (l *LayoutList) index() int {
 869  	switch l.dir {
 870  	case iterateBackward:
 871  		return l.Position.First - 1
 872  	case iterateForward:
 873  		return l.Position.First + len(l.children)
 874  	default:
 875  		panic("Index called before Next")
 876  	}
 877  }
 878  
 879  func (l *LayoutList) more() bool {
 880  	return l.dir != iterateNone
 881  }
 882  
 883  func (l *LayoutList) nextDir() iterationDir {
 884  	_, vsize := l.Axis.mainConstraint(l.cs)
 885  	last := l.Position.First + len(l.children)
 886  	if l.maxSize-l.Position.Offset < vsize && last == l.len {
 887  		l.Position.Offset = l.maxSize - vsize
 888  	}
 889  	if l.Position.Offset < 0 && l.Position.First == 0 {
 890  		l.Position.Offset = 0
 891  	}
 892  	firstSize, lastSize := 0, 0
 893  	if len(l.children) > 0 {
 894  		if l.Position.First > 0 {
 895  			firstChild := l.children[0]
 896  			firstSize = l.Axis.Convert(firstChild.size).X + l.Gap
 897  		}
 898  		if last < l.len {
 899  			lastChild := l.children[len(l.children)-1]
 900  			lastSize = l.Axis.Convert(lastChild.size).X + l.Gap
 901  		}
 902  	}
 903  	switch {
 904  	case len(l.children) == l.len:
 905  		return iterateNone
 906  	case l.maxSize-l.Position.Offset-lastSize < vsize:
 907  		return iterateForward
 908  	case l.Position.Offset-firstSize < 0:
 909  		return iterateBackward
 910  	}
 911  	return iterateNone
 912  }
 913  
 914  func (l *LayoutList) end(dims Dimensions, call CallOp) {
 915  	child := scrollChild{dims.Size, call}
 916  	mainSize := l.Axis.Convert(child.size).X
 917  	if len(l.children) > 0 {
 918  		l.maxSize += l.Gap
 919  	}
 920  	l.maxSize += mainSize
 921  	switch l.dir {
 922  	case iterateForward:
 923  		l.children = append(l.children, child)
 924  	case iterateBackward:
 925  		l.children = append(l.children, scrollChild{})
 926  		copy(l.children[1:], l.children)
 927  		l.children[0] = child
 928  		l.Position.First--
 929  		l.Position.Offset += mainSize + l.Gap
 930  	default:
 931  		panic("call Next before End")
 932  	}
 933  	l.dir = iterateNone
 934  }
 935  
 936  func (l *LayoutList) layout(ops *OpList, macro MacroOp) Dimensions {
 937  	if l.more() {
 938  		panic("unfinished child")
 939  	}
 940  	mainMin, mainMax := l.Axis.mainConstraint(l.cs)
 941  	children := l.children
 942  	var first scrollChild
 943  	for len(children) > 0 {
 944  		child := children[0]
 945  		sz := child.size
 946  		mainSize := l.Axis.Convert(sz).X
 947  		if l.Position.Offset < mainSize {
 948  			break
 949  		}
 950  		l.Position.First++
 951  		l.Position.Offset -= mainSize + l.Gap
 952  		first = child
 953  		children = children[1:]
 954  	}
 955  	size := -l.Position.Offset
 956  	var maxCross int
 957  	var last scrollChild
 958  	for i, child := range children {
 959  		sz := l.Axis.Convert(child.size)
 960  		if c := sz.Y; c > maxCross {
 961  			maxCross = c
 962  		}
 963  		if i > 0 {
 964  			size += l.Gap
 965  		}
 966  		size += sz.X
 967  		if size >= mainMax {
 968  			if i < len(children)-1 {
 969  				last = children[i+1]
 970  			}
 971  			children = children[:i+1]
 972  			break
 973  		}
 974  	}
 975  	l.Position.Count = len(children)
 976  	l.Position.OffsetLast = mainMax - size
 977  	if space := l.Position.OffsetLast; l.ScrollToEnd && space > 0 {
 978  		l.Position.Offset -= space
 979  	}
 980  	pos := -l.Position.Offset
 981  	layoutChild := func(child scrollChild) {
 982  		sz := l.Axis.Convert(child.size)
 983  		var cross int
 984  		switch l.Alignment {
 985  		case End:
 986  			cross = maxCross - sz.Y
 987  		case Middle:
 988  			cross = (maxCross - sz.Y) / 2
 989  		}
 990  		childSize := sz.X
 991  		pt := l.Axis.Convert(image.Pt(pos, cross))
 992  		trans := OpOffset(pt).Push(ops)
 993  		child.call.Add(ops)
 994  		trans.Pop()
 995  		pos += childSize
 996  	}
 997  	// Lay out leading invisible child.
 998  	if first != (scrollChild{}) {
 999  		sz := l.Axis.Convert(first.size)
1000  		pos -= sz.X + l.Gap
1001  		layoutChild(first)
1002  		pos += l.Gap
1003  	}
1004  	for i, child := range children {
1005  		if i > 0 {
1006  			pos += l.Gap
1007  		}
1008  		layoutChild(child)
1009  	}
1010  	// Lay out trailing invisible child.
1011  	if last != (scrollChild{}) {
1012  		pos += l.Gap
1013  		layoutChild(last)
1014  	}
1015  	atStart := l.Position.First == 0 && l.Position.Offset <= 0
1016  	atEnd := l.Position.First+len(children) == l.len && mainMax >= pos
1017  	if atStart && l.scrollDelta < 0 || atEnd && l.scrollDelta > 0 {
1018  		l.scroll.Stop()
1019  	}
1020  	l.Position.BeforeEnd = !atEnd
1021  	if pos < mainMin {
1022  		pos = mainMin
1023  	}
1024  	if pos > mainMax {
1025  		pos = mainMax
1026  	}
1027  	if crossMin, crossMax := l.Axis.crossConstraint(l.cs); maxCross < crossMin {
1028  		maxCross = crossMin
1029  	} else if maxCross > crossMax {
1030  		maxCross = crossMax
1031  	}
1032  	dims := l.Axis.Convert(image.Pt(pos, maxCross))
1033  	call := macro.Stop()
1034  	defer ClipRect(image.Rectangle{Max: dims}).Push(ops).Pop()
1035  
1036  	call.Add(ops)
1037  	return Dimensions{Size: dims}
1038  }
1039  
1040  // ScrollBy scrolls the list by a relative amount of items.
1041  func (l *LayoutList) ScrollBy(num float32) {
1042  	i, f := math.Modf(float64(num))
1043  	l.Position.First += int(i)
1044  	itemHeight := float64(l.Position.Length) / float64(l.len)
1045  	l.Position.Offset += int(math.Round(itemHeight * f))
1046  	l.Position.BeforeEnd = true
1047  }
1048  
1049  // ScrollTo scrolls to the specified item.
1050  func (l *LayoutList) ScrollTo(n int) {
1051  	l.Position.First = n
1052  	l.Position.Offset = 0
1053  	l.Position.BeforeEnd = true
1054  }
1055