mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
29639565cd
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>
296 lines
6.6 KiB
Go
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")
|
|
}
|
|
}
|