345 lines
7.5 KiB
Go
345 lines
7.5 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package layout
|
|
|
|
import (
|
|
"image"
|
|
|
|
"github.com/mleku/gio/f32"
|
|
"github.com/mleku/gio/op"
|
|
"github.com/mleku/gio/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")
|
|
}
|
|
}
|