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 // ////////////////////////////////////////////////////////////////////////// //
26 public align(1) struct FuiPoint
{
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;");
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
; }
43 public align(1) struct FuiSize
{
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
; }
53 public align(1) struct FuiRect
{
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
); }
77 public align(1) struct FuiMargin
{
80 pure nothrow @trusted @nogc:
81 this (const(int)[] v
...) { if (v
.length
> 4) v
= v
[0..4]; ltrb
[0..v
.length
] = v
[]; }
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
{
105 /// "NPD" means "non-packing direction"
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
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
) {
150 if (it
is root
) break;
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
) {
160 if (it
is root
) break;
166 // internal housekeeping for layouter
167 FuiLayoutProps
[Orientation
.max
+1] groupHead
;
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) {
182 visitAll(it
.firstChild
);
186 if (root
is null || dg
is null) return;
188 visitAll(root
.firstChild
);
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
;
200 aroot
.nextSibling
= null;
201 scope(exit
) { aroot
.parent
= oparent
; aroot
.nextSibling
= onexts
; }
204 // layout children in this item
205 void layit() (FuiLayoutProps lp
) {
206 if (lp
is null ||
!lp
.visible
) return;
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
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
);
233 // flush current line
235 if (lastCIdx
is null) return;
236 // mark last item as line break
237 lastCIdx
.tempLineBreak
= true;
239 maxW
= max(maxW
, curWidth
);
241 maxH
+= lineH
+(lineCount ? lp
.lineSpacing
: 0);
243 curWidth
= bpadLeft
+bpadRight
;
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
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
262 // and flush line immediately
265 // flush current line
267 // and add this item to new one
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
277 // for horizontal box, logic is somewhat messy
279 if (clp
.lineBreak
) flushLine();
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
);
289 if (hbox
) flushLine(); // flush last line for horizontal box (it is safe to flush empty line)
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
);
302 int flexTotal
; // total sum of flex fields
303 int flexBoxCount
; // number of boxes
304 int curSpc
; // "current" spacing in layout calculations (for bspc)
308 // layout horizontal box; we should do this for each line separately
309 int lineStartY
= bpadTop
;
315 spaceLeft
= maxW
-(bpadLeft
+bpadRight
);
319 auto lstart
= lp
.firstChild
;
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);
328 for (auto clp
= lstart
; clp
!is null; clp
= clp
.nextSibling
) {
329 if (!clp
.visible
) continue;
330 auto dim
= clp
.size
.w
+curSpc
;
332 lineH
= max(lineH
, clp
.size
.h
);
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
353 bool doChildrenRelayout
= false;
354 // fix non-packing coordinate (and, maybe, non-packing dimension)
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;
372 //{ import iv.vfs.io; write("\x07"); }
373 int toadd
= cast(int)(left
*cast(float)clp
.flex
/flt
+0.5);
375 // size changed, relayout children
376 doChildrenRelayout
= true;
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
;
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
;
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
416 bool doChildrenRelayout
= false;
417 // fix non-packing coordinate (and, maybe, non-packing dimension)
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;
430 clp
.pos
.x
= bpadLeft
;
435 int toadd
= cast(int)(left
*cast(float)clp
.flex
/flt
);
437 // size changed, relayout children
438 doChildrenRelayout
= true;
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
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]) {
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
484 //FIXME: mark changed items and process only those
485 void fixGroups (FuiLayoutProps it
, int grp
) nothrow @nogc {
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
]);
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
);
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]);
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
521 // no groups -> nothing to do
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;
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);