gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/layout/layout.go (about)

     1  // SPDX-License-Identifier: Unlicense OR MIT
     2  
     3  package layout
     4  
     5  import (
     6  	"image"
     7  
     8  	"gioui.org/f32"
     9  	"gioui.org/op"
    10  	"gioui.org/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  // Constrain a size so each dimension is in the range [min;max].
    89  func (c Constraints) Constrain(size image.Point) image.Point {
    90  	if min := c.Min.X; size.X < min {
    91  		size.X = min
    92  	}
    93  	if min := c.Min.Y; size.Y < min {
    94  		size.Y = min
    95  	}
    96  	if max := c.Max.X; size.X > max {
    97  		size.X = max
    98  	}
    99  	if max := c.Max.Y; size.Y > max {
   100  		size.Y = max
   101  	}
   102  	return size
   103  }
   104  
   105  // AddMin returns a copy of Constraints with the Min constraint enlarged by up to delta
   106  // while still fitting within the Max constraint. The Max is unchanged, and the Min constraint
   107  // will not go negative.
   108  func (c Constraints) AddMin(delta image.Point) Constraints {
   109  	c.Min = c.Min.Add(delta)
   110  	if c.Min.X < 0 {
   111  		c.Min.X = 0
   112  	}
   113  	if c.Min.Y < 0 {
   114  		c.Min.Y = 0
   115  	}
   116  	c.Min = c.Constrain(c.Min)
   117  	return c
   118  }
   119  
   120  // SubMax returns a copy of Constraints with the Max constraint shrunk by up to delta
   121  // while not going negative. The values of delta are expected to be positive.
   122  // The Min constraint is adjusted to fit within the new Max constraint.
   123  func (c Constraints) SubMax(delta image.Point) Constraints {
   124  	c.Max = c.Max.Sub(delta)
   125  	if c.Max.X < 0 {
   126  		c.Max.X = 0
   127  	}
   128  	if c.Max.Y < 0 {
   129  		c.Max.Y = 0
   130  	}
   131  	c.Min = c.Constrain(c.Min)
   132  	return c
   133  }
   134  
   135  // Inset adds space around a widget by decreasing its maximum
   136  // constraints. The minimum constraints will be adjusted to ensure
   137  // they do not exceed the maximum.
   138  type Inset struct {
   139  	Top, Bottom, Left, Right unit.Dp
   140  }
   141  
   142  // Layout a widget.
   143  func (in Inset) Layout(gtx Context, w Widget) Dimensions {
   144  	top := gtx.Dp(in.Top)
   145  	right := gtx.Dp(in.Right)
   146  	bottom := gtx.Dp(in.Bottom)
   147  	left := gtx.Dp(in.Left)
   148  	mcs := gtx.Constraints
   149  	mcs.Max.X -= left + right
   150  	if mcs.Max.X < 0 {
   151  		left = 0
   152  		right = 0
   153  		mcs.Max.X = 0
   154  	}
   155  	if mcs.Min.X > mcs.Max.X {
   156  		mcs.Min.X = mcs.Max.X
   157  	}
   158  	mcs.Max.Y -= top + bottom
   159  	if mcs.Max.Y < 0 {
   160  		bottom = 0
   161  		top = 0
   162  		mcs.Max.Y = 0
   163  	}
   164  	if mcs.Min.Y > mcs.Max.Y {
   165  		mcs.Min.Y = mcs.Max.Y
   166  	}
   167  	gtx.Constraints = mcs
   168  	trans := op.Offset(image.Pt(left, top)).Push(gtx.Ops)
   169  	dims := w(gtx)
   170  	trans.Pop()
   171  	return Dimensions{
   172  		Size:     dims.Size.Add(image.Point{X: right + left, Y: top + bottom}),
   173  		Baseline: dims.Baseline + bottom,
   174  	}
   175  }
   176  
   177  // UniformInset returns an Inset with a single inset applied to all
   178  // edges.
   179  func UniformInset(v unit.Dp) Inset {
   180  	return Inset{Top: v, Right: v, Bottom: v, Left: v}
   181  }
   182  
   183  // Layout a widget according to the direction.
   184  // The widget is called with the context constraints minimum cleared.
   185  func (d Direction) Layout(gtx Context, w Widget) Dimensions {
   186  	macro := op.Record(gtx.Ops)
   187  	csn := gtx.Constraints.Min
   188  	switch d {
   189  	case N, S:
   190  		gtx.Constraints.Min.Y = 0
   191  	case E, W:
   192  		gtx.Constraints.Min.X = 0
   193  	default:
   194  		gtx.Constraints.Min = image.Point{}
   195  	}
   196  	dims := w(gtx)
   197  	call := macro.Stop()
   198  	sz := dims.Size
   199  	if sz.X < csn.X {
   200  		sz.X = csn.X
   201  	}
   202  	if sz.Y < csn.Y {
   203  		sz.Y = csn.Y
   204  	}
   205  
   206  	p := d.Position(dims.Size, sz)
   207  	defer op.Offset(p).Push(gtx.Ops).Pop()
   208  	call.Add(gtx.Ops)
   209  
   210  	return Dimensions{
   211  		Size:     sz,
   212  		Baseline: dims.Baseline + sz.Y - dims.Size.Y - p.Y,
   213  	}
   214  }
   215  
   216  // Position calculates widget position according to the direction.
   217  func (d Direction) Position(widget, bounds image.Point) image.Point {
   218  	var p image.Point
   219  
   220  	switch d {
   221  	case N, S, Center:
   222  		p.X = (bounds.X - widget.X) / 2
   223  	case NE, SE, E:
   224  		p.X = bounds.X - widget.X
   225  	}
   226  
   227  	switch d {
   228  	case W, Center, E:
   229  		p.Y = (bounds.Y - widget.Y) / 2
   230  	case SW, S, SE:
   231  		p.Y = bounds.Y - widget.Y
   232  	}
   233  
   234  	return p
   235  }
   236  
   237  // Spacer adds space between widgets.
   238  type Spacer struct {
   239  	Width, Height unit.Dp
   240  }
   241  
   242  func (s Spacer) Layout(gtx Context) Dimensions {
   243  	return Dimensions{
   244  		Size: gtx.Constraints.Constrain(image.Point{
   245  			X: gtx.Dp(s.Width),
   246  			Y: gtx.Dp(s.Height),
   247  		}),
   248  	}
   249  }
   250  
   251  func (a Alignment) String() string {
   252  	switch a {
   253  	case Start:
   254  		return "Start"
   255  	case End:
   256  		return "End"
   257  	case Middle:
   258  		return "Middle"
   259  	case Baseline:
   260  		return "Baseline"
   261  	default:
   262  		panic("unreachable")
   263  	}
   264  }
   265  
   266  // Convert a point in (x, y) coordinates to (main, cross) coordinates,
   267  // or vice versa. Specifically, Convert((x, y)) returns (x, y) unchanged
   268  // for the horizontal axis, or (y, x) for the vertical axis.
   269  func (a Axis) Convert(pt image.Point) image.Point {
   270  	if a == Horizontal {
   271  		return pt
   272  	}
   273  	return image.Pt(pt.Y, pt.X)
   274  }
   275  
   276  // FConvert a point in (x, y) coordinates to (main, cross) coordinates,
   277  // or vice versa. Specifically, FConvert((x, y)) returns (x, y) unchanged
   278  // for the horizontal axis, or (y, x) for the vertical axis.
   279  func (a Axis) FConvert(pt f32.Point) f32.Point {
   280  	if a == Horizontal {
   281  		return pt
   282  	}
   283  	return f32.Pt(pt.Y, pt.X)
   284  }
   285  
   286  // mainConstraint returns the min and max main constraints for axis a.
   287  func (a Axis) mainConstraint(cs Constraints) (int, int) {
   288  	if a == Horizontal {
   289  		return cs.Min.X, cs.Max.X
   290  	}
   291  	return cs.Min.Y, cs.Max.Y
   292  }
   293  
   294  // crossConstraint returns the min and max cross constraints for axis a.
   295  func (a Axis) crossConstraint(cs Constraints) (int, int) {
   296  	if a == Horizontal {
   297  		return cs.Min.Y, cs.Max.Y
   298  	}
   299  	return cs.Min.X, cs.Max.X
   300  }
   301  
   302  // constraints returns the constraints for axis a.
   303  func (a Axis) constraints(mainMin, mainMax, crossMin, crossMax int) Constraints {
   304  	if a == Horizontal {
   305  		return Constraints{Min: image.Pt(mainMin, crossMin), Max: image.Pt(mainMax, crossMax)}
   306  	}
   307  	return Constraints{Min: image.Pt(crossMin, mainMin), Max: image.Pt(crossMax, mainMax)}
   308  }
   309  
   310  func (a Axis) String() string {
   311  	switch a {
   312  	case Horizontal:
   313  		return "Horizontal"
   314  	case Vertical:
   315  		return "Vertical"
   316  	default:
   317  		panic("unreachable")
   318  	}
   319  }
   320  
   321  func (d Direction) String() string {
   322  	switch d {
   323  	case NW:
   324  		return "NW"
   325  	case N:
   326  		return "N"
   327  	case NE:
   328  		return "NE"
   329  	case E:
   330  		return "E"
   331  	case SE:
   332  		return "SE"
   333  	case S:
   334  		return "S"
   335  	case SW:
   336  		return "SW"
   337  	case W:
   338  		return "W"
   339  	case Center:
   340  		return "Center"
   341  	default:
   342  		panic("unreachable")
   343  	}
   344  }