1 // SPDX-License-Identifier: Unlicense OR MIT
2 3 package layout
4 5 import (
6 "image"
7 8 "github.com/p9c/p9/pkg/gel/gio/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 var maxCross int
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 stack := op.Save(gtx.Ops)
187 pt := f.Axis.Convert(image.Pt(mainSize, cross))
188 op.Offset(FPt(pt)).Add(gtx.Ops)
189 child.call.Add(gtx.Ops)
190 stack.Load()
191 mainSize += f.Axis.Convert(dims.Size).X
192 if i < len(children)-1 {
193 switch f.Spacing {
194 case SpaceEvenly:
195 mainSize += space / (1 + len(children))
196 case SpaceAround:
197 if len(children) > 0 {
198 mainSize += space / len(children)
199 }
200 case SpaceBetween:
201 if len(children) > 1 {
202 mainSize += space / (len(children) - 1)
203 }
204 }
205 }
206 }
207 switch f.Spacing {
208 case SpaceSides:
209 mainSize += space / 2
210 case SpaceEnd:
211 mainSize += space
212 case SpaceEvenly:
213 mainSize += space / (1 + len(children))
214 case SpaceAround:
215 if len(children) > 0 {
216 mainSize += space / (len(children) * 2)
217 }
218 }
219 sz := f.Axis.Convert(image.Pt(mainSize, maxCross))
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 }
241