github.com/Seikaijyu/gio@v0.0.1/layout/flex.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package layout
     4  
     5  import (
     6  	"image"
     7  
     8  	"github.com/Seikaijyu/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  	maxCross := crossMin
   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  		pt := f.Axis.Convert(image.Pt(mainSize, cross))
   187  		trans := op.Offset(pt).Push(gtx.Ops)
   188  		child.call.Add(gtx.Ops)
   189  		trans.Pop()
   190  		mainSize += f.Axis.Convert(dims.Size).X
   191  		if i < len(children)-1 {
   192  			switch f.Spacing {
   193  			case SpaceEvenly:
   194  				mainSize += space / (1 + len(children))
   195  			case SpaceAround:
   196  				if len(children) > 0 {
   197  					mainSize += space / len(children)
   198  				}
   199  			case SpaceBetween:
   200  				if len(children) > 1 {
   201  					mainSize += space / (len(children) - 1)
   202  				}
   203  			}
   204  		}
   205  	}
   206  	switch f.Spacing {
   207  	case SpaceSides:
   208  		mainSize += space / 2
   209  	case SpaceEnd:
   210  		mainSize += space
   211  	case SpaceEvenly:
   212  		mainSize += space / (1 + len(children))
   213  	case SpaceAround:
   214  		if len(children) > 0 {
   215  			mainSize += space / (len(children) * 2)
   216  		}
   217  	}
   218  	sz := f.Axis.Convert(image.Pt(mainSize, maxCross))
   219  	sz = cs.Constrain(sz)
   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  }