gioui.org@v0.6.1-0.20240506124620-7a9ce51988ce/layout/flex.go (about) 1 // SPDX-License-Identifier: Unlicense OR MIT 2 3 package layout 4 5 import ( 6 "image" 7 8 "gioui.org/op" 9 ) 10 11 // Flex lays out child elements along an axis, 12 // according to alignment and weights. 13 type Flex struct { 14 // Axis is the main axis, either Horizontal or Vertical. 15 Axis Axis 16 // Spacing controls the distribution of space left after 17 // layout. 18 Spacing Spacing 19 // Alignment is the alignment in the cross axis. 20 Alignment Alignment 21 // WeightSum is the sum of weights used for the weighted 22 // size of Flexed children. If WeightSum is zero, the sum 23 // of all Flexed weights is used. 24 WeightSum float32 25 } 26 27 // FlexChild is the descriptor for a Flex child. 28 type FlexChild struct { 29 flex bool 30 weight float32 31 32 widget Widget 33 34 // Scratch space. 35 call op.CallOp 36 dims Dimensions 37 } 38 39 // Spacing determine the spacing mode for a Flex. 40 type Spacing uint8 41 42 const ( 43 // SpaceEnd leaves space at the end. 44 SpaceEnd Spacing = iota 45 // SpaceStart leaves space at the start. 46 SpaceStart 47 // SpaceSides shares space between the start and end. 48 SpaceSides 49 // SpaceAround distributes space evenly between children, 50 // with half as much space at the start and end. 51 SpaceAround 52 // SpaceBetween distributes space evenly between children, 53 // leaving no space at the start and end. 54 SpaceBetween 55 // SpaceEvenly distributes space evenly between children and 56 // at the start and end. 57 SpaceEvenly 58 ) 59 60 // Rigid returns a Flex child with a maximal constraint of the 61 // remaining space. 62 func Rigid(widget Widget) FlexChild { 63 return FlexChild{ 64 widget: widget, 65 } 66 } 67 68 // Flexed returns a Flex child forced to take up weight fraction of the 69 // space left over from Rigid children. The fraction is weight 70 // divided by either the weight sum of all Flexed children or the Flex 71 // WeightSum if non zero. 72 func Flexed(weight float32, widget Widget) FlexChild { 73 return FlexChild{ 74 flex: true, 75 weight: weight, 76 widget: widget, 77 } 78 } 79 80 // Layout a list of children. The position of the children are 81 // determined by the specified order, but Rigid children are laid out 82 // before Flexed children. 83 func (f Flex) Layout(gtx Context, children ...FlexChild) Dimensions { 84 size := 0 85 cs := gtx.Constraints 86 mainMin, mainMax := f.Axis.mainConstraint(cs) 87 crossMin, crossMax := f.Axis.crossConstraint(cs) 88 remaining := mainMax 89 var totalWeight float32 90 cgtx := gtx 91 // Lay out Rigid children. 92 for i, child := range children { 93 if child.flex { 94 totalWeight += child.weight 95 continue 96 } 97 macro := op.Record(gtx.Ops) 98 cgtx.Constraints = f.Axis.constraints(0, remaining, crossMin, crossMax) 99 dims := child.widget(cgtx) 100 c := macro.Stop() 101 sz := f.Axis.Convert(dims.Size).X 102 size += sz 103 remaining -= sz 104 if remaining < 0 { 105 remaining = 0 106 } 107 children[i].call = c 108 children[i].dims = dims 109 } 110 if w := f.WeightSum; w != 0 { 111 totalWeight = w 112 } 113 // fraction is the rounding error from a Flex weighting. 114 var fraction float32 115 flexTotal := remaining 116 // Lay out Flexed children. 117 for i, child := range children { 118 if !child.flex { 119 continue 120 } 121 var flexSize int 122 if remaining > 0 && totalWeight > 0 { 123 // Apply weight and add any leftover fraction from a 124 // previous Flexed. 125 childSize := float32(flexTotal) * child.weight / totalWeight 126 flexSize = int(childSize + fraction + .5) 127 fraction = childSize - float32(flexSize) 128 if flexSize > remaining { 129 flexSize = remaining 130 } 131 } 132 macro := op.Record(gtx.Ops) 133 cgtx.Constraints = f.Axis.constraints(flexSize, flexSize, crossMin, crossMax) 134 dims := child.widget(cgtx) 135 c := macro.Stop() 136 sz := f.Axis.Convert(dims.Size).X 137 size += sz 138 remaining -= sz 139 if remaining < 0 { 140 remaining = 0 141 } 142 children[i].call = c 143 children[i].dims = dims 144 } 145 maxCross := crossMin 146 var maxBaseline int 147 for _, child := range children { 148 if c := f.Axis.Convert(child.dims.Size).Y; c > maxCross { 149 maxCross = c 150 } 151 if b := child.dims.Size.Y - child.dims.Baseline; b > maxBaseline { 152 maxBaseline = b 153 } 154 } 155 var space int 156 if mainMin > size { 157 space = mainMin - size 158 } 159 var mainSize int 160 switch f.Spacing { 161 case SpaceSides: 162 mainSize += space / 2 163 case SpaceStart: 164 mainSize += space 165 case SpaceEvenly: 166 mainSize += space / (1 + len(children)) 167 case SpaceAround: 168 if len(children) > 0 { 169 mainSize += space / (len(children) * 2) 170 } 171 } 172 for i, child := range children { 173 dims := child.dims 174 b := dims.Size.Y - dims.Baseline 175 var cross int 176 switch f.Alignment { 177 case End: 178 cross = maxCross - f.Axis.Convert(dims.Size).Y 179 case Middle: 180 cross = (maxCross - f.Axis.Convert(dims.Size).Y) / 2 181 case Baseline: 182 if f.Axis == Horizontal { 183 cross = maxBaseline - b 184 } 185 } 186 pt := f.Axis.Convert(image.Pt(mainSize, cross)) 187 trans := op.Offset(pt).Push(gtx.Ops) 188 child.call.Add(gtx.Ops) 189 trans.Pop() 190 mainSize += f.Axis.Convert(dims.Size).X 191 if i < len(children)-1 { 192 switch f.Spacing { 193 case SpaceEvenly: 194 mainSize += space / (1 + len(children)) 195 case SpaceAround: 196 if len(children) > 0 { 197 mainSize += space / len(children) 198 } 199 case SpaceBetween: 200 if len(children) > 1 { 201 mainSize += space / (len(children) - 1) 202 } 203 } 204 } 205 } 206 switch f.Spacing { 207 case SpaceSides: 208 mainSize += space / 2 209 case SpaceEnd: 210 mainSize += space 211 case SpaceEvenly: 212 mainSize += space / (1 + len(children)) 213 case SpaceAround: 214 if len(children) > 0 { 215 mainSize += space / (len(children) * 2) 216 } 217 } 218 sz := f.Axis.Convert(image.Pt(mainSize, maxCross)) 219 sz = cs.Constrain(sz) 220 return Dimensions{Size: sz, Baseline: sz.Y - maxBaseline} 221 } 222 223 func (s Spacing) String() string { 224 switch s { 225 case SpaceEnd: 226 return "SpaceEnd" 227 case SpaceStart: 228 return "SpaceStart" 229 case SpaceSides: 230 return "SpaceSides" 231 case SpaceAround: 232 return "SpaceAround" 233 case SpaceBetween: 234 return "SpaceAround" 235 case SpaceEvenly: 236 return "SpaceEvenly" 237 default: 238 panic("unreachable") 239 } 240 }