// 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") } }