flex.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/op"
   9  )
  10  
  11  // Flex lays out child elements along an axis,
  12  // according to alignment and weights.
  13  type Flex struct {
  14  	// Axis is the main axis, either Horizontal or Vertical.
  15  	Axis Axis
  16  	// Spacing controls the distribution of space left after
  17  	// layout.
  18  	Spacing Spacing
  19  	// Alignment is the alignment in the cross axis.
  20  	Alignment Alignment
  21  	// WeightSum is the sum of weights used for the weighted
  22  	// size of Flexed children. If WeightSum is zero, the sum
  23  	// of all Flexed weights is used.
  24  	WeightSum float32
  25  }
  26  
  27  // FlexChild is the descriptor for a Flex child.
  28  type FlexChild struct {
  29  	flex   bool
  30  	weight float32
  31  
  32  	widget Widget
  33  
  34  	// Scratch space.
  35  	call op.CallOp
  36  	dims Dimensions
  37  }
  38  
  39  // Spacing determine the spacing mode for a Flex.
  40  type Spacing uint8
  41  
  42  const (
  43  	// SpaceEnd leaves space at the end.
  44  	SpaceEnd Spacing = iota
  45  	// SpaceStart leaves space at the start.
  46  	SpaceStart
  47  	// SpaceSides shares space between the start and end.
  48  	SpaceSides
  49  	// SpaceAround distributes space evenly between children,
  50  	// with half as much space at the start and end.
  51  	SpaceAround
  52  	// SpaceBetween distributes space evenly between children,
  53  	// leaving no space at the start and end.
  54  	SpaceBetween
  55  	// SpaceEvenly distributes space evenly between children and
  56  	// at the start and end.
  57  	SpaceEvenly
  58  )
  59  
  60  // Rigid returns a Flex child with a maximal constraint of the
  61  // remaining space.
  62  func Rigid(widget Widget) FlexChild {
  63  	return FlexChild{
  64  		widget: widget,
  65  	}
  66  }
  67  
  68  // Flexed returns a Flex child forced to take up weight fraction of the
  69  // space left over from Rigid children. The fraction is weight
  70  // divided by either the weight sum of all Flexed children or the Flex
  71  // WeightSum if non zero.
  72  func Flexed(weight float32, widget Widget) FlexChild {
  73  	return FlexChild{
  74  		flex:   true,
  75  		weight: weight,
  76  		widget: widget,
  77  	}
  78  }
  79  
  80  // Layout a list of children. The position of the children are
  81  // determined by the specified order, but Rigid children are laid out
  82  // before Flexed children.
  83  func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
  84  	size := 0
  85  	cs := gtx.Constraints
  86  	mainMin, mainMax := f.Axis.mainConstraint(cs)
  87  	crossMin, crossMax := f.Axis.crossConstraint(cs)
  88  	remaining := mainMax
  89  	var totalWeight float32
  90  	cgtx := gtx
  91  	// Lay out Rigid children.
  92  	for i, child := range children {
  93  		if child.flex {
  94  			totalWeight += child.weight
  95  			continue
  96  		}
  97  		macro := op.Record(gtx.Ops)
  98  		cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax)
  99  		dims := child.widget(cgtx)
 100  		c := macro.Stop()
 101  		sz := f.Axis.Convert(dims.Size).X
 102  		size += sz
 103  		remaining -= sz
 104  		if remaining < 0 {
 105  			remaining = 0
 106  		}
 107  		children[i].call = c
 108  		children[i].dims = dims
 109  	}
 110  	if w := f.WeightSum; w != 0 {
 111  		totalWeight = w
 112  	}
 113  	// fraction is the rounding error from a Flex weighting.
 114  	var fraction float32
 115  	flexTotal := remaining
 116  	// Lay out Flexed children.
 117  	for i, child := range children {
 118  		if !child.flex {
 119  			continue
 120  		}
 121  		var flexSize int
 122  		if remaining > 0 && totalWeight > 0 {
 123  			// Apply weight and add any leftover fraction from a
 124  			// previous Flexed.
 125  			childSize := float32(flexTotal) * child.weight / totalWeight
 126  			flexSize = int(childSize + fraction + .5)
 127  			fraction = childSize - float32(flexSize)
 128  			if flexSize > remaining {
 129  				flexSize = remaining
 130  			}
 131  		}
 132  		macro := op.Record(gtx.Ops)
 133  		cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, crossMax)
 134  		dims := child.widget(cgtx)
 135  		c := macro.Stop()
 136  		sz := f.Axis.Convert(dims.Size).X
 137  		size += sz
 138  		remaining -= sz
 139  		if remaining < 0 {
 140  			remaining = 0
 141  		}
 142  		children[i].call = c
 143  		children[i].dims = dims
 144  	}
 145  	var maxCross int
 146  	var maxBaseline int
 147  	for _, child := range children {
 148  		if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross {
 149  			maxCross = c
 150  		}
 151  		if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline {
 152  			maxBaseline = b
 153  		}
 154  	}
 155  	var space int
 156  	if mainMin > size {
 157  		space = mainMin - size
 158  	}
 159  	var mainSize int
 160  	switch f.Spacing {
 161  	case SpaceSides:
 162  		mainSize += space / 2
 163  	case SpaceStart:
 164  		mainSize += space
 165  	case SpaceEvenly:
 166  		mainSize += space / (1 + len(children))
 167  	case SpaceAround:
 168  		if len(children) > 0 {
 169  			mainSize += space / (len(children) * 2)
 170  		}
 171  	}
 172  	for i, child := range children {
 173  		dims := child.dims
 174  		b := dims.Size.Y - dims.Baseline
 175  		var cross int
 176  		switch f.Alignment {
 177  		case End:
 178  			cross = maxCross - f.Axis.Convert(dims.Size).Y
 179  		case Middle:
 180  			cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2
 181  		case Baseline:
 182  			if f.Axis == Horizontal {
 183  				cross = maxBaseline - b
 184  			}
 185  		}
 186  		stack := op.Save(gtx.Ops)
 187  		pt := f.Axis.Convert(image.Pt(mainSize, cross))
 188  		op.Offset(FPt(pt)).Add(gtx.Ops)
 189  		child.call.Add(gtx.Ops)
 190  		stack.Load()
 191  		mainSize += f.Axis.Convert(dims.Size).X
 192  		if i < len(children)-1 {
 193  			switch f.Spacing {
 194  			case SpaceEvenly:
 195  				mainSize += space / (1 + len(children))
 196  			case SpaceAround:
 197  				if len(children) > 0 {
 198  					mainSize += space / len(children)
 199  				}
 200  			case SpaceBetween:
 201  				if len(children) > 1 {
 202  					mainSize += space / (len(children) - 1)
 203  				}
 204  			}
 205  		}
 206  	}
 207  	switch f.Spacing {
 208  	case SpaceSides:
 209  		mainSize += space / 2
 210  	case SpaceEnd:
 211  		mainSize += space
 212  	case SpaceEvenly:
 213  		mainSize += space / (1 + len(children))
 214  	case SpaceAround:
 215  		if len(children) > 0 {
 216  			mainSize += space / (len(children) * 2)
 217  		}
 218  	}
 219  	sz := f.Axis.Convert(image.Pt(mainSize, maxCross))
 220  	return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
 221  }
 222  
 223  func (s Spacing) String() string {
 224  	switch s {
 225  	case SpaceEnd:
 226  		return "SpaceEnd"
 227  	case SpaceStart:
 228  		return "SpaceStart"
 229  	case SpaceSides:
 230  		return "SpaceSides"
 231  	case SpaceAround:
 232  		return "SpaceAround"
 233  	case SpaceBetween:
 234  		return "SpaceAround"
 235  	case SpaceEvenly:
 236  		return "SpaceEvenly"
 237  	default:
 238  		panic("unreachable")
 239  	}
 240  }
 241