some updates
[iv.d.git] / flexlay2.d
blobd6bb3ea1a711ae3f597090402230a0b2da59f674
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:
35 minSize
36 maxSize
37 prefSize (preferred, initial size)
38 also, there is runtime property
39 size (real box size)
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 // ////////////////////////////////////////////////////////////////////////// //
76 /**
77 Flexbox Layouter
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) {
84 public:
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;
119 public:
120 // the only interesting method here ;-)
121 void layout (BoxIdT rootId) {
122 if (!isValidBoxId(rootId)) return;
123 // phase 0
124 setInitSizes(rootId);
125 //version(flexlay_debug) dumpLayout();
126 // phase 1
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();
132 // phase 2
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();
140 private:
141 void forEachChild (BoxIdT id, scope void delegate (BoxIdT id) dg) {
142 assert(dg !is null);
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; }
155 return res;
158 private:
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;
166 // horiz size
167 setSize(id, true, clampval(getPrefSize(id, true), getMinSize(id, true), getMaxSize(id, true)));
168 // vert size
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));
178 // process children
179 forEachChild(id, &setInitSizes);
182 void shrinkBy (BoxIdT id, in bool horiz, int delta) {
183 assert(delta >= 0);
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; });
214 if (ccount == 0) {
215 if (getSize(id, horiz) > maxsz) setSize(id, horiz, maxsz);
216 return;
219 // shrink (fit) loop
220 bool wasShrink = true;
221 while (wasShrink) {
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;
230 wasShrink = false;
231 forEachChild(id, (box) {
232 if (!extra) return;
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);
237 if (nsz < osz) {
238 wasShrink = true;
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;
252 // degenerate case?
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);
262 return;
265 // top level?
266 if (isRoot) {
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
277 int ccount = 0;
278 int flextotal = 0;
279 forEachChild(id, (box) {
280 ++ccount;
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
288 if (flextotal) {
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); }
292 if (csz < left) {
293 left -= csz;
294 // grow flexboxes
295 while (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) {
300 if (!left) return;
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;
307 addsp += !addsp;
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);
314 left -= addsp;
315 wasChange = true;
317 if (!wasChange) break;
322 // setup coords
323 immutable bool mainDir = (horiz == isHorizBox(id));
324 immutable int fsz = getFinalSize(id, horiz);
326 int coord = 0;
327 BoxIdT last; // save last child, we may need it later
328 forEachChild(id, (box) {
329 last = box;
330 setFinalPos(box, horiz, coord);
331 if (mainDir) {
332 coord += getFinalSize(box, horiz);
333 } else {
334 // expand each box
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); }
337 if (bsz < fsz) {
338 // respect max size
339 immutable int mxsz = getMaxSize(box, horiz);
340 if (bsz < mxsz) setFinalSize(box, horiz, min2(fsz, mxsz));
345 // expand last box
346 if (mainDir && coord < fsz && isValidBoxId(last)) {
347 coord = fsz-coord;
348 immutable int lmxsz = getMaxSize(last, horiz);
349 immutable int finsz = getFinalSize(last, horiz);
350 if (finsz < lmxsz) {
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) {
360 //HACK!
361 immutable int osz = getSize(box, horiz);
362 setSize(box, horiz, getFinalSize(box, horiz));
363 calcFinalSize(box, horiz, false);
364 setSize(box, horiz, osz);