1 /* Invisible Vector Library
2 * simple FlexBox-based layouting engine
4 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
5 * Understanding is not required. Only obedience.
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, version 3 of the License ONLY.
11 * This program is distributed in the hope that it will be useful,
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 * GNU General Public License for more details.
16 * You should have received a copy of the GNU General Public License
17 * along with this program. If not, see <http://www.gnu.org/licenses/>.
19 /// this engine can layout any boxset (if it is valid)
20 module iv
.flexlay2
/*is aliced*/;
23 the idea of flexbox layout is very simple:
25 the size of a box is equal to the size of its parent multiplied by the
26 value of the its `flex` property, and divided by the sum of all the
27 `flex` properties of all boxes included in its parent.
31 the algorithm is multipass, and it also does separate layouting for
32 horizontal and vertical containers.
34 each box has the following basic properties:
37 prefSize (preferred, initial size)
38 also, there is runtime property
40 finalSize (expanded box size; see below)
42 let's see how horizontal container is layouted.
44 first pass: set initial sizes.
45 recursively calls `setInitSizes()` for all children.
46 each child sets its initial size to `prefsize`, and calls
47 `setInitSizes()` for all its children.
49 second pass: calculate bounding size.
50 recursively calls `fitToSize()` for all children.
51 on this pass, we simply sum sizes of all children, and check if
52 they fit in our maxsize. if they aren't, calculate the extra,
53 and ask each children to shrink itself with `shrinkBy()`.
54 we can check child `size` property to see if a shrink succeeded.
55 loop over all children until either there is no more extra, or
56 all children refuse to shrink. if we have some extra left,
57 add it to our size (because this is everything we can do now).
59 third pass: set real sizes.
60 recursively calls `calcFinalSize()` for all children.
61 on this pass, we have correct sizes set, so we simply process
62 flexbox expanding, as described above.
64 the same thing is done for vertical containers then.
66 note that horizontal second pass can perform wrapping too. if we hit a hard
67 limit, we can try to wrap some children before distributing extra. i didn't
68 implemented it yet, tho, because i never need to wrap anything, and i prefer
69 to keep the code clean (haha) and small.
72 //version = flexlay_debug;
75 // ////////////////////////////////////////////////////////////////////////// //
79 BoxIdT should be lightweight and copyable; also, BoxIdT.init should mean "no box".
81 the layouter will never allocate anything by itself (but it is highly recursive).
83 struct FuiFlexLayouter(BoxIdT
) {
85 bool delegate (BoxIdT id
) isValidBoxId
;
87 // the following delegates will never receive invalid box id from the layouter
88 BoxIdT
delegate (BoxIdT id
) firstChild
;
89 BoxIdT
delegate (BoxIdT id
) nextSibling
;
91 // make sure that returned sizes are never negative (zero is allowed)
92 // also, make sure that max size is bigger than min size
93 // max size can return `int.max` for "unlimited"
95 int delegate (BoxIdT id
, in bool horiz
) getMinSize
;
96 int delegate (BoxIdT id
, in bool horiz
) getMaxSize
;
97 int delegate (BoxIdT id
, in bool horiz
) getPrefSize
;
99 // is this box a horizontal container?
100 bool delegate (BoxIdT id
) isHorizBox
;
102 // flex value for this box; <=0 means "not flexible"
103 int delegate (BoxIdT id
) getFlex
;
105 // unexpanded box size; never bigger than final size
106 // this can be used to align the box inside the expanded (final) rectangle
107 // the layouter will never try to get this size before setting it
108 int delegate (BoxIdT id
, in bool horiz
) getSize
;
109 void delegate (BoxIdT id
, in bool horiz
, int val
) setSize
;
111 // final expanded box size
112 // the layouter will never try to get this size before setting it
113 int delegate (BoxIdT id
, in bool horiz
) getFinalSize
;
114 void delegate (BoxIdT id
, in bool horiz
, int val
) setFinalSize
;
116 // final expanded box coordinates inside the parent (i.e. relative to the parent origin)
117 void delegate (BoxIdT id
, in bool horiz
, int val
) setFinalPos
;
120 // the only interesting method here ;-)
121 void layout (BoxIdT rootId
) {
122 if (!isValidBoxId(rootId
)) return;
124 setInitSizes(rootId
);
125 //version(flexlay_debug) dumpLayout();
127 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, "=== fit-to-size: horiz ===\n"); }
128 fitToSize(rootId
, true, getMaxSize(rootId
, true));
129 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, "=== fit-to-size: vert ===\n"); }
130 fitToSize(rootId
, false, getMaxSize(rootId
, false));
131 //version(flexlay_debug) dumpLayout();
133 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, "=== calc-final: horiz ===\n"); }
134 calcFinalSize(rootId
, true, true);
135 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, "=== calc-final: vert ===\n"); }
136 calcFinalSize(rootId
, false, true);
137 //version(flexlay_debug) dumpLayout();
141 void forEachChild (BoxIdT id
, scope void delegate (BoxIdT id
) dg
) {
143 if (!isValidBoxId(id
)) return;
144 for (id
= firstChild(id
); isValidBoxId(id
); id
= nextSibling(id
)) dg(id
);
147 int calcChildrenSize (BoxIdT id
, in bool horiz
) {
148 if (!isValidBoxId(id
)) return 0;
149 immutable bool mainDir
= (horiz
== isHorizBox(id
));
150 int res
= (mainDir ?
0 : int.min
);
151 for (id
= firstChild(id
); isValidBoxId(id
); id
= nextSibling(id
)) {
152 immutable int sz
= getSize(id
, horiz
);
153 if (mainDir
) res
+= sz
; else { if (res
< sz
) res
= sz
; }
159 static T
min2(T
) (in T a
, in T b
) pure nothrow @safe @nogc { pragma(inline
, true); return (a
< b ? a
: b
); }
160 static T
max2(T
) (in T a
, in T b
) pure nothrow @safe @nogc { pragma(inline
, true); return (a
> b ? a
: b
); }
161 static T
clampval(T
) (in T val
, in T min
, in T max
) pure nothrow @safe @nogc { pragma(inline
, true); return (val
< min ? min
: val
> max ? max
: val
); }
163 // setup initial size from preferred size
164 void setInitSizes (BoxIdT id
) {
165 if (!isValidBoxId(id
)) return;
167 setSize(id
, true, clampval(getPrefSize(id
, true), getMinSize(id
, true), getMaxSize(id
, true)));
169 setSize(id
, false, clampval(getPrefSize(id
, false), getMinSize(id
, false), getMaxSize(id
, false)));
170 version(flexlay_debug
) {
171 import core
.stdc
.stdio
: stderr
, fprintf
;
172 fprintf(stderr
, "setInitSizes for %08x; min=(%d,%d); max=(%d,%d); pref=(%d,%d); size=(%d,%d)\n", cast(uint)cast(void*)id
,
173 getMinSize(id
, true), getMinSize(id
, false),
174 getMaxSize(id
, true), getMaxSize(id
, false),
175 getPrefSize(id
, true), getPrefSize(id
, false),
176 getSize(id
, true), getSize(id
, false));
179 forEachChild(id
, &setInitSizes
);
182 void shrinkBy (BoxIdT id
, in bool horiz
, int delta
) {
184 if (delta
< 1) return;
185 immutable int sz
= getSize(id
, horiz
);
186 immutable int minsz
= getMinSize(id
, horiz
);
187 if (sz
<= minsz
) return;
188 immutable int maxShrink
= sz
-minsz
;
189 assert(maxShrink
> 0);
190 if (delta
> maxShrink
) delta
= maxShrink
;
191 immutable int nsz
= sz
-delta
;
192 fitToSize(id
, horiz
, nsz
);
195 // fit into the given size
196 // recursively calls `fitToSize()` for all children.
197 // on this pass, we simply sum sizes of all children, and check if
198 // they fit in our maxsize. if they aren't, calculate the extra,
199 // and ask each children to shrink itself with `shrinkBy()`.
200 // we can check child `size` property to see if a shrink succeeded.
201 // loop over all children until either there is no more extra, or
202 // all children refuse to shrink. if we have some extra left,
203 // add it to our size (because this is everything we can do now).
204 // this also expands box size to accomodate all children.
205 void fitToSize (BoxIdT id
, in bool horiz
, int maxsz
) {
206 if (!isValidBoxId(id
)) return;
208 maxsz
= max2(0, clampval(maxsz
, getMinSize(id
, horiz
), getMaxSize(id
, horiz
)));
209 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, "fitToSize for %08x: maxsz=%d; size=(%d,%d)\n", cast(uint)cast(void*)id
, maxsz
, getSize(id
, true), getSize(id
, false)); }
211 int ccount
= 0; // number of visible children
212 // recursively calls `fitToSize()` for all children
213 forEachChild(id
, (box
) { fitToSize(box
, horiz
, maxsz
); ++ccount
; });
215 if (getSize(id
, horiz
) > maxsz
) setSize(id
, horiz
, maxsz
);
220 bool wasShrink
= true;
222 // check if our size fits in max
223 immutable int csz
= calcChildrenSize(id
, horiz
);
224 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, " csz for %08x=%d; maxsz=%d\n", cast(uint)cast(void*)id
, csz
, maxsz
); }
225 if (csz
<= maxsz
) break; // we're done
226 // can't fit, need to shrink children
227 int extra
= csz
-maxsz
;
228 int shrinkStep
= extra
/ccount
;
229 shrinkStep
+= !shrinkStep
;
231 forEachChild(id
, (box
) {
233 immutable int osz
= getSize(box
, horiz
);
234 immutable int sk
= (shrinkStep
< extra ? shrinkStep
: extra
);
235 shrinkBy(box
, horiz
, sk
);
236 immutable int nsz
= getSize(box
, horiz
);
239 extra
= max2(0, extra
-(osz
-nsz
));
244 // expand this box (up to max size) to fit all children
245 immutable int fcsz
= min2(maxsz
, calcChildrenSize(id
, horiz
));
246 if (getSize(id
, horiz
) < fcsz
) setSize(id
, horiz
, fcsz
);
249 void calcFinalSize (BoxIdT id
, in bool horiz
, in bool isRoot
) {
250 if (!isValidBoxId(id
)) return;
253 if (getSize(id
, horiz
) <= 0) {
254 setSize(id
, horiz
, 0);
255 setFinalPos(id
, horiz
, 0);
256 setFinalSize(id
, horiz
, 0);
257 forEachChild(id
, (box
) {
258 setSize(box
, horiz
, 0);
259 setFinalPos(box
, horiz
, 0);
260 calcFinalSize(box
, horiz
, false);
267 setFinalPos(id
, horiz
, 0);
268 setFinalSize(id
, horiz
, getSize(id
, horiz
));
271 // expand all flexboxes
272 // the size of a box is equal to the size of its parent multiplied by the
273 // value of the its `flex` property, and divided by the sum of all the
274 // `flex` properties of all boxes included in its parent.
276 // actually, calculate the remaining free space, and distribute it
279 forEachChild(id
, (box
) {
281 immutable int flex
= getFlex(box
);
282 if (flex
> 0) flextotal
+= flex
;
283 // also, set initial final size
284 setFinalSize(box
, horiz
, getSize(box
, horiz
));
287 // flexbox processing
289 immutable int csz
= calcChildrenSize(id
, horiz
);
290 int left
= getSize(id
, horiz
);
291 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, "flex for %08x; csz=%d; left=%d\n", cast(uint)cast(void*)id
, csz
, left
); }
296 bool wasChange
= false;
297 immutable int totalsz
= left
;
298 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, "flexpass for %08x: left=%d; flextotal=%d\n", cast(uint)cast(void*)id
, left
, flextotal
); }
299 forEachChild(id
, (box
) {
301 immutable int flex
= getFlex(box
);
302 if (flex
<= 0) return;
303 immutable int finsz
= getFinalSize(box
, horiz
);
304 immutable int maxsz
= getMaxSize(box
, horiz
);
305 if (finsz
>= maxsz
) return;
306 int addsp
= totalsz
*flex
/flextotal
;
308 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, " %08x: left=%d; flextotal=%d; flex=%d; addsp=%d\n", cast(uint)cast(void*)box
, left
, flextotal
, getFlex(box
), addsp
); }
309 if (addsp
> left
) addsp
= left
;
310 immutable int maxgrow
= maxsz
-finsz
;
311 if (addsp
> maxgrow
) addsp
= maxgrow
;
312 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, " %08x: left=%d; flextotal=%d; flex=%d; addsp=%d (maxgrow=%d)\n", cast(uint)cast(void*)box
, left
, flextotal
, getFlex(box
), addsp
, maxgrow
); }
313 setFinalSize(box
, horiz
, finsz
+addsp
);
317 if (!wasChange
) break;
323 immutable bool mainDir
= (horiz
== isHorizBox(id
));
324 immutable int fsz
= getFinalSize(id
, horiz
);
327 BoxIdT last
; // save last child, we may need it later
328 forEachChild(id
, (box
) {
330 setFinalPos(box
, horiz
, coord
);
332 coord
+= getFinalSize(box
, horiz
);
335 immutable int bsz
= getSize(box
, horiz
);
336 version(flexlay_debug
) { import core
.stdc
.stdio
: stderr
, fprintf
; fprintf(stderr
, " %08x: expand; bsz=%d; fsz=%d\n", cast(uint)cast(void*)box
, bsz
, fsz
); }
339 immutable int mxsz
= getMaxSize(box
, horiz
);
340 if (bsz
< mxsz
) setFinalSize(box
, horiz
, min2(fsz
, mxsz
));
346 if (mainDir
&& coord
< fsz
&& isValidBoxId(last
)) {
348 immutable int lmxsz
= getMaxSize(last
, horiz
);
349 immutable int finsz
= getFinalSize(last
, horiz
);
351 setFinalSize(last
, horiz
, min2(finsz
+coord
, lmxsz
));
355 // now do the same for all visible children
356 // this is done as the last step, because children has their sizes set now
357 // we will temporarily set box size property to calculated size, so
358 // the box will correctly redistribute everything
359 forEachChild(id
, (box
) {
361 immutable int osz
= getSize(box
, horiz
);
362 setSize(box
, horiz
, getFinalSize(box
, horiz
));
363 calcFinalSize(box
, horiz
, false);
364 setSize(box
, horiz
, osz
);