gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/layout/flex.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package layout
     4  
     5  import (
     6  	"image"
     7  
     8  	"gioui.org/ui"
     9  	"gioui.org/ui/f32"
    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  	ctx       *Context
    24  	macro     ui.MacroOp
    25  	mode      flexMode
    26  	size      int
    27  	rigidSize int
    28  	// fraction is the rounding error from a Flexible weighting.
    29  	fraction    float32
    30  	maxCross    int
    31  	maxBaseline int
    32  }
    33  
    34  // FlexChild is the layout result of a call End.
    35  type FlexChild struct {
    36  	macro ui.MacroOp
    37  	dims  Dimensions
    38  }
    39  
    40  // Spacing determine the spacing mode for a Flex.
    41  type Spacing uint8
    42  
    43  type flexMode uint8
    44  
    45  const (
    46  	// SpaceEnd leaves space at the end.
    47  	SpaceEnd Spacing = iota
    48  	// SpaceStart leaves space at the start.
    49  	SpaceStart
    50  	// SpaceSides shares space between the start and end.
    51  	SpaceSides
    52  	// SpaceAround distributes space evenly between children,
    53  	// with half as much space at the start and end.
    54  	SpaceAround
    55  	// SpaceBetween distributes space evenly between children,
    56  	// leaving no space at the start and end.
    57  	SpaceBetween
    58  	// SpaceEvenly distributes space evenly between children and
    59  	// at the start and end.
    60  	SpaceEvenly
    61  )
    62  
    63  const (
    64  	modeNone flexMode = iota
    65  	modeBegun
    66  	modeRigid
    67  	modeFlex
    68  )
    69  
    70  // Init must be called before Rigid or Flexible.
    71  func (f *Flex) Init(gtx *Context) *Flex {
    72  	if f.mode > modeBegun {
    73  		panic("must End the current child before calling Init again")
    74  	}
    75  	f.mode = modeBegun
    76  	f.ctx = gtx
    77  	f.size = 0
    78  	f.rigidSize = 0
    79  	f.maxCross = 0
    80  	f.maxBaseline = 0
    81  	return f
    82  }
    83  
    84  func (f *Flex) begin(mode flexMode) {
    85  	switch {
    86  	case f.mode == modeNone:
    87  		panic("must Init before adding a child")
    88  	case f.mode > modeBegun:
    89  		panic("must End before adding a child")
    90  	}
    91  	f.mode = mode
    92  	f.macro.Record(f.ctx.Ops)
    93  }
    94  
    95  // Rigid lays out a widget with the main axis constrained to the range
    96  // from 0 to the remaining space.
    97  func (f *Flex) Rigid(w Widget) FlexChild {
    98  	f.begin(modeRigid)
    99  	cs := f.ctx.Constraints
   100  	mainc := axisMainConstraint(f.Axis, cs)
   101  	mainMax := mainc.Max - f.size
   102  	if mainMax < 0 {
   103  		mainMax = 0
   104  	}
   105  	cs = axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, cs))
   106  	dims := f.ctx.Layout(cs, w)
   107  	return f.end(dims)
   108  }
   109  
   110  // Flexible is like Rigid, where the main axis size is also constrained to a
   111  // fraction of the space not taken up by Rigid children.
   112  func (f *Flex) Flexible(weight float32, w Widget) FlexChild {
   113  	f.begin(modeFlex)
   114  	cs := f.ctx.Constraints
   115  	mainc := axisMainConstraint(f.Axis, cs)
   116  	var flexSize int
   117  	if mainc.Max > f.size {
   118  		flexSize = mainc.Max - f.rigidSize
   119  		// Apply weight and add any leftover fraction from a
   120  		// previous Flexible.
   121  		size := float32(flexSize)*weight + f.fraction
   122  		flexSize = int(size + .5)
   123  		f.fraction = size - float32(flexSize)
   124  		if max := mainc.Max - f.size; flexSize > max {
   125  			flexSize = max
   126  		}
   127  	}
   128  	submainc := Constraint{Max: flexSize}
   129  	cs = axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, cs))
   130  	dims := f.ctx.Layout(cs, w)
   131  	return f.end(dims)
   132  }
   133  
   134  // End a child by specifying its dimensions. Pass the returned layout result
   135  // to Layout.
   136  func (f *Flex) end(dims Dimensions) FlexChild {
   137  	if f.mode <= modeBegun {
   138  		panic("End called without an active child")
   139  	}
   140  	f.macro.Stop()
   141  	sz := axisMain(f.Axis, dims.Size)
   142  	f.size += sz
   143  	if f.mode == modeRigid {
   144  		f.rigidSize += sz
   145  	}
   146  	f.mode = modeBegun
   147  	if c := axisCross(f.Axis, dims.Size); c > f.maxCross {
   148  		f.maxCross = c
   149  	}
   150  	if b := dims.Baseline; b > f.maxBaseline {
   151  		f.maxBaseline = b
   152  	}
   153  	return FlexChild{f.macro, dims}
   154  }
   155  
   156  // Layout a list of children. The order of the children determines their laid
   157  // out order.
   158  func (f *Flex) Layout(children ...FlexChild) {
   159  	cs := f.ctx.Constraints
   160  	mainc := axisMainConstraint(f.Axis, cs)
   161  	crossSize := axisCrossConstraint(f.Axis, cs).Constrain(f.maxCross)
   162  	var space int
   163  	if mainc.Min > f.size {
   164  		space = mainc.Min - f.size
   165  	}
   166  	var mainSize int
   167  	var baseline int
   168  	switch f.Spacing {
   169  	case SpaceSides:
   170  		mainSize += space / 2
   171  	case SpaceStart:
   172  		mainSize += space
   173  	case SpaceEvenly:
   174  		mainSize += space / (1 + len(children))
   175  	case SpaceAround:
   176  		mainSize += space / (len(children) * 2)
   177  	}
   178  	for i, child := range children {
   179  		dims := child.dims
   180  		b := dims.Baseline
   181  		var cross int
   182  		switch f.Alignment {
   183  		case End:
   184  			cross = crossSize - axisCross(f.Axis, dims.Size)
   185  		case Middle:
   186  			cross = (crossSize - axisCross(f.Axis, dims.Size)) / 2
   187  		case Baseline:
   188  			if f.Axis == Horizontal {
   189  				cross = f.maxBaseline - b
   190  			}
   191  		}
   192  		var stack ui.StackOp
   193  		stack.Push(f.ctx.Ops)
   194  		ui.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(f.ctx.Ops)
   195  		child.macro.Add(f.ctx.Ops)
   196  		stack.Pop()
   197  		mainSize += axisMain(f.Axis, dims.Size)
   198  		if i < len(children)-1 {
   199  			switch f.Spacing {
   200  			case SpaceEvenly:
   201  				mainSize += space / (1 + len(children))
   202  			case SpaceAround:
   203  				mainSize += space / len(children)
   204  			case SpaceBetween:
   205  				mainSize += space / (len(children) - 1)
   206  			}
   207  		}
   208  		if b != dims.Size.Y {
   209  			baseline = b
   210  		}
   211  	}
   212  	switch f.Spacing {
   213  	case SpaceSides:
   214  		mainSize += space / 2
   215  	case SpaceEnd:
   216  		mainSize += space
   217  	case SpaceEvenly:
   218  		mainSize += space / (1 + len(children))
   219  	case SpaceAround:
   220  		mainSize += space / (len(children) * 2)
   221  	}
   222  	sz := axisPoint(f.Axis, mainSize, crossSize)
   223  	if baseline == 0 {
   224  		baseline = sz.Y
   225  	}
   226  	f.ctx.Dimensions = Dimensions{Size: sz, Baseline: baseline}
   227  }
   228  
   229  func axisPoint(a Axis, main, cross int) image.Point {
   230  	if a == Horizontal {
   231  		return image.Point{main, cross}
   232  	} else {
   233  		return image.Point{cross, main}
   234  	}
   235  }
   236  
   237  func axisMain(a Axis, sz image.Point) int {
   238  	if a == Horizontal {
   239  		return sz.X
   240  	} else {
   241  		return sz.Y
   242  	}
   243  }
   244  
   245  func axisCross(a Axis, sz image.Point) int {
   246  	if a == Horizontal {
   247  		return sz.Y
   248  	} else {
   249  		return sz.X
   250  	}
   251  }
   252  
   253  func axisMainConstraint(a Axis, cs Constraints) Constraint {
   254  	if a == Horizontal {
   255  		return cs.Width
   256  	} else {
   257  		return cs.Height
   258  	}
   259  }
   260  
   261  func axisCrossConstraint(a Axis, cs Constraints) Constraint {
   262  	if a == Horizontal {
   263  		return cs.Height
   264  	} else {
   265  		return cs.Width
   266  	}
   267  }
   268  
   269  func axisConstraints(a Axis, mainc, crossc Constraint) Constraints {
   270  	if a == Horizontal {
   271  		return Constraints{Width: mainc, Height: crossc}
   272  	} else {
   273  		return Constraints{Width: crossc, Height: mainc}
   274  	}
   275  }
   276  
   277  func toPointF(p image.Point) f32.Point {
   278  	return f32.Point{X: float32(p.X), Y: float32(p.Y)}
   279  }
   280  
   281  func (s Spacing) String() string {
   282  	switch s {
   283  	case SpaceEnd:
   284  		return "SpaceEnd"
   285  	case SpaceStart:
   286  		return "SpaceStart"
   287  	case SpaceSides:
   288  		return "SpaceSides"
   289  	case SpaceAround:
   290  		return "SpaceAround"
   291  	case SpaceBetween:
   292  		return "SpaceAround"
   293  	case SpaceEvenly:
   294  		return "SpaceEvenly"
   295  	default:
   296  		panic("unreachable")
   297  	}
   298  }