layout.go raw

   1  // SPDX-License-Identifier: Unlicense OR MIT
   2  
   3  package layout
   4  
   5  import (
   6  	"image"
   7  
   8  	"github.com/p9c/p9/pkg/gel/gio/f32"
   9  	"github.com/p9c/p9/pkg/gel/gio/op"
  10  	"github.com/p9c/p9/pkg/gel/gio/unit"
  11  )
  12  
  13  // Constraints represent the minimum and maximum size of a widget.
  14  //
  15  // A widget does not have to treat its constraints as "hard". For
  16  // example, if it's passed a constraint with a minimum size that's
  17  // smaller than its actual minimum size, it should return its minimum
  18  // size dimensions instead. Parent widgets should deal appropriately
  19  // with child widgets that return dimensions that do not fit their
  20  // constraints (for example, by clipping).
  21  type Constraints struct {
  22  	Min, Max image.Point
  23  }
  24  
  25  // Dimensions are the resolved size and baseline for a widget.
  26  //
  27  // Baseline is the distance from the bottom of a widget to the baseline of
  28  // any text it contains (or 0). The purpose is to be able to align text
  29  // that span multiple widgets.
  30  type Dimensions struct {
  31  	Size     image.Point
  32  	Baseline int
  33  }
  34  
  35  // Axis is the Horizontal or Vertical direction.
  36  type Axis uint8
  37  
  38  // Alignment is the mutual alignment of a list of widgets.
  39  type Alignment uint8
  40  
  41  // Direction is the alignment of widgets relative to a containing
  42  // space.
  43  type Direction uint8
  44  
  45  // Widget is a function scope for drawing, processing events and
  46  // computing dimensions for a user interface element.
  47  type Widget func(gtx Context) Dimensions
  48  
  49  const (
  50  	Start Alignment = iota
  51  	End
  52  	Middle
  53  	Baseline
  54  )
  55  
  56  const (
  57  	NW Direction = iota
  58  	N
  59  	NE
  60  	E
  61  	SE
  62  	S
  63  	SW
  64  	W
  65  	Center
  66  )
  67  
  68  const (
  69  	Horizontal Axis = iota
  70  	Vertical
  71  )
  72  
  73  // Exact returns the Constraints with the minimum and maximum size
  74  // set to size.
  75  func Exact(size image.Point) Constraints {
  76  	return Constraints{
  77  		Min: size, Max: size,
  78  	}
  79  }
  80  
  81  // FPt converts an point to a f32.Point.
  82  func FPt(p image.Point) f32.Point {
  83  	return f32.Point{
  84  		X: float32(p.X), Y: float32(p.Y),
  85  	}
  86  }
  87  
  88  // FRect converts a rectangle to a f32.Rectangle.
  89  func FRect(r image.Rectangle) f32.Rectangle {
  90  	return f32.Rectangle{
  91  		Min: FPt(r.Min), Max: FPt(r.Max),
  92  	}
  93  }
  94  
  95  // Constrain a size so each dimension is in the range [min;max].
  96  func (c Constraints) Constrain(size image.Point) image.Point {
  97  	if min := c.Min.X; size.X < min {
  98  		size.X = min
  99  	}
 100  	if min := c.Min.Y; size.Y < min {
 101  		size.Y = min
 102  	}
 103  	if max := c.Max.X; size.X > max {
 104  		size.X = max
 105  	}
 106  	if max := c.Max.Y; size.Y > max {
 107  		size.Y = max
 108  	}
 109  	return size
 110  }
 111  
 112  // Inset adds space around a widget by decreasing its maximum
 113  // constraints. The minimum constraints will be adjusted to ensure
 114  // they do not exceed the maximum.
 115  type Inset struct {
 116  	Top, Right, Bottom, Left unit.Value
 117  }
 118  
 119  // Layout a widget.
 120  func (in Inset) Layout(gtx Context, w Widget) Dimensions {
 121  	top := gtx.Px(in.Top)
 122  	right := gtx.Px(in.Right)
 123  	bottom := gtx.Px(in.Bottom)
 124  	left := gtx.Px(in.Left)
 125  	mcs := gtx.Constraints
 126  	mcs.Max.X -= left + right
 127  	if mcs.Max.X < 0 {
 128  		left = 0
 129  		right = 0
 130  		mcs.Max.X = 0
 131  	}
 132  	if mcs.Min.X > mcs.Max.X {
 133  		mcs.Min.X = mcs.Max.X
 134  	}
 135  	mcs.Max.Y -= top + bottom
 136  	if mcs.Max.Y < 0 {
 137  		bottom = 0
 138  		top = 0
 139  		mcs.Max.Y = 0
 140  	}
 141  	if mcs.Min.Y > mcs.Max.Y {
 142  		mcs.Min.Y = mcs.Max.Y
 143  	}
 144  	stack := op.Save(gtx.Ops)
 145  	op.Offset(FPt(image.Point{X: left, Y: top})).Add(gtx.Ops)
 146  	gtx.Constraints = mcs
 147  	dims := w(gtx)
 148  	stack.Load()
 149  	return Dimensions{
 150  		Size:     dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
 151  		Baseline: dims.Baseline + bottom,
 152  	}
 153  }
 154  
 155  // UniformInset returns an Inset with a single inset applied to all
 156  // edges.
 157  func UniformInset(v unit.Value) Inset {
 158  	return Inset{Top: v, Right: v, Bottom: v, Left: v}
 159  }
 160  
 161  // Layout a widget according to the direction.
 162  // The widget is called with the context constraints minimum cleared.
 163  func (d Direction) Layout(gtx Context, w Widget) Dimensions {
 164  	macro := op.Record(gtx.Ops)
 165  	cs := gtx.Constraints
 166  	gtx.Constraints.Min = image.Point{}
 167  	dims := w(gtx)
 168  	call := macro.Stop()
 169  	sz := dims.Size
 170  	if sz.X < cs.Min.X {
 171  		sz.X = cs.Min.X
 172  	}
 173  	if sz.Y < cs.Min.Y {
 174  		sz.Y = cs.Min.Y
 175  	}
 176  
 177  	defer op.Save(gtx.Ops).Load()
 178  	p := d.Position(dims.Size, sz)
 179  	op.Offset(FPt(p)).Add(gtx.Ops)
 180  	call.Add(gtx.Ops)
 181  
 182  	return Dimensions{
 183  		Size:     sz,
 184  		Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
 185  	}
 186  }
 187  
 188  // Position calculates widget position according to the direction.
 189  func (d Direction) Position(widget, bounds image.Point) image.Point {
 190  	var p image.Point
 191  
 192  	switch d {
 193  	case N, S, Center:
 194  		p.X = (bounds.X - widget.X) / 2
 195  	case NE, SE, E:
 196  		p.X = bounds.X - widget.X
 197  	}
 198  
 199  	switch d {
 200  	case W, Center, E:
 201  		p.Y = (bounds.Y - widget.Y) / 2
 202  	case SW, S, SE:
 203  		p.Y = bounds.Y - widget.Y
 204  	}
 205  
 206  	return p
 207  }
 208  
 209  // Spacer adds space between widgets.
 210  type Spacer struct {
 211  	Width, Height unit.Value
 212  }
 213  
 214  func (s Spacer) Layout(gtx Context) Dimensions {
 215  	return Dimensions{
 216  		Size: image.Point{
 217  			X: gtx.Px(s.Width),
 218  			Y: gtx.Px(s.Height),
 219  		},
 220  	}
 221  }
 222  
 223  func (a Alignment) String() string {
 224  	switch a {
 225  	case Start:
 226  		return "Start"
 227  	case End:
 228  		return "End"
 229  	case Middle:
 230  		return "Middle"
 231  	case Baseline:
 232  		return "Baseline"
 233  	default:
 234  		panic("unreachable")
 235  	}
 236  }
 237  
 238  // Convert a point in (x, y) coordinates to (main, cross) coordinates,
 239  // or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
 240  // for the horizontal axis, or (y, x) for the vertical axis.
 241  func (a Axis) Convert(pt image.Point) image.Point {
 242  	if a == Horizontal {
 243  		return pt
 244  	}
 245  	return image.Pt(pt.Y, pt.X)
 246  }
 247  
 248  // mainConstraint returns the min and max main constraints for axis a.
 249  func (a Axis) mainConstraint(cs Constraints) (int, int) {
 250  	if a == Horizontal {
 251  		return cs.Min.X, cs.Max.X
 252  	}
 253  	return cs.Min.Y, cs.Max.Y
 254  }
 255  
 256  // crossConstraint returns the min and max cross constraints for axis a.
 257  func (a Axis) crossConstraint(cs Constraints) (int, int) {
 258  	if a == Horizontal {
 259  		return cs.Min.Y, cs.Max.Y
 260  	}
 261  	return cs.Min.X, cs.Max.X
 262  }
 263  
 264  // constraints returns the constraints for axis a.
 265  func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
 266  	if a == Horizontal {
 267  		return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
 268  	}
 269  	return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
 270  }
 271  
 272  func (a Axis) String() string {
 273  	switch a {
 274  	case Horizontal:
 275  		return "Horizontal"
 276  	case Vertical:
 277  		return "Vertical"
 278  	default:
 279  		panic("unreachable")
 280  	}
 281  }
 282  
 283  func (d Direction) String() string {
 284  	switch d {
 285  	case NW:
 286  		return "NW"
 287  	case N:
 288  		return "N"
 289  	case NE:
 290  		return "NE"
 291  	case E:
 292  		return "E"
 293  	case SE:
 294  		return "SE"
 295  	case S:
 296  		return "S"
 297  	case SW:
 298  		return "SW"
 299  	case W:
 300  		return "W"
 301  	case Center:
 302  		return "Center"
 303  	default:
 304  		panic("unreachable")
 305  	}
 306  }
 307