cmdcon: added `glconHide()`
[iv.d.git] / flexlayout.d
blob24730c71fd65a21fc0a2b68d2108676ab07ea2a0
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 // ////////////////////////////////////////////////////////////////////////// //
25 /// point
26 public align(1) struct FuiPoint {
27 align(1):
28 int x, y;
29 @property pure nothrow @safe @nogc:
30 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); }
31 ref FuiPoint opOpAssign(string op) (in auto ref FuiPoint pt) if (op == "+" || op == "-") {
32 mixin("x"~op~"=pt.x; y"~op~"=pt.y;");
33 return this;
35 FuiPoint opBinary(string op) (in auto ref FuiPoint pt) if (op == "+" || op == "-") {
36 mixin("return FuiPoint(x"~op~"pt.x, y"~op~"pt.y);");
38 int opIndex (usize idx) const { pragma(inline, true); return (idx == 0 ? x : idx == 1 ? y : 0); }
39 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx == 0) x = v; else if (idx == 1) y = v; }
42 /// size
43 public align(1) struct FuiSize {
44 align(1):
45 int w, h;
46 @property pure nothrow @safe @nogc:
47 int opIndex (usize idx) const { pragma(inline, true); return (idx == 0 ? w : idx == 1 ? h : 0); }
48 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx == 0) w = v; else if (idx == 1) h = v; }
52 /// rectangle
53 public align(1) struct FuiRect {
54 align(1):
55 FuiPoint pos;
56 FuiSize size;
57 @property pure nothrow @safe @nogc:
58 int x () const { pragma(inline, true); return pos.x; }
59 int y () const { pragma(inline, true); return pos.y; }
60 int w () const { pragma(inline, true); return size.w; }
61 int h () const { pragma(inline, true); return size.h; }
62 void x (int v) { pragma(inline, true); pos.x = v; }
63 void y (int v) { pragma(inline, true); pos.y = v; }
64 void w (int v) { pragma(inline, true); size.w = v; }
65 void h (int v) { pragma(inline, true); size.h = v; }
67 ref int xp () { pragma(inline, true); return pos.x; }
68 ref int yp () { pragma(inline, true); return pos.y; }
69 ref int wp () { pragma(inline, true); return size.w; }
70 ref int hp () { pragma(inline, true); return size.h; }
72 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); }
76 /// margins
77 public align(1) struct FuiMargin {
78 align(1):
79 int[4] ltrb;
80 pure nothrow @trusted @nogc:
81 this (const(int)[] v...) { if (v.length > 4) v = v[0..4]; ltrb[0..v.length] = v[]; }
82 @property:
83 int left () const { pragma(inline, true); return ltrb.ptr[0]; }
84 int top () const { pragma(inline, true); return ltrb.ptr[1]; }
85 int right () const { pragma(inline, true); return ltrb.ptr[2]; }
86 int bottom () const { pragma(inline, true); return ltrb.ptr[3]; }
87 void left (int v) { pragma(inline, true); ltrb.ptr[0] = v; }
88 void top (int v) { pragma(inline, true); ltrb.ptr[1] = v; }
89 void right (int v) { pragma(inline, true); ltrb.ptr[2] = v; }
90 void bottom (int v) { pragma(inline, true); ltrb.ptr[3] = v; }
91 int opIndex (usize idx) const { pragma(inline, true); return (idx < 4 ? ltrb.ptr[idx] : 0); }
92 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx < 4) ltrb.ptr[idx] = v; }
96 // ////////////////////////////////////////////////////////////////////////// //
97 /// properties for layouter
98 public class FuiLayoutProps {
99 ///
100 enum Orientation {
101 Horizontal, ///
102 Vertical, ///
105 /// "NPD" means "non-packing direction"
106 enum Align {
107 Center, /// the available space is divided evenly
108 Start, /// the NPD edge of each box is placed along the NPD of the parent box
109 End, /// the opposite-NPD edge of each box is placed along the opposite-NPD of the parent box
110 Stretch, /// the NPD-size of each boxes is adjusted to fill the parent box
113 void layoutingStarted () {} /// called before layouting starts
114 void layoutingComplete () {} /// called after layouting complete
116 //WARNING! the following properties should be set to correct values before layouting
117 // you can use `layoutingStarted()` method to do this
119 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)
120 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)
121 bool ignoreSpacing; /// (should be set to correct values before layouting; you can use `layoutingStarted()` method to do this)
123 Orientation orientation = Orientation.Horizontal; /// box orientation
124 Align aligning = Align.Start; /// NPD for children; sadly, "align" keyword is reserved
125 int flex; /// <=0: not flexible
127 FuiMargin padding; /// padding for this widget
128 int spacing; /// spacing for children
129 int lineSpacing; /// line spacing for horizontal boxes
130 FuiSize minSize; /// minimal control size
131 FuiSize maxSize; /// maximal control size (0 means "unlimited")
133 /// controls in a horizontal group has the same width, and the same height in a vertical group
134 FuiLayoutProps[Orientation.max+1] groupNext; /// next sibling for this control's group or null
136 /// calculated item dimensions
137 FuiRect rect;
138 final @property ref inout(FuiPoint) pos () pure inout nothrow @safe @nogc { pragma(inline, true); return rect.pos; } ///
139 final @property ref inout(FuiSize) size () pure inout nothrow @safe @nogc { pragma(inline, true); return rect.size; } ///
141 FuiLayoutProps parent; /// null for root element
142 FuiLayoutProps firstChild; /// null for "no children"
143 FuiLayoutProps nextSibling; /// null for last item
145 /// you can specify your own root if necessary
146 final FuiPoint toGlobal (FuiPoint pt, FuiLayoutProps root=null) const pure nothrow @trusted @nogc {
147 for (FuiLayoutProps it = cast(FuiLayoutProps)this; it !is null; it = it.parent) {
148 pt.x += it.pos.x;
149 pt.y += it.pos.y;
150 if (it is root) break;
152 return pt;
155 /// you can specify your own root if necessary
156 final FuiPoint toLocal (FuiPoint pt, FuiLayoutProps root=null) const pure nothrow @trusted @nogc {
157 for (FuiLayoutProps it = cast(FuiLayoutProps)this; it !is null; it = it.parent) {
158 pt.x -= it.pos.x;
159 pt.y -= it.pos.y;
160 if (it is root) break;
162 return pt;
165 private:
166 // internal housekeeping for layouter
167 FuiLayoutProps[Orientation.max+1] groupHead;
168 bool tempLineBreak;
170 final:
171 void resetLayouterFlags () { pragma(inline, true); tempLineBreak = false; groupHead[] = null; }
175 // ////////////////////////////////////////////////////////////////////////// //
176 // you can set maximum dimesions by setting root panel maxSize
177 // visit `root` and it's children
178 private static void forEachItem (FuiLayoutProps root, scope void delegate (FuiLayoutProps it) dg) {
179 void visitAll (FuiLayoutProps it) {
180 while (it !is null) {
181 dg(it);
182 visitAll(it.firstChild);
183 it = it.nextSibling;
186 if (root is null || dg is null) return;
187 dg(root);
188 visitAll(root.firstChild);
192 /// do layouting
193 void flexLayout (FuiLayoutProps aroot) {
194 import std.algorithm : min, max;
196 if (aroot is null) return;
197 auto oparent = aroot.parent;
198 auto onexts = aroot.nextSibling;
199 aroot.parent = null;
200 aroot.nextSibling = null;
201 scope(exit) { aroot.parent = oparent; aroot.nextSibling = onexts; }
202 auto mroot = aroot;
204 // layout children in this item
205 void layit() (FuiLayoutProps lp) {
206 if (lp is null || !lp.visible) return;
208 // cache values
209 immutable bpadLeft = max(0, lp.padding.left);
210 immutable bpadRight = max(0, lp.padding.right);
211 immutable bpadTop = max(0, lp.padding.top);
212 immutable bpadBottom = max(0, lp.padding.bottom);
213 immutable bspc = max(0, lp.spacing);
214 immutable hbox = (lp.orientation == FuiLayoutProps.Orientation.Horizontal);
216 // widget can only grow, and while doing that, `maxSize` will be respected, so we don't need to fix it's size
218 // layout children, insert line breaks, if necessary
219 int curWidth = bpadLeft+bpadRight, maxW = bpadLeft+bpadRight, maxH = bpadTop+bpadBottom;
220 FuiLayoutProps lastCIdx = null; // last processed item for the current line
221 int lineH = 0; // for the current line
222 int lineCount = 0;
223 int lineMaxW = (lp.size.w > 0 ? lp.size.w : (lp.maxSize.w > 0 ? lp.maxSize.w : int.max));
225 // unconditionally add current item to the current line
226 void addToLine (FuiLayoutProps clp) {
227 clp.tempLineBreak = false;
228 curWidth += clp.size.w+(lastCIdx !is null && !lastCIdx.ignoreSpacing ? bspc : 0);
229 lineH = max(lineH, clp.size.h);
230 lastCIdx = clp;
233 // flush current line
234 void flushLine () {
235 if (lastCIdx is null) return;
236 // mark last item as line break
237 lastCIdx.tempLineBreak = true;
238 // fix max width
239 maxW = max(maxW, curWidth);
240 // fix max height
241 maxH += lineH+(lineCount ? lp.lineSpacing : 0);
242 // restart line
243 curWidth = bpadLeft+bpadRight;
244 lastCIdx = null;
245 lineH = 0;
246 ++lineCount;
249 // put item, do line management
250 void putItem (FuiLayoutProps clp) {
251 int nw = curWidth+clp.size.w+(lastCIdx !is null && !lastCIdx.ignoreSpacing ? bspc : 0);
252 // do we neeed to start a new line?
253 if (nw <= lineMaxW) {
254 // no, just put item into the current line
255 addToLine(clp);
256 return;
258 // yes, check if we have at least one item in the current line
259 if (lastCIdx is null) {
260 // alas, no items in the current line, put clp into it anyway
261 addToLine(clp);
262 // and flush line immediately
263 flushLine();
264 } else {
265 // flush current line
266 flushLine();
267 // and add this item to new one
268 addToLine(clp);
272 // layout children, insert "soft" line breaks
273 for (auto clp = lp.firstChild, cspc = 0; clp !is null; clp = clp.nextSibling) {
274 if (!clp.visible) continue; // invisible, skip it
275 layit(clp); // layout children of this box
276 if (hbox) {
277 // for horizontal box, logic is somewhat messy
278 putItem(clp);
279 if (clp.lineBreak) flushLine();
280 } else {
281 // for vertical box, it is as easy as this
282 clp.tempLineBreak = true;
283 maxW = max(maxW, clp.size.w+bpadLeft+bpadRight);
284 maxH += clp.size.h+cspc;
285 cspc = (clp.ignoreSpacing ? 0 : bspc);
286 ++lineCount;
289 if (hbox) flushLine(); // flush last line for horizontal box (it is safe to flush empty line)
290 // fix max sizes
291 if (lp.maxSize.w > 0 && maxW > lp.maxSize.w) maxW = lp.maxSize.w;
292 if (lp.maxSize.h > 0 && maxH > lp.maxSize.h) maxH = lp.maxSize.h;
294 // grow box or clamp max size
295 // but only if size is not defined; in other cases our size is changed by parent to fit in
296 if (lp.size.w == 0) lp.size.w = max(0, lp.minSize.w, maxW);
297 if (lp.size.h == 0) lp.size.h = max(0, lp.minSize.h, maxH);
298 // cache values
299 maxH = lp.size.h;
300 maxW = lp.size.w;
302 int flexTotal; // total sum of flex fields
303 int flexBoxCount; // number of boxes
304 int curSpc; // "current" spacing in layout calculations (for bspc)
305 int spaceLeft;
307 if (hbox) {
308 // layout horizontal box; we should do this for each line separately
309 int lineStartY = bpadTop;
311 void resetLine () {
312 flexTotal = 0;
313 flexBoxCount = 0;
314 curSpc = 0;
315 spaceLeft = maxW-(bpadLeft+bpadRight);
316 lineH = 0;
319 auto lstart = lp.firstChild;
320 int lineNum = 0;
321 for (;;) {
322 if (lstart is null) break;
323 if (!lstart.visible) continue;
324 // calculate flex variables and line height
325 --lineCount; // so 0 will be "last line"
326 assert(lineCount >= 0);
327 resetLine();
328 for (auto clp = lstart; clp !is null; clp = clp.nextSibling) {
329 if (!clp.visible) continue;
330 auto dim = clp.size.w+curSpc;
331 spaceLeft -= dim;
332 lineH = max(lineH, clp.size.h);
333 // process flex
334 if (clp.flex > 0) { flexTotal += clp.flex; ++flexBoxCount; }
335 if (clp.tempLineBreak) break; // no more in this line
336 curSpc = (clp.ignoreSpacing ? 0 : bspc);
338 if (lineCount == 0) lineH = max(lineH, maxH-bpadBottom-lineStartY-lineH);
339 debug(fui_layout) { import core.stdc.stdio : printf; printf("lineStartY=%d; lineH=%d\n", lineStartY, lineH); }
341 // distribute flex space, fix coordinates
342 debug(fui_layout) { import core.stdc.stdio : printf; printf("flexTotal=%d; flexBoxCount=%d; spaceLeft=%d\n", flexTotal, flexBoxCount, spaceLeft); }
343 if (spaceLeft < 0) spaceLeft = 0;
344 float flt = cast(float)flexTotal;
345 float left = cast(float)spaceLeft;
346 //{ import iv.vfs.io; VFile("zlay.log", "a").writefln("flt=%s; left=%s", flt, left); }
347 int curpos = bpadLeft;
348 for (auto clp = lstart; clp !is null; clp = clp.nextSibling) {
349 lstart = clp.nextSibling;
350 if (!clp.visible) continue;
351 // fix packing coordinate
352 clp.pos.x = curpos;
353 bool doChildrenRelayout = false;
354 // fix non-packing coordinate (and, maybe, non-packing dimension)
355 // fix y coord
356 final switch (clp.aligning) {
357 case FuiLayoutProps.Align.Start: clp.pos.y = lineStartY; break;
358 case FuiLayoutProps.Align.End: clp.pos.y = (lineStartY+lineH)-clp.size.h; break;
359 case FuiLayoutProps.Align.Center: clp.pos.y = lineStartY+(lineH-clp.size.h)/2; break;
360 case FuiLayoutProps.Align.Stretch:
361 clp.pos.y = lineStartY;
362 int nd = min(max(0, lineH, clp.minSize.h), (clp.maxSize.h > 0 ? clp.maxSize.h : int.max));
363 if (nd != clp.size.h) {
364 // size changed, relayout children
365 doChildrenRelayout = true;
366 clp.size.h = nd;
368 break;
370 // fix flexbox size
371 if (clp.flex > 0) {
372 //{ import iv.vfs.io; write("\x07"); }
373 int toadd = cast(int)(left*cast(float)clp.flex/flt+0.5);
374 if (toadd > 0) {
375 // size changed, relayout children
376 doChildrenRelayout = true;
377 clp.size.w += toadd;
378 // compensate (crudely) rounding errors
379 if (toadd > 1 && clp.size.w <= maxW && maxW-(curpos+clp.size.w) < 0) clp.size.w -= 1;
382 // advance packing coordinate
383 curpos += clp.size.w+(clp.ignoreSpacing ? 0 : bspc);
384 // relayout children if dimensions was changed
385 if (doChildrenRelayout) layit(clp);
386 if (clp.tempLineBreak) break; // exit if we have linebreak
387 // next line, please!
389 // yep, move to next line
390 debug(fui_layout) { import core.stdc.stdio : printf; printf("lineStartY=%d; next lineStartY=%d\n", lineStartY, lineStartY+lineH+lp.lineSpacing); }
391 lineStartY += lineH+lp.lineSpacing;
393 } else {
394 // layout vertical box, it is much easier
395 spaceLeft = maxH-(bpadTop+bpadBottom);
396 if (spaceLeft < 0) spaceLeft = 0;
398 // calculate flex variables
399 for (auto clp = lp.firstChild; clp !is null; clp = clp.nextSibling) {
400 if (!clp.visible) continue;
401 auto dim = clp.size.h+curSpc;
402 spaceLeft -= dim;
403 // process flex
404 if (clp.flex > 0) { flexTotal += clp.flex; ++flexBoxCount; }
405 curSpc = (clp.ignoreSpacing ? 0 : bspc);
408 // distribute flex space, fix coordinates
409 float flt = cast(float)flexTotal;
410 float left = cast(float)spaceLeft;
411 int curpos = bpadTop;
412 for (auto clp = lp.firstChild; clp !is null; clp = clp.nextSibling) {
413 if (!clp.visible) break;
414 // fix packing coordinate
415 clp.pos.y = curpos;
416 bool doChildrenRelayout = false;
417 // fix non-packing coordinate (and, maybe, non-packing dimension)
418 // fix x coord
419 final switch (clp.aligning) {
420 case FuiLayoutProps.Align.Start: clp.pos.x = bpadLeft; break;
421 case FuiLayoutProps.Align.End: clp.pos.x = maxW-bpadRight-clp.size.w; break;
422 case FuiLayoutProps.Align.Center: clp.pos.x = (maxW-clp.size.w)/2; break;
423 case FuiLayoutProps.Align.Stretch:
424 int nd = min(max(0, maxW-(bpadLeft+bpadRight), clp.minSize.w), (clp.maxSize.w > 0 ? clp.maxSize.w : int.max));
425 if (nd != clp.size.w) {
426 // size changed, relayout children
427 doChildrenRelayout = true;
428 clp.size.w = nd;
430 clp.pos.x = bpadLeft;
431 break;
433 // fix flexbox size
434 if (clp.flex > 0) {
435 int toadd = cast(int)(left*cast(float)clp.flex/flt);
436 if (toadd > 0) {
437 // size changed, relayout children
438 doChildrenRelayout = true;
439 clp.size.h += toadd;
440 // compensate (crudely) rounding errors
441 if (toadd > 1 && clp.size.h <= maxH && maxH-(curpos+clp.size.h) < 0) clp.size.h -= 1;
444 // advance packing coordinate
445 curpos += clp.size.h+(clp.ignoreSpacing ? bspc : 0);
446 // relayout children if dimensions was changed
447 if (doChildrenRelayout) layit(clp);
449 // that's all for vertical boxes
453 // main code
454 if (mroot is null) return;
456 bool[FuiLayoutProps.Orientation.max+1] seenGroup = false;
458 // reset flags, check if we have any groups
459 forEachItem(mroot, (FuiLayoutProps it) {
460 it.layoutingStarted();
461 it.resetLayouterFlags();
462 it.pos = it.pos.init;
463 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) if (it.groupNext[gidx] !is null) seenGroup[gidx] = true;
464 if (!it.visible) { it.size = it.size.init; return; }
465 it.size = it.size.init;
468 if (seenGroup[0] || seenGroup[1]) {
469 // fix groups
470 forEachItem(mroot, (FuiLayoutProps it) {
471 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) {
472 if (it.groupNext[gidx] is null || it.groupHead[gidx] !is null) continue;
473 // this item is group member, but has no head set, so this is new head: fix the whole list
474 for (FuiLayoutProps gm = it; gm !is null; gm = gm.groupNext[gidx]) gm.groupHead[gidx] = it;
479 // do top-level packing
480 for (;;) {
481 layit(mroot);
482 bool doFix = false;
484 //FIXME: mark changed items and process only those
485 void fixGroups (FuiLayoutProps it, int grp) nothrow @nogc {
486 int dim = 0;
487 // calcluate maximal dimension
488 for (FuiLayoutProps clp = it; clp !is null; clp = clp.groupNext[grp]) {
489 if (!clp.visible) continue;
490 dim = max(dim, clp.size[grp]);
492 // fix dimensions
493 for (FuiLayoutProps clp = it; clp !is null; clp = clp.groupNext[grp]) {
494 if (!clp.visible) continue;
495 auto od = clp.size[grp];
496 int nd = max(od, dim);
497 auto mx = clp.maxSize[grp];
498 if (mx > 0) nd = min(nd, mx);
499 version(none) {
500 import core.stdc.stdio;
501 auto fo = fopen("zlx.log", "a");
502 //fo.fprintf("%.*s: od=%d; nd=%d\n", cast(uint)clp.classinfo.name.length, clp.classinfo.name.ptr, od, nd);
503 fo.fprintf("gidx=%d; dim=%d; w=%d; h=%d\n", grp, dim, clp.size[0], clp.size[1]);
504 fo.fclose();
506 if (od != nd) {
507 doFix = true;
508 clp.size[grp] = nd;
513 if (seenGroup[0] || seenGroup[1]) {
514 forEachItem(mroot, (FuiLayoutProps it) {
515 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) {
516 if (it.groupHead[gidx] is it) fixGroups(it, gidx);
519 if (!doFix) break; // nothing to do
520 } else {
521 // no groups -> nothing to do
522 break;
526 // signal completions
527 forEachItem(mroot, (FuiLayoutProps it) { it.layoutingComplete(); });
531 debug(flexlayout_dump) void dumpLayout() (FuiLayoutProps mroot, const(char)[] foname=null) {
532 import core.stdc.stdio : stderr, fopen, fclose, fprintf;
533 import std.internal.cstring;
535 auto fo = (foname.length ? stderr : fopen(foname.tempCString, "w"));
536 if (fo is null) return;
537 scope(exit) if (foname.length) fclose(fo);
539 void ind (int indent) { foreach (immutable _; 0..indent) fo.fprintf(" "); }
541 void dumpItem() (FuiLayoutProps lp, int indent) {
542 if (lp is null || !lp.visible) return;
543 ind(indent);
544 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);
545 for (lp = lp.firstChild; lp !is null; lp = lp.nextSibling) {
546 if (!lp.visible) continue;
547 dumpItem(lp, indent+2);
551 dumpItem(mroot, 0);