DEADSOFTWARE

FlexUI: "padding" property for boxes, so i don't have to insert dummy spans everywhere
[d2df-sdl.git] / src / flexui / fui_flexlay.pas
1 (* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 *)
17 {$INCLUDE ../shared/a_modes.inc}
18 unit fui_flexlay;
19 (*
20 control default size will be increased by margins
21 negative margins are ignored
22 ControlT:
23 procedure layPrepare (); // called before registering control in layouter
24 function getDefSize (): TLaySize; // default size; <0: use max size
25 function getMargins (): TLayMargins;
26 function getPadding (): TLaySize; // children padding (each non-first child will get this on left/top)
27 function getMaxSize (): TLaySize; // max size; <0: set to some huge value
28 function getFlex (): Integer; // <=0: not flexible
29 function isHorizBox (): Boolean; // horizontal layout for children?
30 function canWrap (): Boolean; // for horizontal boxes: can wrap children? for child: `false` means 'nonbreakable at *next* ctl'
31 function noPad (): Boolean; // ignore padding in box direction for this control
32 function isLineStart (): Boolean; // `true` if this ctl should start a new line; ignored for vertical boxes
33 function getAlign (): Integer; // aligning in non-main direction: <0: left/up; 0: center; >0: right/down
34 function getExpand (): Boolean; // expanding in non-main direction: `true` will ignore align and eat all available space
35 procedure setActualSizePos (constref apos: TLayPos; constref asize: TLaySize);
36 function getHGroup (): AnsiString; // empty: not grouped
37 function getVGroup (): AnsiString; // empty: not grouped
38 function nextSibling (): ControlT;
39 function firstChild (): ControlT;
40 *)
42 interface
44 uses
45 fui_common;
48 // ////////////////////////////////////////////////////////////////////////// //
49 type
50 generic TFlexLayouterBase<ControlT> = class
51 public
52 type CtlT = ControlT;
54 private
55 type LayControlIdx = Integer;
57 private
58 // flags
59 const
60 FlagHorizBox = LongWord(1) shl 0; // horizontal layout for children
61 FlagLineStart = LongWord(1) shl 1;
62 FlagLineCanWrap = LongWord(1) shl 2;
63 FlagNoPad = LongWord(1) shl 3;
64 // internal
65 FlagLineDidWrap = LongWord(1) shl 8; // will be set when line was wrapped
66 FlagInGroup = LongWord(1) shl 9; // set if this control is a member of any group
67 FlagExpand = LongWord(1) shl 10;
68 FlagLineFirst = LongWord(1) shl 11;
70 private
71 type
72 PLayControl = ^TLayControl;
73 TLayControl = record
74 public
75 myidx: LayControlIdx;
76 tempFlex: Integer;
77 flags: LongWord; // see above
78 aligndir: Integer;
79 startsize: TLaySize; // current
80 desiredsize: TLaySize;
81 maxsize: TLaySize;
82 margins: TLayMargins; // can never be negative
83 padding: TLaySize;
84 desiredpos: TLayPos;
85 ctl: ControlT;
86 parent: LayControlIdx; // = -1;
87 firstChild: LayControlIdx; // = -1;
88 nextSibling: LayControlIdx; // = -1;
90 private
91 function getDidWrap (): Boolean; inline;
92 procedure setDidWrap (v: Boolean); inline;
94 public
95 procedure initialize (); inline;
97 function horizBox (): Boolean; inline;
98 function lineStart (): Boolean; inline;
99 function canWrap (): Boolean; inline;
100 function inGroup (): Boolean; inline;
101 function firstInLine (): Boolean; inline;
102 function noPad (): Boolean; inline;
104 function getExpand (): Boolean; inline;
105 procedure setExpand (v: Boolean); inline;
107 function alignLeft (): Boolean; inline;
108 function alignTop (): Boolean; inline;
109 function alignRight (): Boolean; inline;
110 function alignBottom (): Boolean; inline;
111 function alignCenter (): Boolean; inline;
113 public
114 property didWrap: Boolean read getDidWrap write setDidWrap;
115 property expand: Boolean read getExpand write setExpand;
116 end;
118 PLayGroup = ^TLayGroup;
119 TLayGroup = record
120 name: AnsiString;
121 ctls: array of LayControlIdx;
122 end;
124 TLayCtlArray = array of TLayControl;
125 TLayGrpArray = array of TLayGroup;
127 private
128 ctlist: TLayCtlArray;
129 groups: array[0..1] of TLayGrpArray; // horiz, vert
131 firstTime: Boolean;
132 groupElementChanged: Boolean;
133 wrappingChanged: Boolean;
135 private
136 procedure fixFlags (cidx: LayControlIdx);
137 procedure doChildren (parent: LayControlIdx; child: ControlT);
138 procedure appendToGroup (const gname: AnsiString;cidx: LayControlIdx;gidx: Integer);
139 procedure setupGroups ();
141 // this also sets `tempFlex`
142 procedure calcMaxSizeInternal (cidx: LayControlIdx);
144 procedure fixLine (me: PLayControl; i0, i1: LayControlIdx; ypad: Integer; var cury: Integer; var spaceLeft: Single);
145 // do box layouting; call `layBox()` recursively if necessary
146 procedure layBox (boxidx: LayControlIdx);
148 procedure firstPass ();
149 procedure secondPass ();
150 procedure thirdPass ();
151 procedure fourthPass ();
153 procedure dumpList (cidx: LayControlIdx; indent: Integer);
155 public
156 type
157 TChildrenEnumerator = record
158 private
159 ctls: TLayCtlArray;
160 cur: Integer;
161 first: Boolean;
162 public
163 constructor Create (constref actls: TLayCtlArray; acur: Integer);
164 function moveNext (): Boolean; inline;
165 function getCurrent (): PLayControl; inline;
166 function getEnumerator (): TChildrenEnumerator; inline;
167 property current: PLayControl read getCurrent;
168 end;
170 public
171 constructor Create ();
172 destructor Destroy (); override;
174 // clear build lists
175 procedure clear ();
177 // build control and group lists
178 procedure setup (root: ControlT);
180 function forChildren (cidx: LayControlIdx): TChildrenEnumerator; inline;
182 procedure layout ();
184 procedure dumpFlat ();
185 procedure dump ();
186 end;
189 implementation
191 uses
192 utils;
195 // ////////////////////////////////////////////////////////////////////////// //
196 procedure TFlexLayouterBase.TLayControl.initialize (); inline;
197 begin
198 FillChar(self, 0, sizeof(self));
199 parent := -1;
200 firstChild := -1;
201 nextSibling := -1;
202 end;
204 function TFlexLayouterBase.TLayControl.horizBox (): Boolean; inline; begin result := ((flags and FlagHorizBox) <> 0); end;
205 function TFlexLayouterBase.TLayControl.lineStart (): Boolean; inline; begin result := ((flags and FlagLineStart) <> 0); end;
206 function TFlexLayouterBase.TLayControl.canWrap (): Boolean; inline; begin result := ((flags and FlagLineCanWrap) <> 0); end;
207 function TFlexLayouterBase.TLayControl.inGroup (): Boolean; inline; begin result := ((flags and FlagInGroup) <> 0); end;
208 function TFlexLayouterBase.TLayControl.firstInLine (): Boolean; inline; begin result := ((flags and FlagLineFirst) <> 0); end;
209 function TFlexLayouterBase.TLayControl.noPad (): Boolean; inline; begin result := ((flags and FlagNoPad) <> 0); end;
211 function TFlexLayouterBase.TLayControl.getDidWrap (): Boolean; inline; begin result := ((flags and FlagLineDidWrap) <> 0); end;
212 procedure TFlexLayouterBase.TLayControl.setDidWrap (v: Boolean); inline; begin if (v) then flags := flags or FlagLineDidWrap else flags := flags and (not FlagLineDidWrap); end;
214 function TFlexLayouterBase.TLayControl.getExpand (): Boolean; inline; begin result := ((flags and FlagExpand) <> 0); end;
215 procedure TFlexLayouterBase.TLayControl.setExpand (v: Boolean); inline; begin if (v) then flags := flags or FlagExpand else flags := flags and (not FlagExpand); end;
217 function TFlexLayouterBase.TLayControl.alignLeft (): Boolean; inline; begin result := (aligndir < 0); end;
218 function TFlexLayouterBase.TLayControl.alignTop (): Boolean; inline; begin result := (aligndir < 0); end;
219 function TFlexLayouterBase.TLayControl.alignRight (): Boolean; inline; begin result := (aligndir > 0); end;
220 function TFlexLayouterBase.TLayControl.alignBottom (): Boolean; inline; begin result := (aligndir > 0); end;
221 function TFlexLayouterBase.TLayControl.alignCenter (): Boolean; inline; begin result := (aligndir = 0); end;
224 // ////////////////////////////////////////////////////////////////////////// //
225 constructor TFlexLayouterBase.TChildrenEnumerator.Create (constref actls: TLayCtlArray; acur: Integer);
226 begin
227 ctls := actls;
228 cur := acur;
229 first := true;
230 end;
232 function TFlexLayouterBase.TChildrenEnumerator.moveNext (): Boolean; inline;
233 begin
234 if first then
235 begin
236 if (cur >= 0) and (cur < Length(ctls)) then cur := ctls[cur].firstChild else cur := -1;
237 first := false;
238 end
239 else
240 begin
241 cur := ctls[cur].nextSibling;
242 end;
243 result := (cur >= 0);
244 end;
246 function TFlexLayouterBase.TChildrenEnumerator.getCurrent (): PLayControl; inline;
247 begin
248 result := @ctls[cur];
249 end;
251 function TFlexLayouterBase.TChildrenEnumerator.getEnumerator (): TChildrenEnumerator; inline;
252 begin
253 result := self;
254 end;
257 // ////////////////////////////////////////////////////////////////////////// //
258 constructor TFlexLayouterBase.Create ();
259 begin
260 ctlist := nil;
261 groups[0] := nil;
262 groups[1] := nil;
264 firstTime := false;
265 groupElementChanged := false;
266 wrappingChanged := false;
267 end;
270 destructor TFlexLayouterBase.Destroy ();
271 begin
272 clear();
273 inherited;
274 end;
277 function TFlexLayouterBase.forChildren (cidx: LayControlIdx): TChildrenEnumerator; inline;
278 begin
279 result := TChildrenEnumerator.Create(ctlist, cidx);
280 end;
283 procedure TFlexLayouterBase.clear ();
284 begin
285 ctlist := nil;
286 groups[0] := nil;
287 groups[1] := nil;
288 end;
291 procedure TFlexLayouterBase.fixFlags (cidx: LayControlIdx);
292 var
293 lc: PLayControl;
294 begin
295 assert((cidx >= 0) and (cidx < Length(ctlist)));
296 lc := @ctlist[cidx];
297 //lc.flags := 0;
298 if (lc.ctl.isHorizBox) then lc.flags := lc.flags or FlagHorizBox;
299 if (lc.ctl.isLineStart) then lc.flags := lc.flags or FlagLineStart;
300 if (lc.ctl.canWrap) then lc.flags := lc.flags or FlagLineCanWrap;
301 if (lc.ctl.getExpand) then lc.flags := lc.flags or FlagExpand;
302 if (lc.ctl.noPad) then lc.flags := lc.flags or FlagNoPad;
303 lc.aligndir := lc.ctl.getAlign;
304 end;
307 procedure TFlexLayouterBase.doChildren (parent: LayControlIdx; child: ControlT);
308 var
309 cidx: LayControlIdx = -1;
310 lc: PLayControl;
311 begin
312 assert((parent >= 0) and (parent < Length(ctlist)));
313 assert(ctlist[parent].firstChild = -1);
314 while (child <> nil) do
315 begin
316 child.layPrepare;
317 SetLength(ctlist, Length(ctlist)+1);
318 lc := @ctlist[High(ctlist)];
319 lc.initialize();
320 if (cidx = -1) then
321 begin
322 cidx := LayControlIdx(High(ctlist));
323 ctlist[parent].firstChild := cidx;
324 // first child is always linestart
325 lc.flags := lc.flags or FlagLineStart or FlagLineFirst;
326 end
327 else
328 begin
329 ctlist[cidx].nextSibling := LayControlIdx(High(ctlist));
330 cidx := LayControlIdx(High(ctlist));
331 end;
332 lc.myidx := cidx;
333 lc.ctl := child;
334 lc.parent := parent;
335 fixFlags(cidx);
336 doChildren(cidx, child.firstChild);
337 child := child.nextSibling;
338 end;
339 end;
342 procedure TFlexLayouterBase.appendToGroup (const gname: AnsiString; cidx: LayControlIdx; gidx: Integer);
343 var
344 f: Integer;
345 begin
346 if (Length(gname) = 0) then exit;
347 assert((cidx >= 0) and (cidx < Length(ctlist)));
348 assert((gidx = 0) or (gidx = 1));
349 ctlist[cidx].flags := ctlist[cidx].flags or FlagInGroup;
350 for f := 0 to High(groups[gidx]) do
351 begin
352 if (groups[gidx][f].name = gname) then
353 begin
354 SetLength(groups[gidx][f].ctls, Length(groups[gidx][f].ctls)+1);
355 groups[gidx][f].ctls[High(groups[gidx][f].ctls)] := cidx;
356 exit;
357 end;
358 end;
359 // new group
360 f := Length(groups[gidx]);
361 SetLength(groups[gidx], f+1);
362 groups[gidx][f].name := gname;
363 SetLength(groups[gidx][f].ctls, Length(groups[gidx][f].ctls)+1);
364 groups[gidx][f].ctls[High(groups[gidx][f].ctls)] := cidx;
365 end;
368 procedure TFlexLayouterBase.setupGroups ();
369 var
370 idx: Integer;
371 lc: PLayControl;
372 begin
373 for idx := 0 to High(ctlist) do
374 begin
375 lc := @ctlist[idx];
376 appendToGroup(lc.ctl.getHGroup, LayControlIdx(idx), 0);
377 appendToGroup(lc.ctl.getVGroup, LayControlIdx(idx), 1);
378 end;
379 end;
382 // build control and group lists
383 procedure TFlexLayouterBase.setup (root: ControlT);
384 begin
385 clear();
386 if (root = nil) then exit;
387 root.layPrepare;
388 try
389 SetLength(ctlist, 1);
390 ctlist[0].initialize();
391 ctlist[0].myidx := 0;
392 ctlist[0].ctl := root;
393 fixFlags(0);
394 ctlist[0].flags := ctlist[0].flags or FlagLineStart or FlagLineFirst;
395 doChildren(0, root.firstChild);
396 setupGroups();
397 except
398 clear();
399 raise;
400 end;
401 end;
404 // this also sets `tempFlex`
405 procedure TFlexLayouterBase.calcMaxSizeInternal (cidx: LayControlIdx);
406 var
407 lc, c: PLayControl;
408 msz: TLaySize;
409 negw, negh: Boolean;
410 zerow: Boolean;
411 curwdt, curhgt, totalhgt: Integer;
412 doWrap: Boolean;
413 xpad, ypad: Integer;
414 realpad: Integer;
415 dopad: Boolean = false;
416 begin
417 if (cidx < 0) or (cidx >= Length(ctlist)) then exit;
419 lc := @ctlist[cidx];
420 msz := lc.ctl.getMaxSize;
421 negw := (lc.startsize.w < 0);
422 negh := (lc.startsize.h < 0);
423 zerow := (lc.startsize.w = 0);
425 lc.tempFlex := lc.ctl.getFlex;
427 for c in forChildren(cidx) do calcMaxSizeInternal(c.myidx);
429 if (lc.horizBox) then
430 begin
431 // horizontal boxes
432 if (negw) then lc.tempFlex := 0; // size is negative: don't expand
433 curwdt := lc.margins.horiz;
434 curhgt := 0;
435 totalhgt := lc.margins.vert;
436 xpad := nmax(0, lc.padding.w);
437 ypad := 0;
438 for c in forChildren(cidx) do
439 begin
440 if (dopad) then realpad := xpad else realpad := 0;
441 // new line?
442 doWrap := (not c.firstInLine) and (c.lineStart);
443 // need to wrap?
444 if (not doWrap) and (not zerow) and (not negw) and (lc.canWrap) and (c.canWrap) and (msz.w > 0) and (curwdt+c.startsize.w+realpad > lc.startsize.w) then doWrap := true;
445 if (doWrap) then
446 begin
447 totalhgt += curhgt;
448 if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
449 curwdt := 0;
450 curhgt := 0;
451 ypad := nmax(0, lc.padding.h);
452 realpad := 0;
453 end;
454 curwdt += c.startsize.w+realpad;
455 if (curhgt < c.startsize.h+ypad) then curhgt := c.startsize.h+ypad;
456 dopad := (xpad > 0) and (not lc.noPad);
457 end;
458 //writeln('00: ', cidx, ': totalhgt=', totalhgt);
459 totalhgt += curhgt;
460 //writeln('01: ', cidx, ': totalhgt=', totalhgt);
461 if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
462 if (lc.startsize.h < totalhgt) then lc.startsize.h := totalhgt;
463 end
464 else
465 begin
466 // vertical boxes
467 if (negh) then lc.tempFlex := 0; // size is negative: don't expand
468 curhgt := lc.margins.vert;
469 ypad := nmax(0, lc.padding.h);
470 for c in forChildren(cidx) do
471 begin
472 if (lc.startsize.w < c.startsize.w+lc.margins.horiz) then lc.startsize.w := c.startsize.w+lc.margins.horiz;
473 curhgt += c.startsize.h;
474 if (dopad) then curhgt += ypad;
475 dopad := (not c.noPad);
476 end;
477 if (lc.startsize.h < curhgt) then lc.startsize.h := curhgt;
478 end;
479 if (lc.startsize.w < 0) then lc.startsize.w := 0;
480 if (lc.startsize.h < 0) then lc.startsize.h := 0;
482 lc.maxsize := msz;
483 if (lc.maxsize.w < lc.startsize.w) then begin if (lc.maxsize.w >= 0) then lc.maxsize.w := lc.startsize.w; end;
484 if (lc.maxsize.h < lc.startsize.h) then begin if (lc.maxsize.h >= 0) then lc.maxsize.h := lc.startsize.h; end;
486 if (msz.w < 0) then msz.w := lc.startsize.w;
487 if (msz.h < 0) then msz.h := lc.startsize.h;
488 lc.maxsize := msz;
489 end;
492 procedure TFlexLayouterBase.firstPass ();
493 var
494 f, c: Integer;
495 needRecalcMaxSize: Boolean;
496 gtype: Integer;
497 grp: PLayGroup;
498 maxsz: Integer;
499 cidx: LayControlIdx;
500 ct: PLayControl;
501 begin
502 // reset all 'laywrap' flags for controls, set initial 'startsize'
503 for f := 0 to High(ctlist) do
504 begin
505 ctlist[f].didWrap := false;
506 ctlist[f].startsize := ctlist[f].ctl.getDefSize;
507 ctlist[f].margins := ctlist[f].ctl.getMargins;
508 ctlist[f].padding := ctlist[f].ctl.getPadding;
509 end;
510 // setup sizes
511 calcMaxSizeInternal(0); // this also sets `tempFlex`
512 //writeln('=== calculated max size (0) ==='); dump();
513 // find max size for group, adjust 'startsize' controls to group max size
514 needRecalcMaxSize := false;
515 for gtype := 0 to 1 do
516 begin
517 for f := 0 to High(groups[gtype]) do
518 begin
519 grp := @groups[gtype][f];
520 maxsz := 0;
521 for c := 0 to High(grp.ctls) do
522 begin
523 cidx := grp.ctls[c];
524 ct := @ctlist[cidx];
525 if (maxsz < ct.startsize[gtype]) then maxsz := ct.startsize[gtype];
526 end;
527 for c := 0 to High(grp.ctls) do
528 begin
529 cidx := grp.ctls[c];
530 ct := @ctlist[cidx];
531 if (maxsz <> ct.startsize[gtype]) then
532 begin
533 needRecalcMaxSize := true;
534 ct.startsize[gtype] := maxsz;
535 end;
536 end;
537 end;
538 end;
539 // recalc maxsize if necessary
540 if (needRecalcMaxSize) then calcMaxSizeInternal(0);
541 // set "desired size" to "start size"
542 for f := 0 to High(ctlist) do ctlist[f].desiredsize := ctlist[f].startsize;
543 // set flags
544 firstTime := true;
545 //writeln('=== calculated max size (final) ==='); dump();
546 end;
549 procedure TFlexLayouterBase.fixLine (me: PLayControl; i0, i1: LayControlIdx; ypad: Integer; var cury: Integer; var spaceLeft: Single);
550 var
551 flexTotal: Integer = 0; // total sum of flex fields
552 flexBoxCount: Integer = 0; // number of boxes
553 curx: Integer;
554 lc: PLayControl;
555 osz: TLaySize;
556 toadd: Integer;
557 sti0: Integer;
558 lineh: Integer;
559 xpad: Integer;
560 begin
561 if (ypad < 0) then ypad := 0;
562 curx := me.margins.left;
563 sti0 := i0;
564 // calc minimal line height, count flexboxes
565 lineh := 0;
566 while (i0 <> i1) do
567 begin
568 lc := @ctlist[i0];
569 lineh := nmax(lineh, lc.startsize.h+ypad);
570 if (lc.tempFlex > 0) then begin flexTotal += lc.tempFlex; flexBoxCount += 1; end;
571 i0 := lc.nextSibling;
572 end;
573 // distribute space, expand/align
574 xpad := nmax(0, me.padding.w);
575 i0 := sti0;
576 while (i0 <> i1) do
577 begin
578 lc := @ctlist[i0];
579 osz := lc.desiredsize;
580 lc.desiredsize := lc.startsize;
581 lc.desiredpos.x := curx;
582 lc.desiredpos.y := cury;
583 curx += lc.desiredsize.w;
584 if (xpad > 0) and (not lc.noPad) then curx += xpad;
585 // fix flexbox size
586 if (lc.tempFlex > 0) and (spaceLeft > 0) then
587 begin
588 toadd := trunc(spaceLeft*lc.tempFlex/flexTotal+0.5);
589 if (toadd > 0) then
590 begin
591 // size changed
592 lc.desiredsize.w += toadd;
593 curx += toadd;
594 // compensate (crudely) rounding errors
595 if (curx > me.desiredsize.w-me.margins.horiz) then begin lc.desiredsize.w -= 1; curx -= 1; end;
596 // relayout children
597 layBox(lc.firstChild);
598 end;
599 end;
600 // expand or align
601 if (lc.expand) then lc.desiredsize.h := nmax(1, lineh) // expand
602 else if (lc.alignBottom) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) // bottom align
603 else if (lc.alignCenter) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) div 2; // center
604 if (not osz.equals(lc.desiredsize)) then
605 begin
606 if (lc.inGroup) then groupElementChanged := true;
607 // relayout children
608 layBox(lc.firstChild);
609 end;
610 i0 := lc.nextSibling;
611 end;
612 flexTotal := 0;
613 flexBoxCount := 0;
614 spaceLeft := me.desiredsize.w-me.margins.horiz;
615 cury += lineh;
616 end;
619 // do box layouting; call `layBox()` recursively if necessary
620 procedure TFlexLayouterBase.layBox (boxidx: LayControlIdx);
621 var
622 me: PLayControl;
623 flexTotal: Integer; // total sum of flex fields
624 flexBoxCount: Integer; // number of boxes
625 spaceLeft: Single;
626 cury: Integer;
627 maxwdt: Integer;
628 lineStartIdx: LayControlIdx;
629 lc: PLayControl;
630 doWrap: Boolean;
631 toadd: Integer;
632 osz: TLaySize;
633 xpad, ypad, realpad: Integer;
634 dopad: Boolean = false;
635 begin
636 if (boxidx < 0) or (boxidx >= Length(ctlist)) then exit;
637 me := @ctlist[boxidx];
639 // if we have no children, there's nothing to do
640 if (me.firstChild <> -1) then
641 begin
642 // first, layout all children
643 for lc in forChildren(boxidx) do layBox(lc.myidx);
645 // second, layout lines, distribute flex data
646 if (me.horizBox) then
647 begin
648 // horizontal boxes
649 cury := me.margins.top;
650 xpad := nmax(0, me.padding.w);
651 ypad := 0;
653 fixLine(me, -1, -1, 0, cury, spaceLeft); //HACK!
655 lineStartIdx := me.firstChild;
656 for lc in forChildren(boxidx) do
657 begin
658 if (dopad) then realpad := xpad else realpad := 0;
659 // new line?
660 doWrap := (not lc.firstInLine) and (lc.lineStart);
661 // need to wrap?
662 if (not doWrap) and (lc.canWrap) and (lc.canWrap) and (lc.desiredsize.w > 0) and (spaceLeft-realpad < lc.desiredsize.w) then doWrap := true;
663 if (doWrap) then
664 begin
665 // new line, fix this one
666 if (not lc.didWrap) then begin wrappingChanged := true; lc.didWrap := true; end;
667 fixLine(me, lineStartIdx, lc.myidx, ypad, cury, spaceLeft);
668 lineStartIdx := lc.myidx;
669 ypad := nmax(0, me.padding.h);
670 realpad := 0;
671 end
672 else
673 begin
674 if (lc.didWrap) then begin wrappingChanged := true; lc.didWrap := false; end;
675 end;
676 spaceLeft -= lc.desiredsize.w+realpad;
677 dopad := (xpad > 0) and (not lc.noPad);
678 //if (maxhgt < lc.desiredsize.h) then maxhgt := lc.desiredsize.h;
679 //if (lc.tempFlex > 0) then begin flexTotal += lc.tempFlex; flexBoxCount += 1; end;
680 end;
681 // fix last line
682 fixLine(me, lineStartIdx, -1, ypad, cury, spaceLeft);
683 end
684 else
685 begin
686 // vertical boxes
687 maxwdt := 0;
688 flexTotal := 0;
689 flexBoxCount := 0;
690 spaceLeft := me.desiredsize.h-me.margins.vert;
691 ypad := nmax(0, me.padding.h);
693 // calc flex
694 for lc in forChildren(boxidx) do
695 begin
696 spaceLeft -= lc.desiredsize.h;
697 if (maxwdt < lc.desiredsize.w) then maxwdt := lc.desiredsize.w;
698 if (lc.tempFlex > 0) then begin flexTotal += lc.tempFlex; flexBoxCount += 1; end;
699 end;
701 // distribute space
702 cury := me.margins.top;
703 //writeln('me: ', boxidx, '; margins: ', me.margins.toString);
704 for lc in forChildren(boxidx) do
705 begin
706 osz := lc.desiredsize;
707 lc.desiredsize := lc.startsize;
708 lc.desiredpos.x := me.margins.left;
709 lc.desiredpos.y := cury;
710 cury += lc.desiredsize.h;
711 if (ypad > 0) and (not lc.noPad) then cury += ypad;
712 // fix flexbox size
713 if (lc.tempFlex > 0) and (spaceLeft > 0) then
714 begin
715 toadd := trunc(spaceLeft*lc.tempFlex/flexTotal+0.5);
716 if (toadd > 0) then
717 begin
718 // size changed
719 lc.desiredsize.h += toadd;
720 cury += toadd;
721 // compensate (crudely) rounding errors
722 if (cury > me.desiredsize.h-me.margins.vert) then begin lc.desiredsize.h -= 1; cury -= 1; end;
723 end;
724 end;
725 // expand or align
726 if (lc.expand) then lc.desiredsize.w := nmax(1, me.desiredsize.w-me.margins.vert) // expand
727 else if (lc.alignRight) then lc.desiredpos.x := me.desiredsize.w-me.margins.right-lc.desiredsize.w // right align
728 else if (lc.alignCenter) then lc.desiredpos.x := (me.desiredsize.w-lc.desiredsize.w) div 2; // center
729 if (not osz.equals(lc.desiredsize)) then
730 begin
731 if (lc.inGroup) then groupElementChanged := true;
732 // relayout children
733 layBox(lc.firstChild);
734 end;
735 end;
736 end;
737 end;
739 if (me.maxsize.w >= 0) and (me.desiredsize.w > me.maxsize.w) then me.desiredsize.w := me.maxsize.w;
740 if (me.maxsize.h >= 0) and (me.desiredsize.h > me.maxsize.h) then me.desiredsize.h := me.maxsize.h;
741 end;
744 procedure TFlexLayouterBase.secondPass ();
745 begin
746 // reset flags
747 groupElementChanged := false;
748 wrappingChanged := false;
750 if (Length(ctlist) > 0) then
751 begin
752 ctlist[0].desiredpos := TLayPos.Create(0, 0);
753 layBox(0);
754 end;
756 // fix 'wrapping-changed' flag
757 if (firstTime) then begin wrappingChanged := false; firstTime := false; end;
758 end;
761 procedure TFlexLayouterBase.thirdPass ();
762 var
763 secondAgain: Boolean;
764 gtype: Integer;
765 maxsz: Integer;
766 grp: PLayGroup;
767 f, c: Integer;
768 cidx: LayControlIdx;
769 ct: PLayControl;
770 begin
771 while true do
772 begin
773 secondPass();
774 secondAgain := false;
775 if (groupElementChanged) then
776 begin
777 secondAgain := true;
778 // find max size for group, adjust 'startsize' controls to group max size
779 for gtype := 0 to 1 do
780 begin
781 for f := 0 to High(groups[gtype]) do
782 begin
783 grp := @groups[gtype][f];
784 maxsz := 0;
785 for c := 0 to High(grp.ctls) do
786 begin
787 cidx := grp.ctls[c];
788 ct := @ctlist[cidx];
789 ct.expand := false; // don't expand grouped controls anymore
790 if (maxsz < ct.startsize[gtype]) then maxsz := ct.startsize[gtype];
791 end;
792 for c := 0 to High(grp.ctls) do
793 begin
794 cidx := grp.ctls[c];
795 ct := @ctlist[cidx];
796 ct.startsize[gtype] := maxsz;
797 ct.desiredsize[gtype] := maxsz;
798 ct.tempFlex := 0; // don't change control size anymore
799 end;
800 end;
801 end;
802 end
803 else
804 begin
805 for f := 0 to High(ctlist) do
806 begin
807 ct := @ctlist[f];
808 if (ct.inGroup) then
809 begin
810 ct.expand := false; // don't expand grouped controls anymore
811 ct.tempFlex := 0; // don't change control size anymore
812 end;
813 (*
814 for c := 0 to 1 do
815 begin
816 if (ct.maxsize[c] < 0) then continue;
817 if (ct.desiredsize[c] > ct.maxsize[c]) then
818 begin
819 //writeln('ctl #', f, '; dimension #', c, ': desired=', ctlist[f].desiredsize[c], '; max=', ctlist[f].maxsize[c]);
820 ct.startsize[c] := ct.maxsize[c];
821 ct.desiredsize[c] := ct.maxsize[c];
822 ct.tempFlex := 0; // don't change control size anymore
823 secondAgain := true;
824 end;
825 end;
826 *)
827 end;
828 end;
829 if (not secondAgain) and (not wrappingChanged) then break;
830 firstTime := false;
831 end;
832 end;
835 (*
836 fourth pass:
837 set 'actualsize' and 'actualpos' to 'desiredsize' and 'desiredpos'
838 return
839 *)
840 procedure TFlexLayouterBase.fourthPass ();
841 var
842 f: Integer;
843 begin
844 for f := 0 to High(ctlist) do
845 begin
846 ctlist[f].ctl.setActualSizePos(ctlist[f].desiredpos, ctlist[f].desiredsize);
847 end;
848 end;
851 procedure TFlexLayouterBase.layout ();
852 begin
853 firstPass();
854 secondPass();
855 thirdPass();
856 fourthPass();
857 end;
860 procedure TFlexLayouterBase.dumpFlat ();
861 var
862 f: Integer;
863 lc: PLayControl;
864 ds, ms: TLaySize;
865 begin
866 for f := 0 to High(ctlist) do
867 begin
868 lc := @ctlist[f];
869 ds := lc.ctl.getDefSize;
870 ms := lc.ctl.getMaxSize;
871 writeln(lc.myidx, ': startsize:', lc.startsize.toString(), '; desiredsize=', lc.desiredsize.toString(), '; maxsize=', lc.maxsize.toString(), '; tempFlex=', lc.tempFlex, '; flags=', lc.flags,
872 '; parent=', lc.parent, '; next=', lc.nextSibling, '; child=', lc.firstChild, '; ctl.size=', ds.toString(), '; ctl.maxsize=', ms.toString());
873 end;
874 end;
877 procedure TFlexLayouterBase.dumpList (cidx: LayControlIdx; indent: Integer);
878 var
879 lc: PLayControl;
880 f: Integer;
881 begin
882 while (cidx >= 0) do
883 begin
884 lc := @ctlist[cidx];
885 for f := 0 to indent do write(' ');
886 writeln(lc.myidx, ': startsize:', lc.startsize.toString, '; desiredsize=', lc.desiredsize.toString, '; maxsize=', lc.maxsize.toString, '; tempFlex=', lc.tempFlex, '; despos=', lc.desiredpos.toString);
887 dumpList(lc.firstChild, indent+2);
888 cidx := lc.nextSibling;
889 end;
890 end;
893 procedure TFlexLayouterBase.dump ();
894 begin
895 dumpList(0, 0);
896 end;
899 // ////////////////////////////////////////////////////////////////////////// //
900 (*
901 void main () begin
902 auto win := new GuiControl();
903 (win ~= new GuiControl()).mSize := TLaySize(10, 5);
904 (win ~= new GuiControl()).mSize := TLaySize(16, 8);
906 //win.mSize := TLaySize(40, 20);
908 auto lay := TFlexLayouterBase!GuiControl();
909 lay.setup(win);
911 writeln('============================');
912 lay.dumpFlat();
914 writeln('=== initial ===');
915 lay.dump();
917 //lay.calcMaxSizeInternal(0);
918 /*
919 lay.firstPass();
920 writeln('=== after first pass ===');
921 lay.dump();
923 lay.secondPass();
924 writeln('=== after second pass ===');
925 lay.dump();
926 */
927 lay.layout();
928 writeln('=== final ===');
929 lay.dump();
930 *)
931 end.