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