gioui.org/ui@v0.0.0-20190926171558-ce74bc0cbaea/layout/flex.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package layout 4 5 import ( 6 "image" 7 8 "gioui.org/ui" 9 "gioui.org/ui/f32" 10 ) 11 12 // Flex lays out child elements along an axis, 13 // according to alignment and weights. 14 type Flex struct { 15 // Axis is the main axis, either Horizontal or Vertical. 16 Axis Axis 17 // Spacing controls the distribution of space left after 18 // layout. 19 Spacing Spacing 20 // Alignment is the alignment in the cross axis. 21 Alignment Alignment 22 23 ctx *Context 24 macro ui.MacroOp 25 mode flexMode 26 size int 27 rigidSize int 28 // fraction is the rounding error from a Flexible weighting. 29 fraction float32 30 maxCross int 31 maxBaseline int 32 } 33 34 // FlexChild is the layout result of a call End. 35 type FlexChild struct { 36 macro ui.MacroOp 37 dims Dimensions 38 } 39 40 // Spacing determine the spacing mode for a Flex. 41 type Spacing uint8 42 43 type flexMode uint8 44 45 const ( 46 // SpaceEnd leaves space at the end. 47 SpaceEnd Spacing = iota 48 // SpaceStart leaves space at the start. 49 SpaceStart 50 // SpaceSides shares space between the start and end. 51 SpaceSides 52 // SpaceAround distributes space evenly between children, 53 // with half as much space at the start and end. 54 SpaceAround 55 // SpaceBetween distributes space evenly between children, 56 // leaving no space at the start and end. 57 SpaceBetween 58 // SpaceEvenly distributes space evenly between children and 59 // at the start and end. 60 SpaceEvenly 61 ) 62 63 const ( 64 modeNone flexMode = iota 65 modeBegun 66 modeRigid 67 modeFlex 68 ) 69 70 // Init must be called before Rigid or Flexible. 71 func (f *Flex) Init(gtx *Context) *Flex { 72 if f.mode > modeBegun { 73 panic("must End the current child before calling Init again") 74 } 75 f.mode = modeBegun 76 f.ctx = gtx 77 f.size = 0 78 f.rigidSize = 0 79 f.maxCross = 0 80 f.maxBaseline = 0 81 return f 82 } 83 84 func (f *Flex) begin(mode flexMode) { 85 switch { 86 case f.mode == modeNone: 87 panic("must Init before adding a child") 88 case f.mode > modeBegun: 89 panic("must End before adding a child") 90 } 91 f.mode = mode 92 f.macro.Record(f.ctx.Ops) 93 } 94 95 // Rigid lays out a widget with the main axis constrained to the range 96 // from 0 to the remaining space. 97 func (f *Flex) Rigid(w Widget) FlexChild { 98 f.begin(modeRigid) 99 cs := f.ctx.Constraints 100 mainc := axisMainConstraint(f.Axis, cs) 101 mainMax := mainc.Max - f.size 102 if mainMax < 0 { 103 mainMax = 0 104 } 105 cs = axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, cs)) 106 dims := f.ctx.Layout(cs, w) 107 return f.end(dims) 108 } 109 110 // Flexible is like Rigid, where the main axis size is also constrained to a 111 // fraction of the space not taken up by Rigid children. 112 func (f *Flex) Flexible(weight float32, w Widget) FlexChild { 113 f.begin(modeFlex) 114 cs := f.ctx.Constraints 115 mainc := axisMainConstraint(f.Axis, cs) 116 var flexSize int 117 if mainc.Max > f.size { 118 flexSize = mainc.Max - f.rigidSize 119 // Apply weight and add any leftover fraction from a 120 // previous Flexible. 121 size := float32(flexSize)*weight + f.fraction 122 flexSize = int(size + .5) 123 f.fraction = size - float32(flexSize) 124 if max := mainc.Max - f.size; flexSize > max { 125 flexSize = max 126 } 127 } 128 submainc := Constraint{Max: flexSize} 129 cs = axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, cs)) 130 dims := f.ctx.Layout(cs, w) 131 return f.end(dims) 132 } 133 134 // End a child by specifying its dimensions. Pass the returned layout result 135 // to Layout. 136 func (f *Flex) end(dims Dimensions) FlexChild { 137 if f.mode <= modeBegun { 138 panic("End called without an active child") 139 } 140 f.macro.Stop() 141 sz := axisMain(f.Axis, dims.Size) 142 f.size += sz 143 if f.mode == modeRigid { 144 f.rigidSize += sz 145 } 146 f.mode = modeBegun 147 if c := axisCross(f.Axis, dims.Size); c > f.maxCross { 148 f.maxCross = c 149 } 150 if b := dims.Baseline; b > f.maxBaseline { 151 f.maxBaseline = b 152 } 153 return FlexChild{f.macro, dims} 154 } 155 156 // Layout a list of children. The order of the children determines their laid 157 // out order. 158 func (f *Flex) Layout(children ...FlexChild) { 159 cs := f.ctx.Constraints 160 mainc := axisMainConstraint(f.Axis, cs) 161 crossSize := axisCrossConstraint(f.Axis, cs).Constrain(f.maxCross) 162 var space int 163 if mainc.Min > f.size { 164 space = mainc.Min - f.size 165 } 166 var mainSize int 167 var baseline int 168 switch f.Spacing { 169 case SpaceSides: 170 mainSize += space / 2 171 case SpaceStart: 172 mainSize += space 173 case SpaceEvenly: 174 mainSize += space / (1 + len(children)) 175 case SpaceAround: 176 mainSize += space / (len(children) * 2) 177 } 178 for i, child := range children { 179 dims := child.dims 180 b := dims.Baseline 181 var cross int 182 switch f.Alignment { 183 case End: 184 cross = crossSize - axisCross(f.Axis, dims.Size) 185 case Middle: 186 cross = (crossSize - axisCross(f.Axis, dims.Size)) / 2 187 case Baseline: 188 if f.Axis == Horizontal { 189 cross = f.maxBaseline - b 190 } 191 } 192 var stack ui.StackOp 193 stack.Push(f.ctx.Ops) 194 ui.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(f.ctx.Ops) 195 child.macro.Add(f.ctx.Ops) 196 stack.Pop() 197 mainSize += axisMain(f.Axis, dims.Size) 198 if i < len(children)-1 { 199 switch f.Spacing { 200 case SpaceEvenly: 201 mainSize += space / (1 + len(children)) 202 case SpaceAround: 203 mainSize += space / len(children) 204 case SpaceBetween: 205 mainSize += space / (len(children) - 1) 206 } 207 } 208 if b != dims.Size.Y { 209 baseline = b 210 } 211 } 212 switch f.Spacing { 213 case SpaceSides: 214 mainSize += space / 2 215 case SpaceEnd: 216 mainSize += space 217 case SpaceEvenly: 218 mainSize += space / (1 + len(children)) 219 case SpaceAround: 220 mainSize += space / (len(children) * 2) 221 } 222 sz := axisPoint(f.Axis, mainSize, crossSize) 223 if baseline == 0 { 224 baseline = sz.Y 225 } 226 f.ctx.Dimensions = Dimensions{Size: sz, Baseline: baseline} 227 } 228 229 func axisPoint(a Axis, main, cross int) image.Point { 230 if a == Horizontal { 231 return image.Point{main, cross} 232 } else { 233 return image.Point{cross, main} 234 } 235 } 236 237 func axisMain(a Axis, sz image.Point) int { 238 if a == Horizontal { 239 return sz.X 240 } else { 241 return sz.Y 242 } 243 } 244 245 func axisCross(a Axis, sz image.Point) int { 246 if a == Horizontal { 247 return sz.Y 248 } else { 249 return sz.X 250 } 251 } 252 253 func axisMainConstraint(a Axis, cs Constraints) Constraint { 254 if a == Horizontal { 255 return cs.Width 256 } else { 257 return cs.Height 258 } 259 } 260 261 func axisCrossConstraint(a Axis, cs Constraints) Constraint { 262 if a == Horizontal { 263 return cs.Height 264 } else { 265 return cs.Width 266 } 267 } 268 269 func axisConstraints(a Axis, mainc, crossc Constraint) Constraints { 270 if a == Horizontal { 271 return Constraints{Width: mainc, Height: crossc} 272 } else { 273 return Constraints{Width: crossc, Height: mainc} 274 } 275 } 276 277 func toPointF(p image.Point) f32.Point { 278 return f32.Point{X: float32(p.X), Y: float32(p.Y)} 279 } 280 281 func (s Spacing) String() string { 282 switch s { 283 case SpaceEnd: 284 return "SpaceEnd" 285 case SpaceStart: 286 return "SpaceStart" 287 case SpaceSides: 288 return "SpaceSides" 289 case SpaceAround: 290 return "SpaceAround" 291 case SpaceBetween: 292 return "SpaceAround" 293 case SpaceEvenly: 294 return "SpaceEvenly" 295 default: 296 panic("unreachable") 297 } 298 }