iv.nanovega: documented FontStash text bounds iterator
[iv.d.git] / flexlayout.d
blobc26ad3a6a6c4c3bb9b1aa0473b66b1d8751cbf9b
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, either version 3 of the License, or
10 * (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
17 * You should have received a copy of the GNU General Public License
18 * along with this program. If not, see <http://www.gnu.org/licenses/>.
20 /// this engine can layout any boxset (if it is valid)
21 module iv.flexlayout /*is aliced*/;
22 import iv.alice;
25 // ////////////////////////////////////////////////////////////////////////// //
26 /// point
27 public align(1) struct FuiPoint {
28 align(1):
29 int x, y;
30 @property pure nothrow @safe @nogc:
31 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); }
32 ref FuiPoint opOpAssign(string op) (in auto ref FuiPoint pt) if (op == "+" || op == "-") {
33 mixin("x"~op~"=pt.x; y"~op~"=pt.y;");
34 return this;
36 FuiPoint opBinary(string op) (in auto ref FuiPoint pt) if (op == "+" || op == "-") {
37 mixin("return FuiPoint(x"~op~"pt.x, y"~op~"pt.y);");
39 int opIndex (usize idx) const { pragma(inline, true); return (idx == 0 ? x : idx == 1 ? y : 0); }
40 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx == 0) x = v; else if (idx == 1) y = v; }
43 /// size
44 public align(1) struct FuiSize {
45 align(1):
46 int w, h;
47 @property pure nothrow @safe @nogc:
48 int opIndex (usize idx) const { pragma(inline, true); return (idx == 0 ? w : idx == 1 ? h : 0); }
49 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx == 0) w = v; else if (idx == 1) h = v; }
53 /// rectangle
54 public align(1) struct FuiRect {
55 align(1):
56 FuiPoint pos;
57 FuiSize size;
58 @property pure nothrow @safe @nogc:
59 int x () const { pragma(inline, true); return pos.x; }
60 int y () const { pragma(inline, true); return pos.y; }
61 int w () const { pragma(inline, true); return size.w; }
62 int h () const { pragma(inline, true); return size.h; }
63 void x (int v) { pragma(inline, true); pos.x = v; }
64 void y (int v) { pragma(inline, true); pos.y = v; }
65 void w (int v) { pragma(inline, true); size.w = v; }
66 void h (int v) { pragma(inline, true); size.h = v; }
68 ref int xp () { pragma(inline, true); return pos.x; }
69 ref int yp () { pragma(inline, true); return pos.y; }
70 ref int wp () { pragma(inline, true); return size.w; }
71 ref int hp () { pragma(inline, true); return size.h; }
73 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); }
77 /// margins
78 public align(1) struct FuiMargin {
79 align(1):
80 int[4] ltrb;
81 pure nothrow @trusted @nogc:
82 this (const(int)[] v...) { if (v.length > 4) v = v[0..4]; ltrb[0..v.length] = v[]; }
83 @property:
84 int left () const { pragma(inline, true); return ltrb.ptr[0]; }
85 int top () const { pragma(inline, true); return ltrb.ptr[1]; }
86 int right () const { pragma(inline, true); return ltrb.ptr[2]; }
87 int bottom () const { pragma(inline, true); return ltrb.ptr[3]; }
88 void left (int v) { pragma(inline, true); ltrb.ptr[0] = v; }
89 void top (int v) { pragma(inline, true); ltrb.ptr[1] = v; }
90 void right (int v) { pragma(inline, true); ltrb.ptr[2] = v; }
91 void bottom (int v) { pragma(inline, true); ltrb.ptr[3] = v; }
92 int opIndex (usize idx) const { pragma(inline, true); return (idx < 4 ? ltrb.ptr[idx] : 0); }
93 void opIndexAssign (int v, usize idx) { pragma(inline, true); if (idx < 4) ltrb.ptr[idx] = v; }
97 // ////////////////////////////////////////////////////////////////////////// //
98 /// properties for layouter
99 public class FuiLayoutProps {
101 enum Orientation {
102 Horizontal, ///
103 Vertical, ///
106 /// "NPD" means "non-packing direction"
107 enum Align {
108 Center, /// the available space is divided evenly
109 Start, /// the NPD edge of each box is placed along the NPD of the parent box
110 End, /// the opposite-NPD edge of each box is placed along the opposite-NPD of the parent box
111 Stretch, /// the NPD-size of each boxes is adjusted to fill the parent box
114 void layoutingStarted () {} /// called before layouting starts
115 void layoutingComplete () {} /// called after layouting complete
117 //WARNING! the following properties should be set to correct values before layouting
118 // you can use `layoutingStarted()` method to do this
120 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)
121 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)
122 bool ignoreSpacing; /// (should be set to correct values before layouting; you can use `layoutingStarted()` method to do this)
124 Orientation orientation = Orientation.Horizontal; /// box orientation
125 Align aligning = Align.Start; /// NPD for children; sadly, "align" keyword is reserved
126 int flex; /// <=0: not flexible
128 FuiMargin padding; /// padding for this widget
129 int spacing; /// spacing for children
130 int lineSpacing; /// line spacing for horizontal boxes
131 FuiSize minSize; /// minimal control size
132 FuiSize maxSize; /// maximal control size (0 means "unlimited")
134 /// controls in a horizontal group has the same width, and the same height in a vertical group
135 FuiLayoutProps[Orientation.max+1] groupNext; /// next sibling for this control's group or null
137 /// calculated item dimensions
138 FuiRect rect;
139 final @property ref inout(FuiPoint) pos () pure inout nothrow @safe @nogc { pragma(inline, true); return rect.pos; } ///
140 final @property ref inout(FuiSize) size () pure inout nothrow @safe @nogc { pragma(inline, true); return rect.size; } ///
142 FuiLayoutProps parent; /// null for root element
143 FuiLayoutProps firstChild; /// null for "no children"
144 FuiLayoutProps nextSibling; /// null for last item
146 /// you can specify your own root if necessary
147 final FuiPoint toGlobal (FuiPoint pt, FuiLayoutProps root=null) const pure nothrow @trusted @nogc {
148 for (FuiLayoutProps it = cast(FuiLayoutProps)this; it !is null; it = it.parent) {
149 pt.x += it.pos.x;
150 pt.y += it.pos.y;
151 if (it is root) break;
153 return pt;
156 /// you can specify your own root if necessary
157 final FuiPoint toLocal (FuiPoint pt, FuiLayoutProps root=null) const pure nothrow @trusted @nogc {
158 for (FuiLayoutProps it = cast(FuiLayoutProps)this; it !is null; it = it.parent) {
159 pt.x -= it.pos.x;
160 pt.y -= it.pos.y;
161 if (it is root) break;
163 return pt;
166 private:
167 // internal housekeeping for layouter
168 FuiLayoutProps[Orientation.max+1] groupHead;
169 bool tempLineBreak;
171 final:
172 void resetLayouterFlags () { pragma(inline, true); tempLineBreak = false; groupHead[] = null; }
176 // ////////////////////////////////////////////////////////////////////////// //
177 // you can set maximum dimesions by setting root panel maxSize
178 // visit `root` and it's children
179 private static void forEachItem (FuiLayoutProps root, scope void delegate (FuiLayoutProps it) dg) {
180 void visitAll (FuiLayoutProps it) {
181 while (it !is null) {
182 dg(it);
183 visitAll(it.firstChild);
184 it = it.nextSibling;
187 if (root is null || dg is null) return;
188 dg(root);
189 visitAll(root.firstChild);
193 /// do layouting
194 void flexLayout (FuiLayoutProps aroot) {
195 import std.algorithm : min, max;
197 if (aroot is null) return;
198 auto oparent = aroot.parent;
199 auto onexts = aroot.nextSibling;
200 aroot.parent = null;
201 aroot.nextSibling = null;
202 scope(exit) { aroot.parent = oparent; aroot.nextSibling = onexts; }
203 auto mroot = aroot;
205 // layout children in this item
206 void layit() (FuiLayoutProps lp) {
207 if (lp is null || !lp.visible) return;
209 // cache values
210 immutable bpadLeft = max(0, lp.padding.left);
211 immutable bpadRight = max(0, lp.padding.right);
212 immutable bpadTop = max(0, lp.padding.top);
213 immutable bpadBottom = max(0, lp.padding.bottom);
214 immutable bspc = max(0, lp.spacing);
215 immutable hbox = (lp.orientation == FuiLayoutProps.Orientation.Horizontal);
217 // widget can only grow, and while doing that, `maxSize` will be respected, so we don't need to fix it's size
219 // layout children, insert line breaks, if necessary
220 int curWidth = bpadLeft+bpadRight, maxW = bpadLeft+bpadRight, maxH = bpadTop+bpadBottom;
221 FuiLayoutProps lastCIdx = null; // last processed item for the current line
222 int lineH = 0; // for the current line
223 int lineCount = 0;
224 int lineMaxW = (lp.size.w > 0 ? lp.size.w : (lp.maxSize.w > 0 ? lp.maxSize.w : int.max));
226 // unconditionally add current item to the current line
227 void addToLine (FuiLayoutProps clp) {
228 clp.tempLineBreak = false;
229 curWidth += clp.size.w+(lastCIdx !is null && !lastCIdx.ignoreSpacing ? bspc : 0);
230 lineH = max(lineH, clp.size.h);
231 lastCIdx = clp;
234 // flush current line
235 void flushLine () {
236 if (lastCIdx is null) return;
237 // mark last item as line break
238 lastCIdx.tempLineBreak = true;
239 // fix max width
240 maxW = max(maxW, curWidth);
241 // fix max height
242 maxH += lineH+(lineCount ? lp.lineSpacing : 0);
243 // restart line
244 curWidth = bpadLeft+bpadRight;
245 lastCIdx = null;
246 lineH = 0;
247 ++lineCount;
250 // put item, do line management
251 void putItem (FuiLayoutProps clp) {
252 int nw = curWidth+clp.size.w+(lastCIdx !is null && !lastCIdx.ignoreSpacing ? bspc : 0);
253 // do we neeed to start a new line?
254 if (nw <= lineMaxW) {
255 // no, just put item into the current line
256 addToLine(clp);
257 return;
259 // yes, check if we have at least one item in the current line
260 if (lastCIdx is null) {
261 // alas, no items in the current line, put clp into it anyway
262 addToLine(clp);
263 // and flush line immediately
264 flushLine();
265 } else {
266 // flush current line
267 flushLine();
268 // and add this item to new one
269 addToLine(clp);
273 // layout children, insert "soft" line breaks
274 for (auto clp = lp.firstChild, cspc = 0; clp !is null; clp = clp.nextSibling) {
275 if (!clp.visible) continue; // invisible, skip it
276 layit(clp); // layout children of this box
277 if (hbox) {
278 // for horizontal box, logic is somewhat messy
279 putItem(clp);
280 if (clp.lineBreak) flushLine();
281 } else {
282 // for vertical box, it is as easy as this
283 clp.tempLineBreak = true;
284 maxW = max(maxW, clp.size.w+bpadLeft+bpadRight);
285 maxH += clp.size.h+cspc;
286 cspc = (clp.ignoreSpacing ? 0 : bspc);
287 ++lineCount;
290 if (hbox) flushLine(); // flush last line for horizontal box (it is safe to flush empty line)
291 // fix max sizes
292 if (lp.maxSize.w > 0 && maxW > lp.maxSize.w) maxW = lp.maxSize.w;
293 if (lp.maxSize.h > 0 && maxH > lp.maxSize.h) maxH = lp.maxSize.h;
295 // grow box or clamp max size
296 // but only if size is not defined; in other cases our size is changed by parent to fit in
297 if (lp.size.w == 0) lp.size.w = max(0, lp.minSize.w, maxW);
298 if (lp.size.h == 0) lp.size.h = max(0, lp.minSize.h, maxH);
299 // cache values
300 maxH = lp.size.h;
301 maxW = lp.size.w;
303 int flexTotal; // total sum of flex fields
304 int flexBoxCount; // number of boxes
305 int curSpc; // "current" spacing in layout calculations (for bspc)
306 int spaceLeft;
308 if (hbox) {
309 // layout horizontal box; we should do this for each line separately
310 int lineStartY = bpadTop;
312 void resetLine () {
313 flexTotal = 0;
314 flexBoxCount = 0;
315 curSpc = 0;
316 spaceLeft = maxW-(bpadLeft+bpadRight);
317 lineH = 0;
320 auto lstart = lp.firstChild;
321 int lineNum = 0;
322 for (;;) {
323 if (lstart is null) break;
324 if (!lstart.visible) continue;
325 // calculate flex variables and line height
326 --lineCount; // so 0 will be "last line"
327 assert(lineCount >= 0);
328 resetLine();
329 for (auto clp = lstart; clp !is null; clp = clp.nextSibling) {
330 if (!clp.visible) continue;
331 auto dim = clp.size.w+curSpc;
332 spaceLeft -= dim;
333 lineH = max(lineH, clp.size.h);
334 // process flex
335 if (clp.flex > 0) { flexTotal += clp.flex; ++flexBoxCount; }
336 if (clp.tempLineBreak) break; // no more in this line
337 curSpc = (clp.ignoreSpacing ? 0 : bspc);
339 if (lineCount == 0) lineH = max(lineH, maxH-bpadBottom-lineStartY-lineH);
340 debug(fui_layout) { import core.stdc.stdio : printf; printf("lineStartY=%d; lineH=%d\n", lineStartY, lineH); }
342 // distribute flex space, fix coordinates
343 debug(fui_layout) { import core.stdc.stdio : printf; printf("flexTotal=%d; flexBoxCount=%d; spaceLeft=%d\n", flexTotal, flexBoxCount, spaceLeft); }
344 if (spaceLeft < 0) spaceLeft = 0;
345 float flt = cast(float)flexTotal;
346 float left = cast(float)spaceLeft;
347 //{ import iv.vfs.io; VFile("zlay.log", "a").writefln("flt=%s; left=%s", flt, left); }
348 int curpos = bpadLeft;
349 for (auto clp = lstart; clp !is null; clp = clp.nextSibling) {
350 lstart = clp.nextSibling;
351 if (!clp.visible) continue;
352 // fix packing coordinate
353 clp.pos.x = curpos;
354 bool doChildrenRelayout = false;
355 // fix non-packing coordinate (and, maybe, non-packing dimension)
356 // fix y coord
357 final switch (clp.aligning) {
358 case FuiLayoutProps.Align.Start: clp.pos.y = lineStartY; break;
359 case FuiLayoutProps.Align.End: clp.pos.y = (lineStartY+lineH)-clp.size.h; break;
360 case FuiLayoutProps.Align.Center: clp.pos.y = lineStartY+(lineH-clp.size.h)/2; break;
361 case FuiLayoutProps.Align.Stretch:
362 clp.pos.y = lineStartY;
363 int nd = min(max(0, lineH, clp.minSize.h), (clp.maxSize.h > 0 ? clp.maxSize.h : int.max));
364 if (nd != clp.size.h) {
365 // size changed, relayout children
366 doChildrenRelayout = true;
367 clp.size.h = nd;
369 break;
371 // fix flexbox size
372 if (clp.flex > 0) {
373 //{ import iv.vfs.io; write("\x07"); }
374 int toadd = cast(int)(left*cast(float)clp.flex/flt+0.5);
375 if (toadd > 0) {
376 // size changed, relayout children
377 doChildrenRelayout = true;
378 clp.size.w += toadd;
379 // compensate (crudely) rounding errors
380 if (toadd > 1 && clp.size.w <= maxW && maxW-(curpos+clp.size.w) < 0) clp.size.w -= 1;
383 // advance packing coordinate
384 curpos += clp.size.w+(clp.ignoreSpacing ? 0 : bspc);
385 // relayout children if dimensions was changed
386 if (doChildrenRelayout) layit(clp);
387 if (clp.tempLineBreak) break; // exit if we have linebreak
388 // next line, please!
390 // yep, move to next line
391 debug(fui_layout) { import core.stdc.stdio : printf; printf("lineStartY=%d; next lineStartY=%d\n", lineStartY, lineStartY+lineH+lp.lineSpacing); }
392 lineStartY += lineH+lp.lineSpacing;
394 } else {
395 // layout vertical box, it is much easier
396 spaceLeft = maxH-(bpadTop+bpadBottom);
397 if (spaceLeft < 0) spaceLeft = 0;
399 // calculate flex variables
400 for (auto clp = lp.firstChild; clp !is null; clp = clp.nextSibling) {
401 if (!clp.visible) continue;
402 auto dim = clp.size.h+curSpc;
403 spaceLeft -= dim;
404 // process flex
405 if (clp.flex > 0) { flexTotal += clp.flex; ++flexBoxCount; }
406 curSpc = (clp.ignoreSpacing ? 0 : bspc);
409 // distribute flex space, fix coordinates
410 float flt = cast(float)flexTotal;
411 float left = cast(float)spaceLeft;
412 int curpos = bpadTop;
413 for (auto clp = lp.firstChild; clp !is null; clp = clp.nextSibling) {
414 if (!clp.visible) break;
415 // fix packing coordinate
416 clp.pos.y = curpos;
417 bool doChildrenRelayout = false;
418 // fix non-packing coordinate (and, maybe, non-packing dimension)
419 // fix x coord
420 final switch (clp.aligning) {
421 case FuiLayoutProps.Align.Start: clp.pos.x = bpadLeft; break;
422 case FuiLayoutProps.Align.End: clp.pos.x = maxW-bpadRight-clp.size.w; break;
423 case FuiLayoutProps.Align.Center: clp.pos.x = (maxW-clp.size.w)/2; break;
424 case FuiLayoutProps.Align.Stretch:
425 int nd = min(max(0, maxW-(bpadLeft+bpadRight), clp.minSize.w), (clp.maxSize.w > 0 ? clp.maxSize.w : int.max));
426 if (nd != clp.size.w) {
427 // size changed, relayout children
428 doChildrenRelayout = true;
429 clp.size.w = nd;
431 clp.pos.x = bpadLeft;
432 break;
434 // fix flexbox size
435 if (clp.flex > 0) {
436 int toadd = cast(int)(left*cast(float)clp.flex/flt);
437 if (toadd > 0) {
438 // size changed, relayout children
439 doChildrenRelayout = true;
440 clp.size.h += toadd;
441 // compensate (crudely) rounding errors
442 if (toadd > 1 && clp.size.h <= maxH && maxH-(curpos+clp.size.h) < 0) clp.size.h -= 1;
445 // advance packing coordinate
446 curpos += clp.size.h+(clp.ignoreSpacing ? bspc : 0);
447 // relayout children if dimensions was changed
448 if (doChildrenRelayout) layit(clp);
450 // that's all for vertical boxes
454 // main code
455 if (mroot is null) return;
457 bool[FuiLayoutProps.Orientation.max+1] seenGroup = false;
459 // reset flags, check if we have any groups
460 forEachItem(mroot, (FuiLayoutProps it) {
461 it.layoutingStarted();
462 it.resetLayouterFlags();
463 it.pos = it.pos.init;
464 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) if (it.groupNext[gidx] !is null) seenGroup[gidx] = true;
465 if (!it.visible) { it.size = it.size.init; return; }
466 it.size = it.size.init;
469 if (seenGroup[0] || seenGroup[1]) {
470 // fix groups
471 forEachItem(mroot, (FuiLayoutProps it) {
472 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) {
473 if (it.groupNext[gidx] is null || it.groupHead[gidx] !is null) continue;
474 // this item is group member, but has no head set, so this is new head: fix the whole list
475 for (FuiLayoutProps gm = it; gm !is null; gm = gm.groupNext[gidx]) gm.groupHead[gidx] = it;
480 // do top-level packing
481 for (;;) {
482 layit(mroot);
483 bool doFix = false;
485 //FIXME: mark changed items and process only those
486 void fixGroups (FuiLayoutProps it, int grp) nothrow @nogc {
487 int dim = 0;
488 // calcluate maximal dimension
489 for (FuiLayoutProps clp = it; clp !is null; clp = clp.groupNext[grp]) {
490 if (!clp.visible) continue;
491 dim = max(dim, clp.size[grp]);
493 // fix dimensions
494 for (FuiLayoutProps clp = it; clp !is null; clp = clp.groupNext[grp]) {
495 if (!clp.visible) continue;
496 auto od = clp.size[grp];
497 int nd = max(od, dim);
498 auto mx = clp.maxSize[grp];
499 if (mx > 0) nd = min(nd, mx);
500 version(none) {
501 import core.stdc.stdio;
502 auto fo = fopen("zlx.log", "a");
503 //fo.fprintf("%.*s: od=%d; nd=%d\n", cast(uint)clp.classinfo.name.length, clp.classinfo.name.ptr, od, nd);
504 fo.fprintf("gidx=%d; dim=%d; w=%d; h=%d\n", grp, dim, clp.size[0], clp.size[1]);
505 fo.fclose();
507 if (od != nd) {
508 doFix = true;
509 clp.size[grp] = nd;
514 if (seenGroup[0] || seenGroup[1]) {
515 forEachItem(mroot, (FuiLayoutProps it) {
516 foreach (int gidx; 0..FuiLayoutProps.Orientation.max+1) {
517 if (it.groupHead[gidx] is it) fixGroups(it, gidx);
520 if (!doFix) break; // nothing to do
521 } else {
522 // no groups -> nothing to do
523 break;
527 // signal completions
528 forEachItem(mroot, (FuiLayoutProps it) { it.layoutingComplete(); });
532 debug(flexlayout_dump) void dumpLayout() (FuiLayoutProps mroot, const(char)[] foname=null) {
533 import core.stdc.stdio : stderr, fopen, fclose, fprintf;
534 import std.internal.cstring;
536 auto fo = (foname.length ? stderr : fopen(foname.tempCString, "w"));
537 if (fo is null) return;
538 scope(exit) if (foname.length) fclose(fo);
540 void ind (int indent) { foreach (immutable _; 0..indent) fo.fprintf(" "); }
542 void dumpItem() (FuiLayoutProps lp, int indent) {
543 if (lp is null || !lp.visible) return;
544 ind(indent);
545 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);
546 for (lp = lp.firstChild; lp !is null; lp = lp.nextSibling) {
547 if (!lp.visible) continue;
548 dumpItem(lp, indent+2);
552 dumpItem(mroot, 0);