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*/;
25 // ////////////////////////////////////////////////////////////////////////// //
27 public align(1) struct FuiPoint
{
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;");
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
; }
44 public align(1) struct FuiSize
{
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
; }
54 public align(1) struct FuiRect
{
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
); }
78 public align(1) struct FuiMargin
{
81 pure nothrow @trusted @nogc:
82 this (const(int)[] v
...) { if (v
.length
> 4) v
= v
[0..4]; ltrb
[0..v
.length
] = v
[]; }
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
{
106 /// "NPD" means "non-packing direction"
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
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
) {
151 if (it
is root
) break;
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
) {
161 if (it
is root
) break;
167 // internal housekeeping for layouter
168 FuiLayoutProps
[Orientation
.max
+1] groupHead
;
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) {
183 visitAll(it
.firstChild
);
187 if (root
is null || dg
is null) return;
189 visitAll(root
.firstChild
);
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
;
201 aroot
.nextSibling
= null;
202 scope(exit
) { aroot
.parent
= oparent
; aroot
.nextSibling
= onexts
; }
205 // layout children in this item
206 void layit() (FuiLayoutProps lp
) {
207 if (lp
is null ||
!lp
.visible
) return;
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
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
);
234 // flush current line
236 if (lastCIdx
is null) return;
237 // mark last item as line break
238 lastCIdx
.tempLineBreak
= true;
240 maxW
= max(maxW
, curWidth
);
242 maxH
+= lineH
+(lineCount ? lp
.lineSpacing
: 0);
244 curWidth
= bpadLeft
+bpadRight
;
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
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
263 // and flush line immediately
266 // flush current line
268 // and add this item to new one
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
278 // for horizontal box, logic is somewhat messy
280 if (clp
.lineBreak
) flushLine();
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
);
290 if (hbox
) flushLine(); // flush last line for horizontal box (it is safe to flush empty line)
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
);
303 int flexTotal
; // total sum of flex fields
304 int flexBoxCount
; // number of boxes
305 int curSpc
; // "current" spacing in layout calculations (for bspc)
309 // layout horizontal box; we should do this for each line separately
310 int lineStartY
= bpadTop
;
316 spaceLeft
= maxW
-(bpadLeft
+bpadRight
);
320 auto lstart
= lp
.firstChild
;
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);
329 for (auto clp
= lstart
; clp
!is null; clp
= clp
.nextSibling
) {
330 if (!clp
.visible
) continue;
331 auto dim
= clp
.size
.w
+curSpc
;
333 lineH
= max(lineH
, clp
.size
.h
);
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
354 bool doChildrenRelayout
= false;
355 // fix non-packing coordinate (and, maybe, non-packing dimension)
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;
373 //{ import iv.vfs.io; write("\x07"); }
374 int toadd
= cast(int)(left
*cast(float)clp
.flex
/flt
+0.5);
376 // size changed, relayout children
377 doChildrenRelayout
= true;
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
;
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
;
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
417 bool doChildrenRelayout
= false;
418 // fix non-packing coordinate (and, maybe, non-packing dimension)
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;
431 clp
.pos
.x
= bpadLeft
;
436 int toadd
= cast(int)(left
*cast(float)clp
.flex
/flt
);
438 // size changed, relayout children
439 doChildrenRelayout
= true;
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
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]) {
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
485 //FIXME: mark changed items and process only those
486 void fixGroups (FuiLayoutProps it
, int grp
) nothrow @nogc {
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
]);
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
);
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]);
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
522 // no groups -> nothing to do
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;
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);