DEADSOFTWARE

Holmes UI: lot of flexbox layouting code fixes
[d2df-sdl.git] / src / gx / gh_flexlay.pas
1 {$INCLUDE ../shared/a_modes.inc}
2 unit gh_flexlay;
4 (*
5 first pass:
6 set all 'temp-flex' flags for controls to 'flex'
7 reset all 'laywrap' flags for controls
8 build group arrays; for each group: find max size for group, adjust 'startsize' controls to group max size
9 call 'calc max size' for top-level control
10 flags set:
11 'firsttime'
13 second pass:
14 calcluate desired sizes (process flexes) using 'startsize', set 'desiredsize' and 'desiredpos'
15 if control has children, call 'second pass' recursively with this control as parent
16 flags set:
17 'group-element-changed', if any group element size was changed
18 'wrapping-changed', if not 'firsttime', and wrapping was changed (i.e. first pass will not set the flag)
20 third pass:
21 if 'group-element-changed':
22 for each group: adjust controls to max desired size (startsize), set 'temp-flex' flags to 0 for 'em, set 'second-again' flag
23 for other controls: if 'desiredsize' > 'maxsize', set 'startsize' to 'maxsize', set 'temp-flex' flag to 0, set 'second-again' flag
24 if 'second-again' or 'wrapping-changed':
25 reset 'second-again'
26 reset 'wrapping-changed'
27 reset 'firsttime'
28 goto second pass
30 fourth pass:
31 set 'actualsize' and 'actualpos' to 'desiredsize' and 'desiredpos'
32 return
34 calc max size:
35 set 'startsize' to max(size, maxsize, 0)
36 if 'size' is negative:
37 set 'temp-flex' flag to 0
38 if has children:
39 call 'calc max size' for each child
40 set 'desiredmax' to 'startsize'
41 do lines, don't distribute space (i.e. calc only wrapping),
42 for each complete line, set 'desiredmax' to max(desiredmax, linesize)
43 if 'maxsize' >= 0:
44 set 'desiredmax' to min(desiredmax, maxsize)
45 set 'startsize' to 'desiredmax'
46 return
49 wrapping lines:
50 try to stuff controls in line until line width is less or equal to maxsize
51 distribute flex for filled line
52 continue until we still has something to stuff
55 for wrapping:
56 we'll hold 'laywrap' flag for each control; it will be set if this control
57 starts a new line (even if this is the first control in line, as it is obviously
58 starts a new line)
60 on redoing second pass, if 'laywrap' flag changed, set 'wrapping-changed' flag
61 *)
64 (*
65 control default size will be increased by margins
66 negative margins are ignored
67 ControlT:
68 procedure layPrepare (); // called before registering control in layouter
69 function getDefSize (): TLaySize; // default size; <0: use max size
70 function getMargins (): TLayMargins;
71 function getMaxSize (): TLaySize; // max size; <0: set to some huge value
72 function getFlex (): Integer; // <=0: not flexible
73 function isHorizBox (): Boolean; // horizontal layout for children?
74 function canWrap (): Boolean; // for horizontal boxes: can wrap children? for child: `false` means 'nonbreakable at *next* ctl'
75 function isLineStart (): Boolean; // `true` if this ctl should start a new line; ignored for vertical boxes
76 function getAlign (): Integer; // aligning in non-main direction: <0: left/up; 0: center; >0: right/down
77 function getExpand (): Boolean; // expanding in non-main direction: `true` will ignore align and eat all available space
78 procedure setActualSizePos (constref apos: TLayPos; constref asize: TLaySize);
79 function getHGroup (): AnsiString; // empty: not grouped
80 function getVGroup (): AnsiString; // empty: not grouped
81 function nextSibling (): ControlT;
82 function firstChild (): ControlT;
83 *)
85 interface
87 uses
88 gh_ui_common;
91 // ////////////////////////////////////////////////////////////////////////// //
92 type
93 generic TFlexLayouterBase<ControlT> = class
94 public
95 type CtlT = ControlT;
97 private
98 type LayControlIdx = Integer;
100 private
101 // flags
102 const
103 FlagHorizBox = LongWord(1) shl 0; // horizontal layout for children
104 FlagLineStart = LongWord(1) shl 1;
105 FlagLineCanWrap = LongWord(1) shl 2;
106 // internal
107 FlagLineDidWrap = LongWord(1) shl 3; // will be set when line was wrapped
108 FlagInGroup = LongWord(1) shl 4; // set if this control is a member of any group
109 FlagExpand = LongWord(1) shl 5;
110 FlagLineFirst = LongWord(1) shl 6;
112 private
113 type
114 PLayControl = ^TLayControl;
115 TLayControl = record
116 public
117 myidx: LayControlIdx;
118 tempFlex: Integer;
119 flags: LongWord; // see below
120 aligndir: Integer;
121 startsize: TLaySize; // current
122 desiredsize: TLaySize;
123 maxsize: TLaySize;
124 margins: TLayMargins; // can never be negative
125 desiredpos: TLayPos;
126 ctl: ControlT;
127 parent: LayControlIdx; // = -1;
128 firstChild: LayControlIdx; // = -1;
129 nextSibling: LayControlIdx; // = -1;
131 private
132 function getDidWrap (): Boolean; inline;
133 procedure setDidWrap (v: Boolean); inline;
135 public
136 procedure initialize (); inline;
138 function horizBox (): Boolean; inline;
139 function lineStart (): Boolean; inline;
140 function canWrap (): Boolean; inline;
141 function inGroup (): Boolean; inline;
142 function expand (): Boolean; inline;
143 function firstInLine (): Boolean; inline;
145 public
146 property didWrap: Boolean read getDidWrap write setDidWrap;
147 end;
149 PLayGroup = ^TLayGroup;
150 TLayGroup = record
151 name: AnsiString;
152 ctls: array of LayControlIdx;
153 end;
155 TLayCtlArray = array of TLayControl;
156 TLayGrpArray = array of TLayGroup;
158 private
159 ctlist: TLayCtlArray;
160 groups: array[0..1] of TLayGrpArray; // horiz, vert
162 firstTime: Boolean;
163 groupElementChanged: Boolean;
164 wrappingChanged: Boolean;
166 private
167 procedure fixFlags (cidx: LayControlIdx);
168 procedure doChildren (parent: LayControlIdx; child: ControlT);
169 procedure appendToGroup (const gname: AnsiString;cidx: LayControlIdx;gidx: Integer);
170 procedure setupGroups ();
172 // this also sets `tempFlex`
173 procedure calcMaxSizeInternal (cidx: LayControlIdx);
175 procedure fixLine (me: PLayControl; i0, i1: LayControlIdx; cury: Integer; var spaceLeft: Single; var flexTotal: Integer; var flexBoxCount: Integer);
176 // do box layouting; call `layBox()` recursively if necessary
177 procedure layBox (boxidx: LayControlIdx);
179 procedure firstPass ();
180 procedure secondPass ();
181 procedure thirdPass ();
182 procedure fourthPass ();
184 procedure dumpList (cidx: LayControlIdx; indent: Integer);
186 public
187 type
188 TChildrenEnumerator = record
189 private
190 ctls: TLayCtlArray;
191 cur: Integer;
192 first: Boolean;
193 public
194 constructor Create (constref actls: TLayCtlArray; acur: Integer);
195 function moveNext (): Boolean; inline;
196 function getCurrent (): PLayControl; inline;
197 function getEnumerator (): TChildrenEnumerator; inline;
198 property current: PLayControl read getCurrent;
199 end;
201 public
202 constructor Create ();
203 destructor Destroy (); override;
205 // clear build lists
206 procedure clear ();
208 // build control and group lists
209 procedure setup (root: ControlT);
211 function forChildren (cidx: LayControlIdx): TChildrenEnumerator; inline;
213 procedure layout ();
215 procedure dumpFlat ();
216 procedure dump ();
217 end;
220 implementation
222 uses
223 utils;
226 // ////////////////////////////////////////////////////////////////////////// //
227 procedure TFlexLayouterBase.TLayControl.initialize (); inline;
228 begin
229 FillChar(self, 0, sizeof(self));
230 parent := -1;
231 firstChild := -1;
232 nextSibling := -1;
233 end;
235 function TFlexLayouterBase.TLayControl.horizBox (): Boolean; inline; begin result := ((flags and FlagHorizBox) <> 0); end;
236 function TFlexLayouterBase.TLayControl.lineStart (): Boolean; inline; begin result := ((flags and FlagLineStart) <> 0); end;
237 function TFlexLayouterBase.TLayControl.canWrap (): Boolean; inline; begin result := ((flags and FlagLineCanWrap) <> 0); end;
238 function TFlexLayouterBase.TLayControl.inGroup (): Boolean; inline; begin result := ((flags and FlagInGroup) <> 0); end;
239 function TFlexLayouterBase.TLayControl.expand (): Boolean; inline; begin result := ((flags and FlagExpand) <> 0); end;
240 function TFlexLayouterBase.TLayControl.firstInLine (): Boolean; inline; begin result := ((flags and FlagLineFirst) <> 0); end;
242 function TFlexLayouterBase.TLayControl.getDidWrap (): Boolean; inline; begin result := ((flags and FlagLineDidWrap) <> 0); end;
243 procedure TFlexLayouterBase.TLayControl.setDidWrap (v: Boolean); inline; begin if (v) then flags := flags or FlagLineDidWrap else flags := flags and (not FlagLineDidWrap); end;
246 // ////////////////////////////////////////////////////////////////////////// //
247 constructor TFlexLayouterBase.TChildrenEnumerator.Create (constref actls: TLayCtlArray; acur: Integer);
248 begin
249 ctls := actls;
250 cur := acur;
251 first := true;
252 end;
254 function TFlexLayouterBase.TChildrenEnumerator.moveNext (): Boolean; inline;
255 begin
256 if first then
257 begin
258 if (cur >= 0) and (cur < Length(ctls)) then cur := ctls[cur].firstChild else cur := -1;
259 first := false;
260 end
261 else
262 begin
263 cur := ctls[cur].nextSibling;
264 end;
265 result := (cur >= 0);
266 end;
268 function TFlexLayouterBase.TChildrenEnumerator.getCurrent (): PLayControl; inline;
269 begin
270 result := @ctls[cur];
271 end;
273 function TFlexLayouterBase.TChildrenEnumerator.getEnumerator (): TChildrenEnumerator; inline;
274 begin
275 result := self;
276 end;
279 // ////////////////////////////////////////////////////////////////////////// //
280 constructor TFlexLayouterBase.Create ();
281 begin
282 ctlist := nil;
283 groups[0] := nil;
284 groups[1] := nil;
286 firstTime := false;
287 groupElementChanged := false;
288 wrappingChanged := false;
289 end;
292 destructor TFlexLayouterBase.Destroy ();
293 begin
294 clear();
295 inherited;
296 end;
299 function TFlexLayouterBase.forChildren (cidx: LayControlIdx): TChildrenEnumerator; inline;
300 begin
301 result := TChildrenEnumerator.Create(ctlist, cidx);
302 end;
305 procedure TFlexLayouterBase.clear ();
306 begin
307 ctlist := nil;
308 groups[0] := nil;
309 groups[1] := nil;
310 end;
313 procedure TFlexLayouterBase.fixFlags (cidx: LayControlIdx);
314 var
315 lc: PLayControl;
316 begin
317 assert((cidx >= 0) and (cidx < Length(ctlist)));
318 lc := @ctlist[cidx];
319 //lc.flags := 0;
320 if (lc.ctl.isHorizBox) then lc.flags := lc.flags or FlagHorizBox;
321 if (lc.ctl.isLineStart) then lc.flags := lc.flags or FlagLineStart;
322 if (lc.ctl.canWrap) then lc.flags := lc.flags or FlagLineCanWrap;
323 if (lc.ctl.getExpand) then lc.flags := lc.flags or FlagExpand;
324 lc.aligndir := lc.ctl.getAlign;
325 end;
328 procedure TFlexLayouterBase.doChildren (parent: LayControlIdx; child: ControlT);
329 var
330 cidx: LayControlIdx = -1;
331 lc: PLayControl;
332 begin
333 assert((parent >= 0) and (parent < Length(ctlist)));
334 assert(ctlist[parent].firstChild = -1);
335 while (child <> nil) do
336 begin
337 child.layPrepare;
338 SetLength(ctlist, Length(ctlist)+1);
339 lc := @ctlist[High(ctlist)];
340 lc.initialize();
341 if (cidx = -1) then
342 begin
343 cidx := LayControlIdx(High(ctlist));
344 ctlist[parent].firstChild := cidx;
345 // first child is always linestart
346 lc.flags := lc.flags or FlagLineStart or FlagLineFirst;
347 end
348 else
349 begin
350 ctlist[cidx].nextSibling := LayControlIdx(High(ctlist));
351 cidx := LayControlIdx(High(ctlist));
352 end;
353 lc.myidx := cidx;
354 lc.ctl := child;
355 lc.parent := parent;
356 fixFlags(cidx);
357 doChildren(cidx, child.firstChild);
358 child := child.nextSibling;
359 end;
360 end;
363 procedure TFlexLayouterBase.appendToGroup (const gname: AnsiString; cidx: LayControlIdx; gidx: Integer);
364 var
365 f: Integer;
366 begin
367 if (Length(gname) = 0) then exit;
368 assert((cidx >= 0) and (cidx < Length(ctlist)));
369 assert((gidx = 0) or (gidx = 1));
370 ctlist[cidx].flags := ctlist[cidx].flags or FlagInGroup;
371 for f := 0 to High(groups[gidx]) do
372 begin
373 if (groups[gidx][f].name = gname) then
374 begin
375 SetLength(groups[gidx][f].ctls, Length(groups[gidx][f].ctls)+1);
376 groups[gidx][f].ctls[High(groups[gidx][f].ctls)] := cidx;
377 exit;
378 end;
379 end;
380 // new group
381 f := Length(groups[gidx]);
382 SetLength(groups[gidx], f+1);
383 groups[gidx][f].name := gname;
384 SetLength(groups[gidx][f].ctls, Length(groups[gidx][f].ctls)+1);
385 groups[gidx][f].ctls[High(groups[gidx][f].ctls)] := cidx;
386 end;
389 procedure TFlexLayouterBase.setupGroups ();
390 var
391 idx: Integer;
392 lc: PLayControl;
393 begin
394 for idx := 0 to High(ctlist) do
395 begin
396 lc := @ctlist[idx];
397 appendToGroup(lc.ctl.getHGroup, LayControlIdx(idx), 0);
398 appendToGroup(lc.ctl.getVGroup, LayControlIdx(idx), 1);
399 end;
400 end;
403 // build control and group lists
404 procedure TFlexLayouterBase.setup (root: ControlT);
405 begin
406 clear();
407 if (root = nil) then exit;
408 root.layPrepare;
409 try
410 SetLength(ctlist, 1);
411 ctlist[0].initialize();
412 ctlist[0].myidx := 0;
413 ctlist[0].ctl := root;
414 fixFlags(0);
415 ctlist[0].flags := ctlist[0].flags or FlagLineStart or FlagLineFirst;
416 doChildren(0, root.firstChild);
417 setupGroups();
418 except
419 clear();
420 raise;
421 end;
422 end;
425 // this also sets `tempFlex`
426 procedure TFlexLayouterBase.calcMaxSizeInternal (cidx: LayControlIdx);
427 var
428 lc, c: PLayControl;
429 msz: TLaySize;
430 negw, negh: Boolean;
431 curwdt, curhgt, totalhgt: Integer;
432 doWrap: Boolean;
433 begin
434 if (cidx < 0) or (cidx >= Length(ctlist)) then exit;
436 lc := @ctlist[cidx];
437 msz := lc.ctl.getMaxSize;
438 negw := (lc.startsize.w <= 0);
439 negh := (lc.startsize.h <= 0);
441 lc.tempFlex := lc.ctl.getFlex;
443 for c in forChildren(cidx) do calcMaxSizeInternal(c.myidx);
445 if (lc.horizBox) then
446 begin
447 // horizontal boxes
448 if (negw) then lc.tempFlex := 0; // size is negative: don't expand
449 curwdt := lc.margins.horiz;
450 curhgt := lc.margins.vert;
451 totalhgt := 0;
452 for c in forChildren(cidx) do
453 begin
454 // new line?
455 doWrap := (not c.firstInLine) and (c.lineStart);
456 // need to wrap?
457 if (not doWrap) and (lc.canWrap) and (c.canWrap) and (msz.w > 0) and (curwdt+c.startsize.w > lc.startsize.w) then doWrap := true;
458 if (doWrap) then
459 begin
460 totalhgt += curhgt;
461 if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
462 curwdt := 0;
463 curhgt := 0;
464 end;
465 curwdt += c.startsize.w;
466 if (curhgt < c.startsize.h) then curhgt := c.startsize.h;
467 end;
468 totalhgt += curhgt;
469 if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
470 if (lc.startsize.h < totalhgt) then lc.startsize.h := totalhgt;
471 end
472 else
473 begin
474 // vertical boxes
475 if (negh) then lc.tempFlex := 0; // size is negative: don't expand
476 curhgt := lc.margins.vert;
477 for c in forChildren(cidx) do
478 begin
479 if (lc.startsize.w < c.startsize.w+lc.margins.horiz) then lc.startsize.w := c.startsize.w+lc.margins.horiz;
480 curhgt += c.startsize.h;
481 end;
482 if (lc.startsize.h < curhgt) then lc.startsize.h := curhgt;
483 end;
484 if (lc.startsize.w < 0) then lc.startsize.w := 0;
485 if (lc.startsize.h < 0) then lc.startsize.h := 0;
486 lc.maxsize := msz;
487 if (lc.maxsize.w < lc.startsize.w) then lc.maxsize.w := lc.startsize.w;
488 if (lc.maxsize.h < lc.startsize.h) then lc.maxsize.h := lc.startsize.h;
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 mr: TLayMargins;
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 mr := ctlist[f].ctl.getMargins;
508 ctlist[f].margins := mr;
509 ctlist[f].startsize.w += mr.horiz;
510 ctlist[f].startsize.h += mr.vert;
511 end;
512 // setup sizes
513 calcMaxSizeInternal(0); // this also sets `tempFlex`
514 // find max size for group, adjust 'startsize' controls to group max size
515 needRecalcMaxSize := false;
516 for gtype := 0 to 1 do
517 begin
518 for f := 0 to High(groups[gtype]) do
519 begin
520 grp := @groups[gtype][f];
521 maxsz := 0;
522 for c := 0 to High(grp.ctls) do
523 begin
524 cidx := grp.ctls[c];
525 if (maxsz < ctlist[cidx].startsize[gtype]) then maxsz := ctlist[cidx].startsize[gtype];
526 end;
527 for c := 0 to High(grp.ctls) do
528 begin
529 cidx := grp.ctls[c];
530 if (maxsz <> ctlist[cidx].startsize[gtype]) then
531 begin
532 needRecalcMaxSize := true;
533 ctlist[cidx].startsize[gtype] := maxsz;
534 end;
535 end;
536 end;
537 end;
538 // recalc maxsize if necessary
539 if (needRecalcMaxSize) then calcMaxSizeInternal(0);
540 // set "desired size" to "start size"
541 for f := 0 to High(ctlist) do ctlist[f].desiredsize := ctlist[f].startsize;
542 // set flags
543 firstTime := true;
544 //writeln('=== calculated max size ===');
545 //dump();
546 end;
549 procedure TFlexLayouterBase.fixLine (me: PLayControl; i0, i1: LayControlIdx; cury: Integer; var spaceLeft: Single; var flexTotal: Integer; var flexBoxCount: Integer);
550 var
551 curx: Integer;
552 lc: PLayControl;
553 osz: TLaySize;
554 toadd: Integer;
555 sti0: Integer;
556 lineh: Integer;
557 begin
558 curx := me.margins.left;
559 sti0 := i0;
560 // calc minimal line height
561 lineh := 0;
562 while (i0 <> i1) do
563 begin
564 lc := @ctlist[i0];
565 lineh := nmax(lineh, lc.startsize.h);
566 i0 := lc.nextSibling;
567 end;
568 // distribute space, expand/align
569 i0 := sti0;
570 while (i0 <> i1) do
571 begin
572 lc := @ctlist[i0];
573 osz := lc.desiredsize;
574 lc.desiredsize := lc.startsize;
575 lc.desiredpos.x := curx;
576 lc.desiredpos.y := cury;
577 curx += lc.desiredsize.w;
578 // fix flexbox size
579 if (lc.tempFlex > 0) and (spaceLeft > 0) then
580 begin
581 toadd := trunc(spaceLeft*lc.tempFlex/flexTotal+0.5);
582 if (toadd > 0) then
583 begin
584 // size changed
585 lc.desiredsize.w += toadd;
586 curx += toadd;
587 // compensate (crudely) rounding errors
588 if (curx > me.desiredsize.w-me.margins.horiz) then begin lc.desiredsize.w -= 1; curx -= 1; end;
589 // relayout children
590 layBox(lc.firstChild);
591 end;
592 end;
593 // expand or align
594 if (lc.expand) then lc.desiredsize.h := nmin(lc.maxsize.h, lineh) // expand
595 else if (lc.aligndir > 0) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) // bottom align
596 else if (lc.aligndir = 0) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) div 2; // center
597 if (not osz.equals(lc.desiredsize)) then
598 begin
599 if (lc.inGroup) then groupElementChanged := true;
600 // relayout children
601 layBox(lc.firstChild);
602 end;
603 i0 := lc.nextSibling;
604 end;
605 flexTotal := 0;
606 flexBoxCount := 0;
607 spaceLeft := me.desiredsize.w-me.margins.horiz;
608 end;
611 // do box layouting; call `layBox()` recursively if necessary
612 procedure TFlexLayouterBase.layBox (boxidx: LayControlIdx);
613 var
614 me: PLayControl;
615 flexTotal: Integer; // total sum of flex fields
616 flexBoxCount: Integer; // number of boxes
617 spaceLeft: Single;
618 cury: Integer;
619 maxwdt, maxhgt: Integer;
620 lineStartIdx: LayControlIdx;
621 lc: PLayControl;
622 doWrap: Boolean;
623 toadd: Integer;
624 osz: TLaySize;
625 begin
626 if (boxidx < 0) or (boxidx >= Length(ctlist)) then exit;
627 me := @ctlist[boxidx];
629 // if we have no children, there's nothing to do
630 if (me.firstChild = -1) then exit;
632 // first, layout all children
633 for lc in forChildren(boxidx) do layBox(lc.myidx);
635 // second, layout lines, distribute flex data
636 if (me.horizBox) then
637 begin
638 // horizontal boxes
639 cury := me.margins.top;
640 maxhgt := 0;
642 fixLine(me, -1, -1, cury, spaceLeft, flexTotal, flexBoxCount); //HACK!
644 lineStartIdx := me.firstChild;
646 for lc in forChildren(boxidx) do
647 begin
648 // new line?
649 doWrap := (not lc.firstInLine) and (lc.lineStart);
650 // need to wrap?
651 if (not doWrap) and (lc.canWrap) and (lc.canWrap) and (lc.desiredsize.w > 0) and (spaceLeft < lc.desiredsize.w) then doWrap := true;
652 if (doWrap) then
653 begin
654 // new line, fix this one
655 if (not lc.didWrap) then
656 begin
657 wrappingChanged := true;
658 lc.didWrap := true;
659 end;
660 fixLine(me, lineStartIdx, lc.myidx, cury, spaceLeft, flexTotal, flexBoxCount);
661 cury += maxhgt;
662 lineStartIdx := lc.myidx;
663 end
664 else
665 begin
666 if (lc.didWrap) then
667 begin
668 wrappingChanged := true;
669 lc.didWrap := false;
670 end;
671 end;
672 spaceLeft -= lc.desiredsize.w;
673 if (maxhgt < lc.desiredsize.h) then maxhgt := lc.desiredsize.h;
674 if (lc.tempFlex > 0) then
675 begin
676 flexTotal += lc.tempFlex;
677 flexBoxCount += 1;
678 end;
679 end;
680 // fix last line
681 fixLine(me, lineStartIdx, -1, cury, spaceLeft, flexTotal, flexBoxCount);
682 end
683 else
684 begin
685 // vertical boxes
686 maxwdt := 0;
687 flexTotal := 0;
688 flexBoxCount := 0;
689 spaceLeft := me.desiredsize.h-me.margins.vert;
691 // calc flex
692 for lc in forChildren(boxidx) do
693 begin
694 spaceLeft -= lc.desiredsize.h;
695 if (maxwdt < lc.desiredsize.w) then maxwdt := lc.desiredsize.w;
696 if (lc.tempFlex > 0) then
697 begin
698 flexTotal += lc.tempFlex;
699 flexBoxCount += 1;
700 end;
701 end;
703 // distribute space
704 cury := me.margins.top;
705 //writeln('me: ', boxidx, '; margins: ', me.margins.toString);
706 for lc in forChildren(boxidx) do
707 begin
708 osz := lc.desiredsize;
709 lc.desiredsize := lc.startsize;
710 lc.desiredpos.x := me.margins.left;
711 lc.desiredpos.y := cury;
712 cury += lc.desiredsize.h;
713 // fix flexbox size
714 if (lc.tempFlex > 0) and (spaceLeft > 0) then
715 begin
716 toadd := trunc(spaceLeft*lc.tempFlex/flexTotal+0.5);
717 if (toadd > 0) then
718 begin
719 // size changed
720 lc.desiredsize.h += toadd;
721 cury += toadd;
722 // compensate (crudely) rounding errors
723 if (cury > me.desiredsize.h-me.margins.vert) then begin lc.desiredsize.h -= 1; cury -= 1; end;
724 end;
725 end;
726 // expand or align
727 if (lc.expand) then lc.desiredsize.w := nmin(lc.maxsize.w, me.desiredsize.w-me.margins.vert) // expand
728 else if (lc.aligndir > 0) then lc.desiredpos.x := me.desiredsize.w-me.margins.right-lc.desiredsize.w // right align
729 else if (lc.aligndir = 0) then lc.desiredpos.x := (me.desiredsize.w-me.margins.horiz-lc.desiredsize.w) div 2; // center
730 if (not osz.equals(lc.desiredsize)) then
731 begin
732 if (lc.inGroup) then groupElementChanged := true;
733 // relayout children
734 layBox(lc.firstChild);
735 end;
736 end;
737 end;
738 end;
741 (*
742 second pass:
743 calcluate desired sizes (process flexes) using 'startsize', set 'desiredsize' and 'desiredpos'
744 if control has children, call 'second pass' recursively with this control as parent
745 flags set:
746 'group-element-changed', if any group element size was changed
747 'wrapping-changed', if not 'firsttime', and wrapping was changed (i.e. first pass will not set the flag)
748 *)
749 procedure TFlexLayouterBase.secondPass ();
750 begin
751 // reset flags
752 groupElementChanged := false;
753 wrappingChanged := false;
755 if (Length(ctlist) > 0) then
756 begin
757 ctlist[0].desiredpos := TLayPos.Create(0, 0);
758 layBox(0);
759 end;
761 // fix 'wrapping-changed' flag
762 if (firstTime) then begin wrappingChanged := false; firstTime := false; end;
763 end;
766 (*
767 third pass:
768 if 'group-element-changed':
769 for each group: adjust controls to max desired size (startsize), set 'temp-flex' flags to 0 for 'em, set 'second-again' flag
770 for other controls: if 'desiredsize' > 'maxsize', set 'startsize' to 'maxsize', set 'temp-flex' flag to 0, set 'second-again' flag
771 if 'second-again' or 'wrapping-changed':
772 reset 'second-again'
773 reset 'wrapping-changed'
774 reset 'firsttime'
775 goto second pass
776 *)
777 procedure TFlexLayouterBase.thirdPass ();
778 var
779 secondAgain: Boolean;
780 gtype: Integer;
781 maxsz: Integer;
782 grp: PLayGroup;
783 f, c: Integer;
784 cidx: LayControlIdx;
785 begin
786 while true do
787 begin
788 secondPass();
789 secondAgain := false;
790 if (groupElementChanged) then
791 begin
792 secondAgain := true;
793 // find max size for group, adjust 'startsize' controls to group max size
794 for gtype := 0 to 1 do
795 begin
796 for f := 0 to High(groups[gtype]) do
797 begin
798 grp := @groups[gtype][f];
799 maxsz := 0;
800 for c := 0 to High(grp.ctls) do
801 begin
802 cidx := grp.ctls[c];
803 if (maxsz < ctlist[cidx].startsize[gtype]) then maxsz := ctlist[cidx].startsize[gtype];
804 end;
805 for c := 0 to High(grp.ctls) do
806 begin
807 cidx := grp.ctls[c];
808 ctlist[cidx].startsize[gtype] := maxsz;
809 ctlist[cidx].desiredsize[gtype] := maxsz;
810 ctlist[cidx].tempFlex := 0; // don't change control size anymore
811 end;
812 end;
813 end;
814 end
815 else
816 begin
817 for f := 0 to High(ctlist) do
818 begin
819 for c := 0 to 1 do
820 begin
821 if (ctlist[f].maxsize[c] <= 0) then continue;
822 if (ctlist[f].desiredsize[c] > ctlist[f].maxsize[c]) then
823 begin
824 //writeln('ctl #', f, '; dimension #', c, ': desired=', ctlist[f].desiredsize[c], '; max=', ctlist[f].maxsize[c]);
825 ctlist[f].startsize[c] := ctlist[f].maxsize[c];
826 ctlist[f].desiredsize[c] := ctlist[f].maxsize[c];
827 ctlist[f].tempFlex := 0; // don't change control size anymore
828 secondAgain := true;
829 end;
830 end;
831 end;
832 end;
833 if (not secondAgain) and (not wrappingChanged) then break;
834 firstTime := false;
835 end;
836 end;
839 (*
840 fourth pass:
841 set 'actualsize' and 'actualpos' to 'desiredsize' and 'desiredpos'
842 return
843 *)
844 procedure TFlexLayouterBase.fourthPass ();
845 var
846 f: Integer;
847 begin
848 for f := 0 to High(ctlist) do
849 begin
850 ctlist[f].ctl.setActualSizePos(ctlist[f].desiredpos, ctlist[f].desiredsize);
851 end;
852 end;
855 procedure TFlexLayouterBase.layout ();
856 begin
857 firstPass();
858 secondPass();
859 thirdPass();
860 fourthPass();
861 end;
864 procedure TFlexLayouterBase.dumpFlat ();
865 var
866 f: Integer;
867 lc: PLayControl;
868 ds, ms: TLaySize;
869 begin
870 for f := 0 to High(ctlist) do
871 begin
872 lc := @ctlist[f];
873 ds := lc.ctl.getDefSize;
874 ms := lc.ctl.getMaxSize;
875 writeln(lc.myidx, ': startsize:', lc.startsize.toString(), '; desiredsize=', lc.desiredsize.toString(), '; maxsize=', lc.maxsize.toString(), '; tempFlex=', lc.tempFlex, '; flags=', lc.flags,
876 '; parent=', lc.parent, '; next=', lc.nextSibling, '; child=', lc.firstChild, '; ctl.size=', ds.toString(), '; ctl.maxsize=', ms.toString());
877 end;
878 end;
881 procedure TFlexLayouterBase.dumpList (cidx: LayControlIdx; indent: Integer);
882 var
883 lc: PLayControl;
884 f: Integer;
885 begin
886 while (cidx >= 0) do
887 begin
888 lc := @ctlist[cidx];
889 for f := 0 to indent do write(' ');
890 writeln(lc.myidx, ': startsize:', lc.startsize.toString, '; desiredsize=', lc.desiredsize.toString, '; maxsize=', lc.maxsize.toString, '; tempFlex=', lc.tempFlex, '; despos=', lc.desiredpos.toString);
891 dumpList(lc.firstChild, indent+2);
892 cidx := lc.nextSibling;
893 end;
894 end;
897 procedure TFlexLayouterBase.dump ();
898 begin
899 dumpList(0, 0);
900 end;
903 // ////////////////////////////////////////////////////////////////////////// //
904 (*
905 void main () begin
906 auto win := new GuiControl();
907 (win ~= new GuiControl()).mSize := TLaySize(10, 5);
908 (win ~= new GuiControl()).mSize := TLaySize(16, 8);
910 //win.mSize := TLaySize(40, 20);
912 auto lay := TFlexLayouterBase!GuiControl();
913 lay.setup(win);
915 writeln('============================');
916 lay.dumpFlat();
918 writeln('=== initial ===');
919 lay.dump();
921 //lay.calcMaxSizeInternal(0);
922 /*
923 lay.firstPass();
924 writeln('=== after first pass ===');
925 lay.dump();
927 lay.secondPass();
928 writeln('=== after second pass ===');
929 lay.dump();
930 */
931 lay.layout();
932 writeln('=== final ===');
933 lay.dump();
934 *)
935 end.