Files
gio/ui/layout/flex.go
T
Elias Naur 29639565cd ui/layout: replace implicit begin/end scopes with explicit function scopes
Before this change, layout objects followed a pattern where a
begin method would set up the layout and return a tweaked set
of constraints, and a end method would take the widget dimensions
and return the tweaked dimensions.

As has been pointed out, this process is error prone, because the
scope of the layout objects are not clear and because it is easy
to swap two begins or two ends.

It turns out that it is possible to implement layout with function
scopes in garbage free way. A typical layout changes from

        al := layout.Align{Alignment: layout.NE}
	cs = al.Begin(ops, cs)
	in := layout.Inset{Top: ui.Dp(16)}
	cs = in.Begin(c, ops, cs)
	txt := fmt.Sprintf("m: %d %s", mallocs, u.profile.Timings)
	dims := text.Label{Material: theme.text, Face: u.face(fonts.mono, 10), Text: txt}.Layout(ops, cs)
	dims = in.End(dims)
	return al.End(dims)

to

        al := layout.Align{Alignment: layout.NE}
	return al.Layout(ops, cs, func(cs layout.Constraints) layout.Dimensions {
	       in := layout.Inset{Top: ui.Dp(16)}
	       return in.Layout(c, ops, cs, func(cs layout.Constraints) layout.Dimensions {
		       txt := fmt.Sprintf("m: %d %s", mallocs, u.profile.Timings)
		       return text.Label{Material: theme.text, Face: u.face(fonts.mono, 10), Text: txt}.Layout(ops, cs)
	       })
	})

Signed-off-by: Elias Naur <mail@eliasnaur.com>
2019-09-18 19:31:36 +02:00

296 lines
6.6 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"gioui.org/ui"
"gioui.org/ui/f32"
)
// Flex lays out child elements along an axis,
// according to alignment and weights.
type Flex struct {
// Axis is the main axis, either Horizontal or Vertical.
Axis Axis
// Spacing controls the distribution of space left after
// layout.
Spacing Spacing
// Alignment is the alignment in the cross axis.
Alignment Alignment
macro ui.MacroOp
ops *ui.Ops
cs Constraints
mode flexMode
size int
rigidSize int
// fraction is the rounding error from a Flexible weighting.
fraction float32
maxCross int
maxBaseline int
}
// FlexChild is the layout result of a call End.
type FlexChild struct {
macro ui.MacroOp
dims Dimensions
}
// Spacing determine the spacing mode for a Flex.
type Spacing uint8
type flexMode uint8
const (
// SpaceEnd leaves space at the end.
SpaceEnd Spacing = iota
// SpaceStart leaves space at the start.
SpaceStart
// SpaceSides shares space between the start and end.
SpaceSides
// SpaceAround distributes space evenly between children,
// with half as much space at the start and end.
SpaceAround
// SpaceBetween distributes space evenly between children,
// leaving no space at the start and end.
SpaceBetween
// SpaceEvenly distributes space evenly between children and
// at the start and end.
SpaceEvenly
)
const (
modeNone flexMode = iota
modeBegun
modeRigid
modeFlex
)
// Init must be called before Rigid or Flexible.
func (f *Flex) Init(ops *ui.Ops, cs Constraints) *Flex {
if f.mode > modeBegun {
panic("must End the current child before calling Init again")
}
f.mode = modeBegun
f.ops = ops
f.cs = cs
f.size = 0
f.rigidSize = 0
f.maxCross = 0
f.maxBaseline = 0
return f
}
func (f *Flex) begin(mode flexMode) {
switch {
case f.mode == modeNone:
panic("must Init before adding a child")
case f.mode > modeBegun:
panic("must End before adding a child")
}
f.mode = mode
f.macro.Record(f.ops)
}
// Rigid lays out a widget with the main axis constrained to the range
// from 0 to the remaining space.
func (f *Flex) Rigid(w Widget) FlexChild {
f.begin(modeRigid)
mainc := axisMainConstraint(f.Axis, f.cs)
mainMax := mainc.Max - f.size
if mainMax < 0 {
mainMax = 0
}
cs := axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, f.cs))
return f.end(w(cs))
}
// Flexible is like Rigid, where the main axis size is also constrained to a
// fraction of the space not taken up by Rigid children.
func (f *Flex) Flexible(weight float32, w Widget) FlexChild {
f.begin(modeFlex)
mainc := axisMainConstraint(f.Axis, f.cs)
var flexSize int
if mainc.Max > f.size {
flexSize = mainc.Max - f.rigidSize
// Apply weight and add any leftover fraction from a
// previous Flexible.
size := float32(flexSize)*weight + f.fraction
flexSize = int(size + .5)
f.fraction = size - float32(flexSize)
if max := mainc.Max - f.size; flexSize > max {
flexSize = max
}
}
submainc := Constraint{Max: flexSize}
cs := axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, f.cs))
return f.end(w(cs))
}
// End a child by specifying its dimensions. Pass the returned layout result
// to Layout.
func (f *Flex) end(dims Dimensions) FlexChild {
if f.mode <= modeBegun {
panic("End called without an active child")
}
f.macro.Stop()
sz := axisMain(f.Axis, dims.Size)
f.size += sz
if f.mode == modeRigid {
f.rigidSize += sz
}
f.mode = modeBegun
if c := axisCross(f.Axis, dims.Size); c > f.maxCross {
f.maxCross = c
}
if b := dims.Baseline; b > f.maxBaseline {
f.maxBaseline = b
}
return FlexChild{f.macro, dims}
}
// Layout a list of children. The order of the children determines their laid
// out order.
func (f *Flex) Layout(children ...FlexChild) Dimensions {
mainc := axisMainConstraint(f.Axis, f.cs)
crossSize := axisCrossConstraint(f.Axis, f.cs).Constrain(f.maxCross)
var space int
if mainc.Min > f.size {
space = mainc.Min - f.size
}
var mainSize int
var baseline int
switch f.Spacing {
case SpaceSides:
mainSize += space / 2
case SpaceStart:
mainSize += space
case SpaceEvenly:
mainSize += space / (1 + len(children))
case SpaceAround:
mainSize += space / (len(children) * 2)
}
for i, child := range children {
dims := child.dims
b := dims.Baseline
var cross int
switch f.Alignment {
case End:
cross = crossSize - axisCross(f.Axis, dims.Size)
case Middle:
cross = (crossSize - axisCross(f.Axis, dims.Size)) / 2
case Baseline:
if f.Axis == Horizontal {
cross = f.maxBaseline - b
}
}
var stack ui.StackOp
stack.Push(f.ops)
ui.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(f.ops)
child.macro.Add(f.ops)
stack.Pop()
mainSize += axisMain(f.Axis, dims.Size)
if i < len(children)-1 {
switch f.Spacing {
case SpaceEvenly:
mainSize += space / (1 + len(children))
case SpaceAround:
mainSize += space / len(children)
case SpaceBetween:
mainSize += space / (len(children) - 1)
}
}
if b != dims.Size.Y {
baseline = b
}
}
switch f.Spacing {
case SpaceSides:
mainSize += space / 2
case SpaceEnd:
mainSize += space
case SpaceEvenly:
mainSize += space / (1 + len(children))
case SpaceAround:
mainSize += space / (len(children) * 2)
}
sz := axisPoint(f.Axis, mainSize, crossSize)
if baseline == 0 {
baseline = sz.Y
}
return Dimensions{Size: sz, Baseline: baseline}
}
func axisPoint(a Axis, main, cross int) image.Point {
if a == Horizontal {
return image.Point{main, cross}
} else {
return image.Point{cross, main}
}
}
func axisMain(a Axis, sz image.Point) int {
if a == Horizontal {
return sz.X
} else {
return sz.Y
}
}
func axisCross(a Axis, sz image.Point) int {
if a == Horizontal {
return sz.Y
} else {
return sz.X
}
}
func axisMainConstraint(a Axis, cs Constraints) Constraint {
if a == Horizontal {
return cs.Width
} else {
return cs.Height
}
}
func axisCrossConstraint(a Axis, cs Constraints) Constraint {
if a == Horizontal {
return cs.Height
} else {
return cs.Width
}
}
func axisConstraints(a Axis, mainc, crossc Constraint) Constraints {
if a == Horizontal {
return Constraints{Width: mainc, Height: crossc}
} else {
return Constraints{Width: crossc, Height: mainc}
}
}
func toPointF(p image.Point) f32.Point {
return f32.Point{X: float32(p.X), Y: float32(p.Y)}
}
func (s Spacing) String() string {
switch s {
case SpaceEnd:
return "SpaceEnd"
case SpaceStart:
return "SpaceStart"
case SpaceSides:
return "SpaceSides"
case SpaceAround:
return "SpaceAround"
case SpaceBetween:
return "SpaceAround"
case SpaceEvenly:
return "SpaceEvenly"
default:
panic("unreachable")
}
}