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*/;
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 // ////////////////////////////////////////////////////////////////////////// //
34 public align(1) struct FuiPoint
{
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;");
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
; }
51 public align(1) struct FuiSize
{
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
; }
61 public align(1) struct FuiRect
{
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
); }
85 public align(1) struct FuiMargin
{
88 pure nothrow @trusted @nogc:
89 this (const(int)[] v
...) { if (v
.length
> 4) v
= v
[0..4]; ltrb
[0..v
.length
] = v
[]; }
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
{
113 /// "NPD" means "non-packing direction"
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
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
) {
158 if (it
is root
) break;
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
) {
168 if (it
is root
) break;
174 // internal housekeeping for layouter
175 FuiLayoutProps
[Orientation
.max
+1] groupHead
;
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) {
190 visitAll(it
.firstChild
);
194 if (root
is null || dg
is null) return;
196 visitAll(root
.firstChild
);
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
;
208 aroot
.nextSibling
= null;
209 scope(exit
) { aroot
.parent
= oparent
; aroot
.nextSibling
= onexts
; }
212 // layout children in this item
213 void layit() (FuiLayoutProps lp
) {
214 if (lp
is null ||
!lp
.visible
) return;
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
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
);
241 // flush current line
243 if (lastCIdx
is null) return;
244 // mark last item as line break
245 lastCIdx
.tempLineBreak
= true;
247 maxW
= max(maxW
, curWidth
);
249 maxH
+= lineH
+(lineCount ? lp
.lineSpacing
: 0);
251 curWidth
= bpadLeft
+bpadRight
;
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
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
270 // and flush line immediately
273 // flush current line
275 // and add this item to new one
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
285 // for horizontal box, logic is somewhat messy
287 if (clp
.lineBreak
) flushLine();
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
);
297 if (hbox
) flushLine(); // flush last line for horizontal box (it is safe to flush empty line)
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
);
310 int flexTotal
; // total sum of flex fields
311 int flexBoxCount
; // number of boxes
312 int curSpc
; // "current" spacing in layout calculations (for bspc)
316 // layout horizontal box; we should do this for each line separately
317 int lineStartY
= bpadTop
;
323 spaceLeft
= maxW
-(bpadLeft
+bpadRight
);
327 auto lstart
= lp
.firstChild
;
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);
336 for (auto clp
= lstart
; clp
!is null; clp
= clp
.nextSibling
) {
337 if (!clp
.visible
) continue;
338 auto dim
= clp
.size
.w
+curSpc
;
340 lineH
= max(lineH
, clp
.size
.h
);
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
361 bool doChildrenRelayout
= false;
362 // fix non-packing coordinate (and, maybe, non-packing dimension)
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;
380 //{ import iv.vfs.io; write("\x07"); }
381 int toadd
= cast(int)(left
*cast(float)clp
.flex
/flt
+0.5);
383 // size changed, relayout children
384 doChildrenRelayout
= true;
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
;
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
;
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
424 bool doChildrenRelayout
= false;
425 // fix non-packing coordinate (and, maybe, non-packing dimension)
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;
438 clp
.pos
.x
= bpadLeft
;
443 int toadd
= cast(int)(left
*cast(float)clp
.flex
/flt
);
445 // size changed, relayout children
446 doChildrenRelayout
= true;
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
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]) {
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
492 //FIXME: mark changed items and process only those
493 void fixGroups (FuiLayoutProps it
, int grp
) nothrow @nogc {
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
]);
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
);
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]);
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
529 // no groups -> nothing to do
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;
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);