forked from joejulian/gio
3af01a3f43
Change the definition of Widget from the implicit
type Widget func()
to the explicit functional
type Widget func(gtx layout.Context) layout.Dimensions
The advantages are numerous:
- Clearer connection between the incoming context and the output dimensions.
- Returning the Dimensions are impossible to omit.
- Contexts passed by value, so its fields can be exported
and freely mutated by the program.
The only disadvantage is the longer function literals and the many "returns".
What tipped the scales in favour of the explicit Widget variant is that type
aliases can dramatically shorten the literals:
type (
C = layout.Context
D = layout.Dimensions
)
widget := func(gtx C) D {
...
}
Note that the aliases are not part of the Gio API and it is up to each user
whether they want to use them.
Finally the Go proposal for lightweight function literals,
https://github.com/golang/go/issues/21498, may remove the disadvantage
completely in future.
Context becomes a plain struct with only public fields, and its Reset is
replaced by a NewContext convenience constructor.
Signed-off-by: Elias Naur <mail@eliasnaur.com>
276 lines
6.3 KiB
Go
276 lines
6.3 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package layout
|
|
|
|
import (
|
|
"image"
|
|
|
|
"gioui.org/op"
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// FlexChild is the descriptor for a Flex child.
|
|
type FlexChild struct {
|
|
flex bool
|
|
weight float32
|
|
|
|
widget Widget
|
|
|
|
// Scratch space.
|
|
macro op.MacroOp
|
|
dims Dimensions
|
|
}
|
|
|
|
// Spacing determine the spacing mode for a Flex.
|
|
type Spacing 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
|
|
)
|
|
|
|
// Rigid returns a Flex child with a maximal constraint of the
|
|
// remaining space.
|
|
func Rigid(widget Widget) FlexChild {
|
|
return FlexChild{
|
|
widget: widget,
|
|
}
|
|
}
|
|
|
|
// Flexed returns a Flex child forced to take up a fraction of
|
|
// the remaining space.
|
|
func Flexed(weight float32, widget Widget) FlexChild {
|
|
return FlexChild{
|
|
flex: true,
|
|
weight: weight,
|
|
widget: widget,
|
|
}
|
|
}
|
|
|
|
// Layout a list of children. The position of the children are
|
|
// determined by the specified order, but Rigid children are laid out
|
|
// before Flexed children.
|
|
func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions {
|
|
size := 0
|
|
// Lay out Rigid children.
|
|
for i, child := range children {
|
|
if child.flex {
|
|
continue
|
|
}
|
|
cs := gtx.Constraints
|
|
_, mainMax := axisMainConstraint(f.Axis, cs)
|
|
mainMax -= size
|
|
if mainMax < 0 {
|
|
mainMax = 0
|
|
}
|
|
crossMin, crossMax := axisCrossConstraint(f.Axis, cs)
|
|
cs = axisConstraints(f.Axis, 0, mainMax, crossMin, crossMax)
|
|
var m op.MacroOp
|
|
m.Record(gtx.Ops)
|
|
gtx := gtx
|
|
gtx.Constraints = cs
|
|
dims := child.widget(gtx)
|
|
m.Stop()
|
|
sz := axisMain(f.Axis, dims.Size)
|
|
size += sz
|
|
children[i].macro = m
|
|
children[i].dims = dims
|
|
}
|
|
rigidSize := size
|
|
// fraction is the rounding error from a Flex weighting.
|
|
var fraction float32
|
|
// Lay out Flexed children.
|
|
for i, child := range children {
|
|
if !child.flex {
|
|
continue
|
|
}
|
|
cs := gtx.Constraints
|
|
_, mainMax := axisMainConstraint(f.Axis, cs)
|
|
var flexSize int
|
|
if mainMax > size {
|
|
flexSize = mainMax - rigidSize
|
|
// Apply weight and add any leftover fraction from a
|
|
// previous Flexed.
|
|
childSize := float32(flexSize)*child.weight + fraction
|
|
flexSize = int(childSize + .5)
|
|
fraction = childSize - float32(flexSize)
|
|
if max := mainMax - size; flexSize > max {
|
|
flexSize = max
|
|
}
|
|
}
|
|
crossMin, crossMax := axisCrossConstraint(f.Axis, cs)
|
|
cs = axisConstraints(f.Axis, flexSize, flexSize, crossMin, crossMax)
|
|
var m op.MacroOp
|
|
m.Record(gtx.Ops)
|
|
gtx := gtx
|
|
gtx.Constraints = cs
|
|
dims := child.widget(gtx)
|
|
m.Stop()
|
|
sz := axisMain(f.Axis, dims.Size)
|
|
size += sz
|
|
children[i].macro = m
|
|
children[i].dims = dims
|
|
}
|
|
var maxCross int
|
|
var maxBaseline int
|
|
for _, child := range children {
|
|
if c := axisCross(f.Axis, child.dims.Size); c > maxCross {
|
|
maxCross = c
|
|
}
|
|
if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline {
|
|
maxBaseline = b
|
|
}
|
|
}
|
|
cs := gtx.Constraints
|
|
mainMin, _ := axisMainConstraint(f.Axis, cs)
|
|
var space int
|
|
if mainMin > size {
|
|
space = mainMin - size
|
|
}
|
|
var mainSize 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.Size.Y - dims.Baseline
|
|
var cross int
|
|
switch f.Alignment {
|
|
case End:
|
|
cross = maxCross - axisCross(f.Axis, dims.Size)
|
|
case Middle:
|
|
cross = (maxCross - axisCross(f.Axis, dims.Size)) / 2
|
|
case Baseline:
|
|
if f.Axis == Horizontal {
|
|
cross = maxBaseline - b
|
|
}
|
|
}
|
|
var stack op.StackOp
|
|
stack.Push(gtx.Ops)
|
|
op.TransformOp{}.Offset(FPt(axisPoint(f.Axis, mainSize, cross))).Add(gtx.Ops)
|
|
child.macro.Add()
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
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, maxCross)
|
|
return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline}
|
|
}
|
|
|
|
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) (int, int) {
|
|
if a == Horizontal {
|
|
return cs.Min.X, cs.Max.X
|
|
} else {
|
|
return cs.Min.Y, cs.Max.Y
|
|
}
|
|
}
|
|
|
|
func axisCrossConstraint(a Axis, cs Constraints) (int, int) {
|
|
if a == Horizontal {
|
|
return cs.Min.Y, cs.Max.Y
|
|
} else {
|
|
return cs.Min.X, cs.Max.X
|
|
}
|
|
}
|
|
|
|
func axisConstraints(a Axis, mainMin, mainMax, crossMin, crossMax int) Constraints {
|
|
if a == Horizontal {
|
|
return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
|
|
} else {
|
|
return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
|
|
}
|
|
}
|
|
|
|
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")
|
|
}
|
|
}
|