github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/layout/flex.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package layout
     4  
     5  import (
     6  	"image"
     7  
     8  	"github.com/gop9/olt/gio/f32"
     9  	"github.com/gop9/olt/gio/op"
    10  )
    11  
    12  // Flex lays out child elements along an axis,
    13  // according to alignment and weights.
    14  type Flex struct {
    15  	// Axis is the main axis, either Horizontal or Vertical.
    16  	Axis Axis
    17  	// Spacing controls the distribution of space left after
    18  	// layout.
    19  	Spacing Spacing
    20  	// Alignment is the alignment in the cross axis.
    21  	Alignment Alignment
    22  }
    23  
    24  // FlexChild is the descriptor for a Flex child.
    25  type FlexChild struct {
    26  	flex   bool
    27  	weight float32
    28  
    29  	widget Widget
    30  
    31  	// Scratch space.
    32  	macro op.MacroOp
    33  	dims  Dimensions
    34  }
    35  
    36  // Spacing determine the spacing mode for a Flex.
    37  type Spacing uint8
    38  
    39  type flexMode uint8
    40  
    41  const (
    42  	// SpaceEnd leaves space at the end.
    43  	SpaceEnd Spacing = iota
    44  	// SpaceStart leaves space at the start.
    45  	SpaceStart
    46  	// SpaceSides shares space between the start and end.
    47  	SpaceSides
    48  	// SpaceAround distributes space evenly between children,
    49  	// with half as much space at the start and end.
    50  	SpaceAround
    51  	// SpaceBetween distributes space evenly between children,
    52  	// leaving no space at the start and end.
    53  	SpaceBetween
    54  	// SpaceEvenly distributes space evenly between children and
    55  	// at the start and end.
    56  	SpaceEvenly
    57  )
    58  
    59  // Rigid returns a Flex child with a maximal constraint of the
    60  // remaining space.
    61  func Rigid(widget Widget) FlexChild {
    62  	return FlexChild{
    63  		widget: widget,
    64  	}
    65  }
    66  
    67  // Flexed returns a Flex child forced to take up a fraction of
    68  // the remaining space.
    69  func Flexed(weight float32, widget Widget) FlexChild {
    70  	return FlexChild{
    71  		flex:   true,
    72  		weight: weight,
    73  		widget: widget,
    74  	}
    75  }
    76  
    77  // Layout a list of children. The position of the children are
    78  // determined by the specified order, but Rigid children are laid out
    79  // before Flexed children.
    80  func (f Flex) Layout(gtx *Context, children ...FlexChild) {
    81  	size := 0
    82  	// Lay out Rigid children.
    83  	for i, child := range children {
    84  		if child.flex {
    85  			continue
    86  		}
    87  		cs := gtx.Constraints
    88  		mainc := axisMainConstraint(f.Axis, cs)
    89  		mainMax := mainc.Max - size
    90  		if mainMax < 0 {
    91  			mainMax = 0
    92  		}
    93  		cs = axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, cs))
    94  		var m op.MacroOp
    95  		m.Record(gtx.Ops)
    96  		dims := ctxLayout(gtx, cs, child.widget)
    97  		m.Stop()
    98  		sz := axisMain(f.Axis, dims.Size)
    99  		size += sz
   100  		children[i].macro = m
   101  		children[i].dims = dims
   102  	}
   103  	rigidSize := size
   104  	// fraction is the rounding error from a Flex weighting.
   105  	var fraction float32
   106  	// Lay out Flexed children.
   107  	for i, child := range children {
   108  		if !child.flex {
   109  			continue
   110  		}
   111  		cs := gtx.Constraints
   112  		mainc := axisMainConstraint(f.Axis, cs)
   113  		var flexSize int
   114  		if mainc.Max > size {
   115  			flexSize = mainc.Max - rigidSize
   116  			// Apply weight and add any leftover fraction from a
   117  			// previous Flexed.
   118  			childSize := float32(flexSize)*child.weight + fraction
   119  			flexSize = int(childSize + .5)
   120  			fraction = childSize - float32(flexSize)
   121  			if max := mainc.Max - size; flexSize > max {
   122  				flexSize = max
   123  			}
   124  		}
   125  		submainc := Constraint{Min: flexSize, Max: flexSize}
   126  		cs = axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, cs))
   127  		var m op.MacroOp
   128  		m.Record(gtx.Ops)
   129  		dims := ctxLayout(gtx, cs, child.widget)
   130  		m.Stop()
   131  		sz := axisMain(f.Axis, dims.Size)
   132  		size += sz
   133  		children[i].macro = m
   134  		children[i].dims = dims
   135  	}
   136  	var maxCross int
   137  	var maxBaseline int
   138  	for _, child := range children {
   139  		if c := axisCross(f.Axis, child.dims.Size); c > maxCross {
   140  			maxCross = c
   141  		}
   142  		if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline {
   143  			maxBaseline = b
   144  		}
   145  	}
   146  	cs := gtx.Constraints
   147  	mainc := axisMainConstraint(f.Axis, cs)
   148  	var space int
   149  	if mainc.Min > size {
   150  		space = mainc.Min - size
   151  	}
   152  	var mainSize int
   153  	switch f.Spacing {
   154  	case SpaceSides:
   155  		mainSize += space / 2
   156  	case SpaceStart:
   157  		mainSize += space
   158  	case SpaceEvenly:
   159  		mainSize += space / (1 + len(children))
   160  	case SpaceAround:
   161  		mainSize += space / (len(children) * 2)
   162  	}
   163  	for i, child := range children {
   164  		dims := child.dims
   165  		b := dims.Size.Y - dims.Baseline
   166  		var cross int
   167  		switch f.Alignment {
   168  		case End:
   169  			cross = maxCross - axisCross(f.Axis, dims.Size)
   170  		case Middle:
   171  			cross = (maxCross - axisCross(f.Axis, dims.Size)) / 2
   172  		case Baseline:
   173  			if f.Axis == Horizontal {
   174  				cross = maxBaseline - b
   175  			}
   176  		}
   177  		var stack op.StackOp
   178  		stack.Push(gtx.Ops)
   179  		op.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(gtx.Ops)
   180  		child.macro.Add()
   181  		stack.Pop()
   182  		mainSize += axisMain(f.Axis, dims.Size)
   183  		if i < len(children)-1 {
   184  			switch f.Spacing {
   185  			case SpaceEvenly:
   186  				mainSize += space / (1 + len(children))
   187  			case SpaceAround:
   188  				mainSize += space / len(children)
   189  			case SpaceBetween:
   190  				mainSize += space / (len(children) - 1)
   191  			}
   192  		}
   193  	}
   194  	switch f.Spacing {
   195  	case SpaceSides:
   196  		mainSize += space / 2
   197  	case SpaceEnd:
   198  		mainSize += space
   199  	case SpaceEvenly:
   200  		mainSize += space / (1 + len(children))
   201  	case SpaceAround:
   202  		mainSize += space / (len(children) * 2)
   203  	}
   204  	sz := axisPoint(f.Axis, mainSize, maxCross)
   205  	gtx.Dimensions = Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
   206  }
   207  
   208  func axisPoint(a Axis, main, cross int) image.Point {
   209  	if a == Horizontal {
   210  		return image.Point{main, cross}
   211  	} else {
   212  		return image.Point{cross, main}
   213  	}
   214  }
   215  
   216  func axisMain(a Axis, sz image.Point) int {
   217  	if a == Horizontal {
   218  		return sz.X
   219  	} else {
   220  		return sz.Y
   221  	}
   222  }
   223  
   224  func axisCross(a Axis, sz image.Point) int {
   225  	if a == Horizontal {
   226  		return sz.Y
   227  	} else {
   228  		return sz.X
   229  	}
   230  }
   231  
   232  func axisMainConstraint(a Axis, cs Constraints) Constraint {
   233  	if a == Horizontal {
   234  		return cs.Width
   235  	} else {
   236  		return cs.Height
   237  	}
   238  }
   239  
   240  func axisCrossConstraint(a Axis, cs Constraints) Constraint {
   241  	if a == Horizontal {
   242  		return cs.Height
   243  	} else {
   244  		return cs.Width
   245  	}
   246  }
   247  
   248  func axisConstraints(a Axis, mainc, crossc Constraint) Constraints {
   249  	if a == Horizontal {
   250  		return Constraints{Width: mainc, Height: crossc}
   251  	} else {
   252  		return Constraints{Width: crossc, Height: mainc}
   253  	}
   254  }
   255  
   256  func toPointF(p image.Point) f32.Point {
   257  	return f32.Point{X: float32(p.X), Y: float32(p.Y)}
   258  }
   259  
   260  func (s Spacing) String() string {
   261  	switch s {
   262  	case SpaceEnd:
   263  		return "SpaceEnd"
   264  	case SpaceStart:
   265  		return "SpaceStart"
   266  	case SpaceSides:
   267  		return "SpaceSides"
   268  	case SpaceAround:
   269  		return "SpaceAround"
   270  	case SpaceBetween:
   271  		return "SpaceAround"
   272  	case SpaceEvenly:
   273  		return "SpaceEvenly"
   274  	default:
   275  		panic("unreachable")
   276  	}
   277  }