flex.go 5.7 KB

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