mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
2782436ffc
Almost every layout and widget need the ui.Config for its environment, an ui.Ops to store operations. Stateful widgets need an input.Queue for events. Add all these common objects to Context, greatly simplifying the function signatures for Gio programs. Fixes gio#33 Signed-off-by: Elias Naur <mail@eliasnaur.com>
299 lines
6.7 KiB
Go
299 lines
6.7 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
|
|
|
|
ctx *Context
|
|
macro ui.MacroOp
|
|
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(c *Context) *Flex {
|
|
if f.mode > modeBegun {
|
|
panic("must End the current child before calling Init again")
|
|
}
|
|
f.mode = modeBegun
|
|
f.ctx = c
|
|
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.ctx.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)
|
|
cs := f.ctx.Constraints
|
|
mainc := axisMainConstraint(f.Axis, cs)
|
|
mainMax := mainc.Max - f.size
|
|
if mainMax < 0 {
|
|
mainMax = 0
|
|
}
|
|
cs = axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, cs))
|
|
dims := f.ctx.Layout(cs, w)
|
|
return f.end(dims)
|
|
}
|
|
|
|
// 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)
|
|
cs := f.ctx.Constraints
|
|
mainc := axisMainConstraint(f.Axis, 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, cs))
|
|
dims := f.ctx.Layout(cs, w)
|
|
return f.end(dims)
|
|
}
|
|
|
|
// 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) {
|
|
cs := f.ctx.Constraints
|
|
mainc := axisMainConstraint(f.Axis, cs)
|
|
crossSize := axisCrossConstraint(f.Axis, 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.ctx.Ops)
|
|
ui.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(f.ctx.Ops)
|
|
child.macro.Add(f.ctx.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
|
|
}
|
|
f.ctx.Dimensions = 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")
|
|
}
|
|
}
|