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 }