github.com/gop9/olt@v0.0.0-20200202132135-d956aad50b08/gio/layout/flex.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package layout 4 5 import ( 6 "image" 7 8 "github.com/gop9/olt/gio/f32" 9 "github.com/gop9/olt/gio/op" 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 24 // FlexChild is the descriptor for a Flex child. 25 type FlexChild struct { 26 flex bool 27 weight float32 28 29 widget Widget 30 31 // Scratch space. 32 macro op.MacroOp 33 dims Dimensions 34 } 35 36 // Spacing determine the spacing mode for a Flex. 37 type Spacing uint8 38 39 type flexMode uint8 40 41 const ( 42 // SpaceEnd leaves space at the end. 43 SpaceEnd Spacing = iota 44 // SpaceStart leaves space at the start. 45 SpaceStart 46 // SpaceSides shares space between the start and end. 47 SpaceSides 48 // SpaceAround distributes space evenly between children, 49 // with half as much space at the start and end. 50 SpaceAround 51 // SpaceBetween distributes space evenly between children, 52 // leaving no space at the start and end. 53 SpaceBetween 54 // SpaceEvenly distributes space evenly between children and 55 // at the start and end. 56 SpaceEvenly 57 ) 58 59 // Rigid returns a Flex child with a maximal constraint of the 60 // remaining space. 61 func Rigid(widget Widget) FlexChild { 62 return FlexChild{ 63 widget: widget, 64 } 65 } 66 67 // Flexed returns a Flex child forced to take up a fraction of 68 // the remaining space. 69 func Flexed(weight float32, widget Widget) FlexChild { 70 return FlexChild{ 71 flex: true, 72 weight: weight, 73 widget: widget, 74 } 75 } 76 77 // Layout a list of children. The position of the children are 78 // determined by the specified order, but Rigid children are laid out 79 // before Flexed children. 80 func (f Flex) Layout(gtx *Context, children ...FlexChild) { 81 size := 0 82 // Lay out Rigid children. 83 for i, child := range children { 84 if child.flex { 85 continue 86 } 87 cs := gtx.Constraints 88 mainc := axisMainConstraint(f.Axis, cs) 89 mainMax := mainc.Max - size 90 if mainMax < 0 { 91 mainMax = 0 92 } 93 cs = axisConstraints(f.Axis, Constraint{Max: mainMax}, axisCrossConstraint(f.Axis, cs)) 94 var m op.MacroOp 95 m.Record(gtx.Ops) 96 dims := ctxLayout(gtx, cs, child.widget) 97 m.Stop() 98 sz := axisMain(f.Axis, dims.Size) 99 size += sz 100 children[i].macro = m 101 children[i].dims = dims 102 } 103 rigidSize := size 104 // fraction is the rounding error from a Flex weighting. 105 var fraction float32 106 // Lay out Flexed children. 107 for i, child := range children { 108 if !child.flex { 109 continue 110 } 111 cs := gtx.Constraints 112 mainc := axisMainConstraint(f.Axis, cs) 113 var flexSize int 114 if mainc.Max > size { 115 flexSize = mainc.Max - rigidSize 116 // Apply weight and add any leftover fraction from a 117 // previous Flexed. 118 childSize := float32(flexSize)*child.weight + fraction 119 flexSize = int(childSize + .5) 120 fraction = childSize - float32(flexSize) 121 if max := mainc.Max - size; flexSize > max { 122 flexSize = max 123 } 124 } 125 submainc := Constraint{Min: flexSize, Max: flexSize} 126 cs = axisConstraints(f.Axis, submainc, axisCrossConstraint(f.Axis, cs)) 127 var m op.MacroOp 128 m.Record(gtx.Ops) 129 dims := ctxLayout(gtx, cs, child.widget) 130 m.Stop() 131 sz := axisMain(f.Axis, dims.Size) 132 size += sz 133 children[i].macro = m 134 children[i].dims = dims 135 } 136 var maxCross int 137 var maxBaseline int 138 for _, child := range children { 139 if c := axisCross(f.Axis, child.dims.Size); c > maxCross { 140 maxCross = c 141 } 142 if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline { 143 maxBaseline = b 144 } 145 } 146 cs := gtx.Constraints 147 mainc := axisMainConstraint(f.Axis, cs) 148 var space int 149 if mainc.Min > size { 150 space = mainc.Min - size 151 } 152 var mainSize int 153 switch f.Spacing { 154 case SpaceSides: 155 mainSize += space / 2 156 case SpaceStart: 157 mainSize += space 158 case SpaceEvenly: 159 mainSize += space / (1 + len(children)) 160 case SpaceAround: 161 mainSize += space / (len(children) * 2) 162 } 163 for i, child := range children { 164 dims := child.dims 165 b := dims.Size.Y - dims.Baseline 166 var cross int 167 switch f.Alignment { 168 case End: 169 cross = maxCross - axisCross(f.Axis, dims.Size) 170 case Middle: 171 cross = (maxCross - axisCross(f.Axis, dims.Size)) / 2 172 case Baseline: 173 if f.Axis == Horizontal { 174 cross = maxBaseline - b 175 } 176 } 177 var stack op.StackOp 178 stack.Push(gtx.Ops) 179 op.TransformOp{}.Offset(toPointF(axisPoint(f.Axis, mainSize, cross))).Add(gtx.Ops) 180 child.macro.Add() 181 stack.Pop() 182 mainSize += axisMain(f.Axis, dims.Size) 183 if i < len(children)-1 { 184 switch f.Spacing { 185 case SpaceEvenly: 186 mainSize += space / (1 + len(children)) 187 case SpaceAround: 188 mainSize += space / len(children) 189 case SpaceBetween: 190 mainSize += space / (len(children) - 1) 191 } 192 } 193 } 194 switch f.Spacing { 195 case SpaceSides: 196 mainSize += space / 2 197 case SpaceEnd: 198 mainSize += space 199 case SpaceEvenly: 200 mainSize += space / (1 + len(children)) 201 case SpaceAround: 202 mainSize += space / (len(children) * 2) 203 } 204 sz := axisPoint(f.Axis, mainSize, maxCross) 205 gtx.Dimensions = Dimensions{Size: sz, Baseline: sz.Y - maxBaseline} 206 } 207 208 func axisPoint(a Axis, main, cross int) image.Point { 209 if a == Horizontal { 210 return image.Point{main, cross} 211 } else { 212 return image.Point{cross, main} 213 } 214 } 215 216 func axisMain(a Axis, sz image.Point) int { 217 if a == Horizontal { 218 return sz.X 219 } else { 220 return sz.Y 221 } 222 } 223 224 func axisCross(a Axis, sz image.Point) int { 225 if a == Horizontal { 226 return sz.Y 227 } else { 228 return sz.X 229 } 230 } 231 232 func axisMainConstraint(a Axis, cs Constraints) Constraint { 233 if a == Horizontal { 234 return cs.Width 235 } else { 236 return cs.Height 237 } 238 } 239 240 func axisCrossConstraint(a Axis, cs Constraints) Constraint { 241 if a == Horizontal { 242 return cs.Height 243 } else { 244 return cs.Width 245 } 246 } 247 248 func axisConstraints(a Axis, mainc, crossc Constraint) Constraints { 249 if a == Horizontal { 250 return Constraints{Width: mainc, Height: crossc} 251 } else { 252 return Constraints{Width: crossc, Height: mainc} 253 } 254 } 255 256 func toPointF(p image.Point) f32.Point { 257 return f32.Point{X: float32(p.X), Y: float32(p.Y)} 258 } 259 260 func (s Spacing) String() string { 261 switch s { 262 case SpaceEnd: 263 return "SpaceEnd" 264 case SpaceStart: 265 return "SpaceStart" 266 case SpaceSides: 267 return "SpaceSides" 268 case SpaceAround: 269 return "SpaceAround" 270 case SpaceBetween: 271 return "SpaceAround" 272 case SpaceEvenly: 273 return "SpaceEvenly" 274 default: 275 panic("unreachable") 276 } 277 }