some updates
[iv.d.git] / flexlayout.d
blob7717f9c18caf5a7ea8ee83ffad3e17e2f33b2129
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.flexlayout /*is aliced*/;
21 import iv.alice;
24 the idea of flexbox layout is very simple:
26 the size of a box is equal to the size of its parent multiplied by the
27 value of the its `flex` property, and divided by the sum of all the
28 `flex` properties of all boxes included in its parent.
32 // ////////////////////////////////////////////////////////////////////////// //
33 /// point
34 public align(1) struct FuiPoint {
35 align(1):
36 int x, y;
37 @property pure nothrow @safe @nogc:
38 bool inside (in FuiRect rc) const { pragma(inline, true); return (x >= rc.pos.x && y >= rc.pos.y && x < rc.pos.x+rc.size.w && y < rc.pos.y+rc.size.h); }
39 ref FuiPoint opOpAssign(string op) (in auto ref FuiPoint pt) if (op == "+" || op == "-") {
40 mixin("x"~op~"=pt.x; y"~op~"=pt.y;");
41 return this;
43 FuiPoint opBinary(string op) (in auto ref FuiPoint pt) if (op == "+" || op == "-") {
44 mixin("return FuiPoint(x"~op~"pt.x, y"~op~"pt.y);");
46 int opIndex (usize idx) const { pragma(inline, true); return (idx == 0 ? x : idx == 1 ? y : 0); }
47 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx == 0) x = v; else if (idx == 1) y = v; }
50 /// size
51 public align(1) struct FuiSize {
52 align(1):
53 int w, h;
54 @property pure nothrow @safe @nogc:
55 int opIndex (usize idx) const { pragma(inline, true); return (idx == 0 ? w : idx == 1 ? h : 0); }
56 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx == 0) w = v; else if (idx == 1) h = v; }
60 /// rectangle
61 public align(1) struct FuiRect {
62 align(1):
63 FuiPoint pos;
64 FuiSize size;
65 @property pure nothrow @safe @nogc:
66 int x () const { pragma(inline, true); return pos.x; }
67 int y () const { pragma(inline, true); return pos.y; }
68 int w () const { pragma(inline, true); return size.w; }
69 int h () const { pragma(inline, true); return size.h; }
70 void x (int v) { pragma(inline, true); pos.x = v; }
71 void y (int v) { pragma(inline, true); pos.y = v; }
72 void w (int v) { pragma(inline, true); size.w = v; }
73 void h (int v) { pragma(inline, true); size.h = v; }
75 ref int xp () { pragma(inline, true); return pos.x; }
76 ref int yp () { pragma(inline, true); return pos.y; }
77 ref int wp () { pragma(inline, true); return size.w; }
78 ref int hp () { pragma(inline, true); return size.h; }
80 bool inside (in FuiPoint pt) const { pragma(inline, true); return (pt.x >= pos.x && pt.y >= pos.y && pt.x < pos.x+size.w && pt.y < pos.y+size.h); }
84 /// margins
85 public align(1) struct FuiMargin {
86 align(1):
87 int[4] ltrb;
88 pure nothrow @trusted @nogc:
89 this (const(int)[] v...) { if (v.length > 4) v = v[0..4]; ltrb[0..v.length] = v[]; }
90 @property:
91 int left () const { pragma(inline, true); return ltrb.ptr[0]; }
92 int top () const { pragma(inline, true); return ltrb.ptr[1]; }
93 int right () const { pragma(inline, true); return ltrb.ptr[2]; }
94 int bottom () const { pragma(inline, true); return ltrb.ptr[3]; }
95 void left (int v) { pragma(inline, true); ltrb.ptr[0] = v; }
96 void top (int v) { pragma(inline, true); ltrb.ptr[1] = v; }
97 void right (int v) { pragma(inline, true); ltrb.ptr[2] = v; }
98 void bottom (int v) { pragma(inline, true); ltrb.ptr[3] = v; }
99 int opIndex (usize idx) const { pragma(inline, true); return (idx < 4 ? ltrb.ptr[idx] : 0); }
100 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx < 4) ltrb.ptr[idx] = v; }
104 // ////////////////////////////////////////////////////////////////////////// //
105 /// properties for layouter
106 public class FuiLayoutProps {
108 enum Orientation {
109 Horizontal, ///
110 Vertical, ///
113 /// "NPD" means "non-packing direction"
114 enum Align {
115 Center, /// the available space is divided evenly
116 Start, /// the NPD edge of each box is placed along the NPD of the parent box
117 End, /// the opposite-NPD edge of each box is placed along the opposite-NPD of the parent box
118 Stretch, /// the NPD-size of each boxes is adjusted to fill the parent box
121 void layoutingStarted () {} /// called before layouting starts
122 void layoutingComplete () {} /// called after layouting complete
124 //WARNING! the following properties should be set to correct values before layouting
125 // you can use `layoutingStarted()` method to do this
127 bool visible; /// invisible controls will be ignored by layouter (should be set to correct values before layouting; you can use `layoutingStarted()` method to do this)
128 bool lineBreak; /// layouter should start a new line after this control (should be set to correct values before layouting; you can use `layoutingStarted()` method to do this)
129 bool ignoreSpacing; /// (should be set to correct values before layouting; you can use `layoutingStarted()` method to do this)
131 Orientation orientation = Orientation.Horizontal; /// box orientation
132 Align aligning = Align.Start; /// NPD for children; sadly, "align" keyword is reserved
133 int flex; /// <=0: not flexible
135 FuiMargin padding; /// padding for this widget
136 int spacing; /// spacing for children
137 int lineSpacing; /// line spacing for horizontal boxes
138 FuiSize minSize; /// minimal control size
139 FuiSize maxSize; /// maximal control size (0 means "unlimited")
141 /// controls in a horizontal group has the same width, and the same height in a vertical group
142 FuiLayoutProps[Orientation.max+1] groupNext; /// next sibling for this control's group or null
144 /// calculated item dimensions
145 FuiRect rect;
146 final @property ref inout(FuiPoint) pos () pure inout nothrow @safe @nogc { pragma(inline, true); return rect.pos; } ///
147 final @property ref inout(FuiSize) size () pure inout nothrow @safe @nogc { pragma(inline, true); return rect.size; } ///
149 FuiLayoutProps parent; /// null for root element
150 FuiLayoutProps firstChild; /// null for "no children"
151 FuiLayoutProps nextSibling; /// null for last item
153 /// you can specify your own root if necessary
154 final FuiPoint toGlobal (FuiPoint pt, FuiLayoutProps root=null) const pure nothrow @trusted @nogc {
155 for (FuiLayoutProps it = cast(FuiLayoutProps)this; it !is null; it = it.parent) {
156 pt.x += it.pos.x;
157 pt.y += it.pos.y;
158 if (it is root) break;
160 return pt;
163 /// you can specify your own root if necessary
164 final FuiPoint toLocal (FuiPoint pt, FuiLayoutProps root=null) const pure nothrow @trusted @nogc {
165 for (FuiLayoutProps it = cast(FuiLayoutProps)this; it !is null; it = it.parent) {
166 pt.x -= it.pos.x;
167 pt.y -= it.pos.y;
168 if (it is root) break;
170 return pt;
173 private:
174 // internal housekeeping for layouter
175 FuiLayoutProps[Orientation.max+1] groupHead;
176 bool tempLineBreak;
178 final:
179 void resetLayouterFlags () { pragma(inline, true); tempLineBreak = false; groupHead[] = null; }
183 // ////////////////////////////////////////////////////////////////////////// //
184 // you can set maximum dimesions by setting root panel maxSize
185 // visit `root` and it's children
186 private static void forEachItem (FuiLayoutProps root, scope void delegate (FuiLayoutProps it) dg) {
187 void visitAll (FuiLayoutProps it) {
188 while (it !is null) {
189 dg(it);
190 visitAll(it.firstChild);
191 it = it.nextSibling;
194 if (root is null || dg is null) return;
195 dg(root);
196 visitAll(root.firstChild);
200 /// do layouting
201 void flexLayout (FuiLayoutProps aroot) {
202 import std.algorithm : min, max;
204 if (aroot is null) return;
205 auto oparent = aroot.parent;
206 auto onexts = aroot.nextSibling;
207 aroot.parent = null;
208 aroot.nextSibling = null;
209 scope(exit) { aroot.parent = oparent; aroot.nextSibling = onexts; }
210 auto mroot = aroot;
212 // layout children in this item
213 void layit() (FuiLayoutProps lp) {
214 if (lp is null || !lp.visible) return;
216 // cache values
217 immutable bpadLeft = max(0, lp.padding.left);
218 immutable bpadRight = max(0, lp.padding.right);
219 immutable bpadTop = max(0, lp.padding.top);
220 immutable bpadBottom = max(0, lp.padding.bottom);
221 immutable bspc = max(0, lp.spacing);
222 immutable hbox = (lp.orientation == FuiLayoutProps.Orientation.Horizontal);
224 // widget can only grow, and while doing that, `maxSize` will be respected, so we don't need to fix it's size
226 // layout children, insert line breaks, if necessary
227 int curWidth = bpadLeft+bpadRight, maxW = bpadLeft+bpadRight, maxH = bpadTop+bpadBottom;
228 FuiLayoutProps lastCIdx = null; // last processed item for the current line
229 int lineH = 0; // for the current line
230 int lineCount = 0;
231 int lineMaxW = (lp.size.w > 0 ? lp.size.w : (lp.maxSize.w > 0 ? lp.maxSize.w : int.max));
233 // unconditionally add current item to the current line
234 void addToLine (FuiLayoutProps clp) {
235 clp.tempLineBreak = false;
236 curWidth += clp.size.w+(lastCIdx !is null && !lastCIdx.ignoreSpacing ? bspc : 0);
237 lineH = max(lineH, clp.size.h);
238 lastCIdx = clp;
241 // flush current line
242 void flushLine () {
243 if (lastCIdx is null) return;
244 // mark last item as line break
245 lastCIdx.tempLineBreak = true;
246 // fix max width
247 maxW = max(maxW, curWidth);
248 // fix max height
249 maxH += lineH+(lineCount ? lp.lineSpacing : 0);
250 // restart line
251 curWidth = bpadLeft+bpadRight;
252 lastCIdx = null;
253 lineH = 0;
254 ++lineCount;
257 // put item, do line management
258 void putItem (FuiLayoutProps clp) {
259 int nw = curWidth+clp.size.w+(lastCIdx !is null && !lastCIdx.ignoreSpacing ? bspc : 0);
260 // do we neeed to start a new line?
261 if (nw <= lineMaxW) {
262 // no, just put item into the current line
263 addToLine(clp);
264 return;
266 // yes, check if we have at least one item in the current line
267 if (lastCIdx is null) {
268 // alas, no items in the current line, put clp into it anyway
269 addToLine(clp);
270 // and flush line immediately
271 flushLine();
272 } else {
273 // flush current line
274 flushLine();
275 // and add this item to new one
276 addToLine(clp);
280 // layout children, insert "soft" line breaks
281 for (auto clp = lp.firstChild, cspc = 0; clp !is null; clp = clp.nextSibling) {
282 if (!clp.visible) continue; // invisible, skip it
283 layit(clp); // layout children of this box
284 if (hbox) {
285 // for horizontal box, logic is somewhat messy
286 putItem(clp);
287 if (clp.lineBreak) flushLine();
288 } else {
289 // for vertical box, it is as easy as this
290 clp.tempLineBreak = true;
291 maxW = max(maxW, clp.size.w+bpadLeft+bpadRight);
292 maxH += clp.size.h+cspc;
293 cspc = (clp.ignoreSpacing ? 0 : bspc);
294 ++lineCount;
297 if (hbox) flushLine(); // flush last line for horizontal box (it is safe to flush empty line)
298 // fix max sizes
299 if (lp.maxSize.w > 0 && maxW > lp.maxSize.w) maxW = lp.maxSize.w;
300 if (lp.maxSize.h > 0 && maxH > lp.maxSize.h) maxH = lp.maxSize.h;
302 // grow box or clamp max size
303 // but only if size is not defined; in other cases our size is changed by parent to fit in
304 if (lp.size.w == 0) lp.size.w = max(0, lp.minSize.w, maxW);
305 if (lp.size.h == 0) lp.size.h = max(0, lp.minSize.h, maxH);
306 // cache values
307 maxH = lp.size.h;
308 maxW = lp.size.w;
310 int flexTotal; // total sum of flex fields
311 int flexBoxCount; // number of boxes
312 int curSpc; // "current" spacing in layout calculations (for bspc)
313 int spaceLeft;
315 if (hbox) {
316 // layout horizontal box; we should do this for each line separately
317 int lineStartY = bpadTop;
319 void resetLine () {
320 flexTotal = 0;
321 flexBoxCount = 0;
322 curSpc = 0;
323 spaceLeft = maxW-(bpadLeft+bpadRight);
324 lineH = 0;
327 auto lstart = lp.firstChild;
328 int lineNum = 0;
329 for (;;) {
330 if (lstart is null) break;
331 if (!lstart.visible) continue;
332 // calculate flex variables and line height
333 --lineCount; // so 0 will be "last line"
334 assert(lineCount >= 0);
335 resetLine();
336 for (auto clp = lstart; clp !is null; clp = clp.nextSibling) {
337 if (!clp.visible) continue;
338 auto dim = clp.size.w+curSpc;
339 spaceLeft -= dim;
340 lineH = max(lineH, clp.size.h);
341 // process flex
342 if (clp.flex > 0) { flexTotal += clp.flex; ++flexBoxCount; }
343 if (clp.tempLineBreak) break; // no more in this line
344 curSpc = (clp.ignoreSpacing ? 0 : bspc);
346 if (lineCount == 0) lineH = max(lineH, maxH-bpadBottom-lineStartY-lineH);
347 debug(fui_layout) { import core.stdc.stdio : printf; printf("lineStartY=%d; lineH=%d\n", lineStartY, lineH); }
349 // distribute flex space, fix coordinates
350 debug(fui_layout) { import core.stdc.stdio : printf; printf("flexTotal=%d; flexBoxCount=%d; spaceLeft=%d\n", flexTotal, flexBoxCount, spaceLeft); }
351 if (spaceLeft < 0) spaceLeft = 0;
352 float flt = cast(float)flexTotal;
353 float left = cast(float)spaceLeft;
354 //{ import iv.vfs.io; VFile("zlay.log", "a").writefln("flt=%s; left=%s", flt, left); }
355 int curpos = bpadLeft;
356 for (auto clp = lstart; clp !is null; clp = clp.nextSibling) {
357 lstart = clp.nextSibling;
358 if (!clp.visible) continue;
359 // fix packing coordinate
360 clp.pos.x = curpos;
361 bool doChildrenRelayout = false;
362 // fix non-packing coordinate (and, maybe, non-packing dimension)
363 // fix y coord
364 final switch (clp.aligning) {
365 case FuiLayoutProps.Align.Start: clp.pos.y = lineStartY; break;
366 case FuiLayoutProps.Align.End: clp.pos.y = (lineStartY+lineH)-clp.size.h; break;
367 case FuiLayoutProps.Align.Center: clp.pos.y = lineStartY+(lineH-clp.size.h)/2; break;
368 case FuiLayoutProps.Align.Stretch:
369 clp.pos.y = lineStartY;
370 int nd = min(max(0, lineH, clp.minSize.h), (clp.maxSize.h > 0 ? clp.maxSize.h : int.max));
371 if (nd != clp.size.h) {
372 // size changed, relayout children
373 doChildrenRelayout = true;
374 clp.size.h = nd;
376 break;
378 // fix flexbox size
379 if (clp.flex > 0) {
380 //{ import iv.vfs.io; write("\x07"); }
381 int toadd = cast(int)(left*cast(float)clp.flex/flt+0.5);
382 if (toadd > 0) {
383 // size changed, relayout children
384 doChildrenRelayout = true;
385 clp.size.w += toadd;
386 // compensate (crudely) rounding errors
387 if (toadd > 1 && clp.size.w <= maxW && maxW-(curpos+clp.size.w) < 0) clp.size.w -= 1;
390 // advance packing coordinate
391 curpos += clp.size.w+(clp.ignoreSpacing ? 0 : bspc);
392 // relayout children if dimensions was changed
393 if (doChildrenRelayout) layit(clp);
394 if (clp.tempLineBreak) break; // exit if we have linebreak
395 // next line, please!
397 // yep, move to next line
398 debug(fui_layout) { import core.stdc.stdio : printf; printf("lineStartY=%d; next lineStartY=%d\n", lineStartY, lineStartY+lineH+lp.lineSpacing); }
399 lineStartY += lineH+lp.lineSpacing;
401 } else {
402 // layout vertical box, it is much easier
403 spaceLeft = maxH-(bpadTop+bpadBottom);
404 if (spaceLeft < 0) spaceLeft = 0;
406 // calculate flex variables
407 for (auto clp = lp.firstChild; clp !is null; clp = clp.nextSibling) {
408 if (!clp.visible) continue;
409 auto dim = clp.size.h+curSpc;
410 spaceLeft -= dim;
411 // process flex
412 if (clp.flex > 0) { flexTotal += clp.flex; ++flexBoxCount; }
413 curSpc = (clp.ignoreSpacing ? 0 : bspc);
416 // distribute flex space, fix coordinates
417 float flt = cast(float)flexTotal;
418 float left = cast(float)spaceLeft;
419 int curpos = bpadTop;
420 for (auto clp = lp.firstChild; clp !is null; clp = clp.nextSibling) {
421 if (!clp.visible) break;
422 // fix packing coordinate
423 clp.pos.y = curpos;
424 bool doChildrenRelayout = false;
425 // fix non-packing coordinate (and, maybe, non-packing dimension)
426 // fix x coord
427 final switch (clp.aligning) {
428 case FuiLayoutProps.Align.Start: clp.pos.x = bpadLeft; break;
429 case FuiLayoutProps.Align.End: clp.pos.x = maxW-bpadRight-clp.size.w; break;
430 case FuiLayoutProps.Align.Center: clp.pos.x = (maxW-clp.size.w)/2; break;
431 case FuiLayoutProps.Align.Stretch:
432 int nd = min(max(0, maxW-(bpadLeft+bpadRight), clp.minSize.w), (clp.maxSize.w > 0 ? clp.maxSize.w : int.max));
433 if (nd != clp.size.w) {
434 // size changed, relayout children
435 doChildrenRelayout = true;
436 clp.size.w = nd;
438 clp.pos.x = bpadLeft;
439 break;
441 // fix flexbox size
442 if (clp.flex > 0) {
443 int toadd = cast(int)(left*cast(float)clp.flex/flt);
444 if (toadd > 0) {
445 // size changed, relayout children
446 doChildrenRelayout = true;
447 clp.size.h += toadd;
448 // compensate (crudely) rounding errors
449 if (toadd > 1 && clp.size.h <= maxH && maxH-(curpos+clp.size.h) < 0) clp.size.h -= 1;
452 // advance packing coordinate
453 curpos += clp.size.h+(clp.ignoreSpacing ? bspc : 0);
454 // relayout children if dimensions was changed
455 if (doChildrenRelayout) layit(clp);
457 // that's all for vertical boxes
461 // main code
462 if (mroot is null) return;
464 bool[FuiLayoutProps.Orientation.max+1] seenGroup = false;
466 // reset flags, check if we have any groups
467 forEachItem(mroot, (FuiLayoutProps it) {
468 it.layoutingStarted();
469 it.resetLayouterFlags();
470 it.pos = it.pos.init;
471 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) if (it.groupNext[gidx] !is null) seenGroup[gidx] = true;
472 if (!it.visible) { it.size = it.size.init; return; }
473 it.size = it.size.init;
476 if (seenGroup[0] || seenGroup[1]) {
477 // fix groups
478 forEachItem(mroot, (FuiLayoutProps it) {
479 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) {
480 if (it.groupNext[gidx] is null || it.groupHead[gidx] !is null) continue;
481 // this item is group member, but has no head set, so this is new head: fix the whole list
482 for (FuiLayoutProps gm = it; gm !is null; gm = gm.groupNext[gidx]) gm.groupHead[gidx] = it;
487 // do top-level packing
488 for (;;) {
489 layit(mroot);
490 bool doFix = false;
492 //FIXME: mark changed items and process only those
493 void fixGroups (FuiLayoutProps it, int grp) nothrow @nogc {
494 int dim = 0;
495 // calcluate maximal dimension
496 for (FuiLayoutProps clp = it; clp !is null; clp = clp.groupNext[grp]) {
497 if (!clp.visible) continue;
498 dim = max(dim, clp.size[grp]);
500 // fix dimensions
501 for (FuiLayoutProps clp = it; clp !is null; clp = clp.groupNext[grp]) {
502 if (!clp.visible) continue;
503 auto od = clp.size[grp];
504 int nd = max(od, dim);
505 auto mx = clp.maxSize[grp];
506 if (mx > 0) nd = min(nd, mx);
507 version(none) {
508 import core.stdc.stdio;
509 auto fo = fopen("zlx.log", "a");
510 //fo.fprintf("%.*s: od=%d; nd=%d\n", cast(uint)clp.classinfo.name.length, clp.classinfo.name.ptr, od, nd);
511 fo.fprintf("gidx=%d; dim=%d; w=%d; h=%d\n", grp, dim, clp.size[0], clp.size[1]);
512 fo.fclose();
514 if (od != nd) {
515 doFix = true;
516 clp.size[grp] = nd;
521 if (seenGroup[0] || seenGroup[1]) {
522 forEachItem(mroot, (FuiLayoutProps it) {
523 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) {
524 if (it.groupHead[gidx] is it) fixGroups(it, gidx);
527 if (!doFix) break; // nothing to do
528 } else {
529 // no groups -> nothing to do
530 break;
534 // signal completions
535 forEachItem(mroot, (FuiLayoutProps it) { it.layoutingComplete(); });
539 debug(flexlayout_dump) void dumpLayout() (FuiLayoutProps mroot, const(char)[] foname=null) {
540 import core.stdc.stdio : stderr, fopen, fclose, fprintf;
541 import std.internal.cstring;
543 auto fo = (foname.length ? stderr : fopen(foname.tempCString, "w"));
544 if (fo is null) return;
545 scope(exit) if (foname.length) fclose(fo);
547 void ind (int indent) { foreach (immutable _; 0..indent) fo.fprintf(" "); }
549 void dumpItem() (FuiLayoutProps lp, int indent) {
550 if (lp is null || !lp.visible) return;
551 ind(indent);
552 fo.fprintf("Ctl#%08x: position:(%d,%d); size:(%d,%d)\n", cast(uint)cast(void*)lp, lp.pos.x, lp.pos.y, lp.size.w, lp.size.h);
553 for (lp = lp.firstChild; lp !is null; lp = lp.nextSibling) {
554 if (!lp.visible) continue;
555 dumpItem(lp, indent+2);
559 dumpItem(mroot, 0);