Files
gio/layout/layout.go
T
Chris Waldon 59695984e5 layout: add resize helpers for constraints
This commit adds two helper methods to layout.Contraints that make it easier to
manipulate the constraints while keeping their invariants. In particular, code
manually manipulating constraints usually fails to correctly ensure that the
max does not become smaller than the min, the min does not exceed the max, and
that no value goes below zero.

It's quite a few lines to check these invariants yourself in every custom layout,
so I think it makes sense to offer helpers for this.

Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
2023-05-02 12:33:30 -06:00

345 lines
7.4 KiB
Go

// SPDX-License-Identifier: Unlicense OR MIT
package layout
import (
"image"
"gioui.org/f32"
"gioui.org/op"
"gioui.org/unit"
)
// Constraints represent the minimum and maximum size of a widget.
//
// A widget does not have to treat its constraints as "hard". For
// example, if it's passed a constraint with a minimum size that's
// smaller than its actual minimum size, it should return its minimum
// size dimensions instead. Parent widgets should deal appropriately
// with child widgets that return dimensions that do not fit their
// constraints (for example, by clipping).
type Constraints struct {
Min, Max image.Point
}
// Dimensions are the resolved size and baseline for a widget.
//
// Baseline is the distance from the bottom of a widget to the baseline of
// any text it contains (or 0). The purpose is to be able to align text
// that span multiple widgets.
type Dimensions struct {
Size image.Point
Baseline int
}
// Axis is the Horizontal or Vertical direction.
type Axis uint8
// Alignment is the mutual alignment of a list of widgets.
type Alignment uint8
// Direction is the alignment of widgets relative to a containing
// space.
type Direction uint8
// Widget is a function scope for drawing, processing events and
// computing dimensions for a user interface element.
type Widget func(gtx Context) Dimensions
const (
Start Alignment = iota
End
Middle
Baseline
)
const (
NW Direction = iota
N
NE
E
SE
S
SW
W
Center
)
const (
Horizontal Axis = iota
Vertical
)
// Exact returns the Constraints with the minimum and maximum size
// set to size.
func Exact(size image.Point) Constraints {
return Constraints{
Min: size, Max: size,
}
}
// FPt converts an point to a f32.Point.
func FPt(p image.Point) f32.Point {
return f32.Point{
X: float32(p.X), Y: float32(p.Y),
}
}
// Constrain a size so each dimension is in the range [min;max].
func (c Constraints) Constrain(size image.Point) image.Point {
if min := c.Min.X; size.X < min {
size.X = min
}
if min := c.Min.Y; size.Y < min {
size.Y = min
}
if max := c.Max.X; size.X > max {
size.X = max
}
if max := c.Max.Y; size.Y > max {
size.Y = max
}
return size
}
// AddMin returns a copy of Constraints with the Min constraint enlarged by up to delta
// while still fitting within the Max constraint. The Max is unchanged, and the Min constraint
// will not go negative.
func (c Constraints) AddMin(delta image.Point) Constraints {
c.Min = c.Min.Add(delta)
if c.Min.X < 0 {
c.Min.X = 0
}
if c.Min.Y < 0 {
c.Min.Y = 0
}
c.Min = c.Constrain(c.Min)
return c
}
// SubMax returns a copy of Constraints with the Max constraint shrunk by up to delta
// while not going negative. The values of delta are expected to be positive.
// The Min constraint is adjusted to fit within the new Max constraint.
func (c Constraints) SubMax(delta image.Point) Constraints {
c.Max = c.Max.Sub(delta)
if c.Max.X < 0 {
c.Max.X = 0
}
if c.Max.Y < 0 {
c.Max.Y = 0
}
c.Min = c.Constrain(c.Min)
return c
}
// Inset adds space around a widget by decreasing its maximum
// constraints. The minimum constraints will be adjusted to ensure
// they do not exceed the maximum.
type Inset struct {
Top, Bottom, Left, Right unit.Dp
}
// Layout a widget.
func (in Inset) Layout(gtx Context, w Widget) Dimensions {
top := gtx.Dp(in.Top)
right := gtx.Dp(in.Right)
bottom := gtx.Dp(in.Bottom)
left := gtx.Dp(in.Left)
mcs := gtx.Constraints
mcs.Max.X -= left + right
if mcs.Max.X < 0 {
left = 0
right = 0
mcs.Max.X = 0
}
if mcs.Min.X > mcs.Max.X {
mcs.Min.X = mcs.Max.X
}
mcs.Max.Y -= top + bottom
if mcs.Max.Y < 0 {
bottom = 0
top = 0
mcs.Max.Y = 0
}
if mcs.Min.Y > mcs.Max.Y {
mcs.Min.Y = mcs.Max.Y
}
gtx.Constraints = mcs
trans := op.Offset(image.Pt(left, top)).Push(gtx.Ops)
dims := w(gtx)
trans.Pop()
return Dimensions{
Size: dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
Baseline: dims.Baseline + bottom,
}
}
// UniformInset returns an Inset with a single inset applied to all
// edges.
func UniformInset(v unit.Dp) Inset {
return Inset{Top: v, Right: v, Bottom: v, Left: v}
}
// Layout a widget according to the direction.
// The widget is called with the context constraints minimum cleared.
func (d Direction) Layout(gtx Context, w Widget) Dimensions {
macro := op.Record(gtx.Ops)
csn := gtx.Constraints.Min
switch d {
case N, S:
gtx.Constraints.Min.Y = 0
case E, W:
gtx.Constraints.Min.X = 0
default:
gtx.Constraints.Min = image.Point{}
}
dims := w(gtx)
call := macro.Stop()
sz := dims.Size
if sz.X < csn.X {
sz.X = csn.X
}
if sz.Y < csn.Y {
sz.Y = csn.Y
}
p := d.Position(dims.Size, sz)
defer op.Offset(p).Push(gtx.Ops).Pop()
call.Add(gtx.Ops)
return Dimensions{
Size: sz,
Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
}
}
// Position calculates widget position according to the direction.
func (d Direction) Position(widget, bounds image.Point) image.Point {
var p image.Point
switch d {
case N, S, Center:
p.X = (bounds.X - widget.X) / 2
case NE, SE, E:
p.X = bounds.X - widget.X
}
switch d {
case W, Center, E:
p.Y = (bounds.Y - widget.Y) / 2
case SW, S, SE:
p.Y = bounds.Y - widget.Y
}
return p
}
// Spacer adds space between widgets.
type Spacer struct {
Width, Height unit.Dp
}
func (s Spacer) Layout(gtx Context) Dimensions {
return Dimensions{
Size: gtx.Constraints.Constrain(image.Point{
X: gtx.Dp(s.Width),
Y: gtx.Dp(s.Height),
}),
}
}
func (a Alignment) String() string {
switch a {
case Start:
return "Start"
case End:
return "End"
case Middle:
return "Middle"
case Baseline:
return "Baseline"
default:
panic("unreachable")
}
}
// Convert a point in (x, y) coordinates to (main, cross) coordinates,
// or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
// for the horizontal axis, or (y, x) for the vertical axis.
func (a Axis) Convert(pt image.Point) image.Point {
if a == Horizontal {
return pt
}
return image.Pt(pt.Y, pt.X)
}
// FConvert a point in (x, y) coordinates to (main, cross) coordinates,
// or vice versa. Specifically, FConvert((x, y)) returns (x, y) unchanged
// for the horizontal axis, or (y, x) for the vertical axis.
func (a Axis) FConvert(pt f32.Point) f32.Point {
if a == Horizontal {
return pt
}
return f32.Pt(pt.Y, pt.X)
}
// mainConstraint returns the min and max main constraints for axis a.
func (a Axis) mainConstraint(cs Constraints) (int, int) {
if a == Horizontal {
return cs.Min.X, cs.Max.X
}
return cs.Min.Y, cs.Max.Y
}
// crossConstraint returns the min and max cross constraints for axis a.
func (a Axis) crossConstraint(cs Constraints) (int, int) {
if a == Horizontal {
return cs.Min.Y, cs.Max.Y
}
return cs.Min.X, cs.Max.X
}
// constraints returns the constraints for axis a.
func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
if a == Horizontal {
return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
}
return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
}
func (a Axis) String() string {
switch a {
case Horizontal:
return "Horizontal"
case Vertical:
return "Vertical"
default:
panic("unreachable")
}
}
func (d Direction) String() string {
switch d {
case NW:
return "NW"
case N:
return "N"
case NE:
return "NE"
case E:
return "E"
case SE:
return "SE"
case S:
return "S"
case SW:
return "SW"
case W:
return "W"
case Center:
return "Center"
default:
panic("unreachable")
}
}