DEADSOFTWARE

0f5d9286fe289a96a85d723736c78aff288b7a90
[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 class function nminX (a, b: Integer): Integer; inline;
103 private
104 // flags
105 const
106 FlagHorizBox = LongWord(1) shl 0; // horizontal layout for children
107 FlagLineStart = LongWord(1) shl 1;
108 FlagLineCanWrap = LongWord(1) shl 2;
109 // internal
110 FlagLineDidWrap = LongWord(1) shl 3; // will be set when line was wrapped
111 FlagInGroup = LongWord(1) shl 4; // set if this control is a member of any group
112 FlagExpand = LongWord(1) shl 5;
113 FlagLineFirst = LongWord(1) shl 6;
115 private
116 type
117 PLayControl = ^TLayControl;
118 TLayControl = record
119 public
120 myidx: LayControlIdx;
121 tempFlex: Integer;
122 flags: LongWord; // see below
123 aligndir: Integer;
124 startsize: TLaySize; // current
125 desiredsize: TLaySize;
126 maxsize: TLaySize;
127 margins: TLayMargins; // can never be negative
128 desiredpos: TLayPos;
129 ctl: ControlT;
130 parent: LayControlIdx; // = -1;
131 firstChild: LayControlIdx; // = -1;
132 nextSibling: LayControlIdx; // = -1;
134 private
135 function getDidWrap (): Boolean; inline;
136 procedure setDidWrap (v: Boolean); inline;
138 public
139 procedure initialize (); inline;
141 function horizBox (): Boolean; inline;
142 function lineStart (): Boolean; inline;
143 function canWrap (): Boolean; inline;
144 function inGroup (): Boolean; inline;
145 function firstInLine (): Boolean; inline;
147 function getExpand (): Boolean; inline;
148 procedure setExpand (v: Boolean); inline;
150 function alignLeft (): Boolean; inline;
151 function alignTop (): Boolean; inline;
152 function alignRight (): Boolean; inline;
153 function alignBottom (): Boolean; inline;
154 function alignCenter (): Boolean; inline;
156 public
157 property didWrap: Boolean read getDidWrap write setDidWrap;
158 property expand: Boolean read getExpand write setExpand;
159 end;
161 PLayGroup = ^TLayGroup;
162 TLayGroup = record
163 name: AnsiString;
164 ctls: array of LayControlIdx;
165 end;
167 TLayCtlArray = array of TLayControl;
168 TLayGrpArray = array of TLayGroup;
170 private
171 ctlist: TLayCtlArray;
172 groups: array[0..1] of TLayGrpArray; // horiz, vert
174 firstTime: Boolean;
175 groupElementChanged: Boolean;
176 wrappingChanged: Boolean;
178 private
179 procedure fixFlags (cidx: LayControlIdx);
180 procedure doChildren (parent: LayControlIdx; child: ControlT);
181 procedure appendToGroup (const gname: AnsiString;cidx: LayControlIdx;gidx: Integer);
182 procedure setupGroups ();
184 // this also sets `tempFlex`
185 procedure calcMaxSizeInternal (cidx: LayControlIdx);
187 procedure fixLine (me: PLayControl; i0, i1: LayControlIdx; var cury: Integer; var spaceLeft: Single);
188 // do box layouting; call `layBox()` recursively if necessary
189 procedure layBox (boxidx: LayControlIdx);
191 procedure firstPass ();
192 procedure secondPass ();
193 procedure thirdPass ();
194 procedure fourthPass ();
196 procedure dumpList (cidx: LayControlIdx; indent: Integer);
198 public
199 type
200 TChildrenEnumerator = record
201 private
202 ctls: TLayCtlArray;
203 cur: Integer;
204 first: Boolean;
205 public
206 constructor Create (constref actls: TLayCtlArray; acur: Integer);
207 function moveNext (): Boolean; inline;
208 function getCurrent (): PLayControl; inline;
209 function getEnumerator (): TChildrenEnumerator; inline;
210 property current: PLayControl read getCurrent;
211 end;
213 public
214 constructor Create ();
215 destructor Destroy (); override;
217 // clear build lists
218 procedure clear ();
220 // build control and group lists
221 procedure setup (root: ControlT);
223 function forChildren (cidx: LayControlIdx): TChildrenEnumerator; inline;
225 procedure layout ();
227 procedure dumpFlat ();
228 procedure dump ();
229 end;
232 implementation
234 uses
235 utils;
238 // ////////////////////////////////////////////////////////////////////////// //
239 class function TFlexLayouterBase.nminX (a, b: Integer): Integer; inline;
240 begin
241 if (a < 0) then begin if (b < 0) then result := 0 else result := b; end
242 else if (b < 0) or (a < b) then result := a
243 else result := b;
244 end;
247 // ////////////////////////////////////////////////////////////////////////// //
248 procedure TFlexLayouterBase.TLayControl.initialize (); inline;
249 begin
250 FillChar(self, 0, sizeof(self));
251 parent := -1;
252 firstChild := -1;
253 nextSibling := -1;
254 end;
256 function TFlexLayouterBase.TLayControl.horizBox (): Boolean; inline; begin result := ((flags and FlagHorizBox) <> 0); end;
257 function TFlexLayouterBase.TLayControl.lineStart (): Boolean; inline; begin result := ((flags and FlagLineStart) <> 0); end;
258 function TFlexLayouterBase.TLayControl.canWrap (): Boolean; inline; begin result := ((flags and FlagLineCanWrap) <> 0); end;
259 function TFlexLayouterBase.TLayControl.inGroup (): Boolean; inline; begin result := ((flags and FlagInGroup) <> 0); end;
260 function TFlexLayouterBase.TLayControl.firstInLine (): Boolean; inline; begin result := ((flags and FlagLineFirst) <> 0); end;
262 function TFlexLayouterBase.TLayControl.getDidWrap (): Boolean; inline; begin result := ((flags and FlagLineDidWrap) <> 0); end;
263 procedure TFlexLayouterBase.TLayControl.setDidWrap (v: Boolean); inline; begin if (v) then flags := flags or FlagLineDidWrap else flags := flags and (not FlagLineDidWrap); end;
265 function TFlexLayouterBase.TLayControl.getExpand (): Boolean; inline; begin result := ((flags and FlagExpand) <> 0); end;
266 procedure TFlexLayouterBase.TLayControl.setExpand (v: Boolean); inline; begin if (v) then flags := flags or FlagExpand else flags := flags and (not FlagExpand); end;
268 function TFlexLayouterBase.TLayControl.alignLeft (): Boolean; inline; begin result := (aligndir < 0); end;
269 function TFlexLayouterBase.TLayControl.alignTop (): Boolean; inline; begin result := (aligndir < 0); end;
270 function TFlexLayouterBase.TLayControl.alignRight (): Boolean; inline; begin result := (aligndir > 0); end;
271 function TFlexLayouterBase.TLayControl.alignBottom (): Boolean; inline; begin result := (aligndir > 0); end;
272 function TFlexLayouterBase.TLayControl.alignCenter (): Boolean; inline; begin result := (aligndir = 0); end;
275 // ////////////////////////////////////////////////////////////////////////// //
276 constructor TFlexLayouterBase.TChildrenEnumerator.Create (constref actls: TLayCtlArray; acur: Integer);
277 begin
278 ctls := actls;
279 cur := acur;
280 first := true;
281 end;
283 function TFlexLayouterBase.TChildrenEnumerator.moveNext (): Boolean; inline;
284 begin
285 if first then
286 begin
287 if (cur >= 0) and (cur < Length(ctls)) then cur := ctls[cur].firstChild else cur := -1;
288 first := false;
289 end
290 else
291 begin
292 cur := ctls[cur].nextSibling;
293 end;
294 result := (cur >= 0);
295 end;
297 function TFlexLayouterBase.TChildrenEnumerator.getCurrent (): PLayControl; inline;
298 begin
299 result := @ctls[cur];
300 end;
302 function TFlexLayouterBase.TChildrenEnumerator.getEnumerator (): TChildrenEnumerator; inline;
303 begin
304 result := self;
305 end;
308 // ////////////////////////////////////////////////////////////////////////// //
309 constructor TFlexLayouterBase.Create ();
310 begin
311 ctlist := nil;
312 groups[0] := nil;
313 groups[1] := nil;
315 firstTime := false;
316 groupElementChanged := false;
317 wrappingChanged := false;
318 end;
321 destructor TFlexLayouterBase.Destroy ();
322 begin
323 clear();
324 inherited;
325 end;
328 function TFlexLayouterBase.forChildren (cidx: LayControlIdx): TChildrenEnumerator; inline;
329 begin
330 result := TChildrenEnumerator.Create(ctlist, cidx);
331 end;
334 procedure TFlexLayouterBase.clear ();
335 begin
336 ctlist := nil;
337 groups[0] := nil;
338 groups[1] := nil;
339 end;
342 procedure TFlexLayouterBase.fixFlags (cidx: LayControlIdx);
343 var
344 lc: PLayControl;
345 begin
346 assert((cidx >= 0) and (cidx < Length(ctlist)));
347 lc := @ctlist[cidx];
348 //lc.flags := 0;
349 if (lc.ctl.isHorizBox) then lc.flags := lc.flags or FlagHorizBox;
350 if (lc.ctl.isLineStart) then lc.flags := lc.flags or FlagLineStart;
351 if (lc.ctl.canWrap) then lc.flags := lc.flags or FlagLineCanWrap;
352 if (lc.ctl.getExpand) then lc.flags := lc.flags or FlagExpand;
353 lc.aligndir := lc.ctl.getAlign;
354 end;
357 procedure TFlexLayouterBase.doChildren (parent: LayControlIdx; child: ControlT);
358 var
359 cidx: LayControlIdx = -1;
360 lc: PLayControl;
361 begin
362 assert((parent >= 0) and (parent < Length(ctlist)));
363 assert(ctlist[parent].firstChild = -1);
364 while (child <> nil) do
365 begin
366 child.layPrepare;
367 SetLength(ctlist, Length(ctlist)+1);
368 lc := @ctlist[High(ctlist)];
369 lc.initialize();
370 if (cidx = -1) then
371 begin
372 cidx := LayControlIdx(High(ctlist));
373 ctlist[parent].firstChild := cidx;
374 // first child is always linestart
375 lc.flags := lc.flags or FlagLineStart or FlagLineFirst;
376 end
377 else
378 begin
379 ctlist[cidx].nextSibling := LayControlIdx(High(ctlist));
380 cidx := LayControlIdx(High(ctlist));
381 end;
382 lc.myidx := cidx;
383 lc.ctl := child;
384 lc.parent := parent;
385 fixFlags(cidx);
386 doChildren(cidx, child.firstChild);
387 child := child.nextSibling;
388 end;
389 end;
392 procedure TFlexLayouterBase.appendToGroup (const gname: AnsiString; cidx: LayControlIdx; gidx: Integer);
393 var
394 f: Integer;
395 begin
396 if (Length(gname) = 0) then exit;
397 assert((cidx >= 0) and (cidx < Length(ctlist)));
398 assert((gidx = 0) or (gidx = 1));
399 ctlist[cidx].flags := ctlist[cidx].flags or FlagInGroup;
400 for f := 0 to High(groups[gidx]) do
401 begin
402 if (groups[gidx][f].name = gname) then
403 begin
404 SetLength(groups[gidx][f].ctls, Length(groups[gidx][f].ctls)+1);
405 groups[gidx][f].ctls[High(groups[gidx][f].ctls)] := cidx;
406 exit;
407 end;
408 end;
409 // new group
410 f := Length(groups[gidx]);
411 SetLength(groups[gidx], f+1);
412 groups[gidx][f].name := gname;
413 SetLength(groups[gidx][f].ctls, Length(groups[gidx][f].ctls)+1);
414 groups[gidx][f].ctls[High(groups[gidx][f].ctls)] := cidx;
415 end;
418 procedure TFlexLayouterBase.setupGroups ();
419 var
420 idx: Integer;
421 lc: PLayControl;
422 begin
423 for idx := 0 to High(ctlist) do
424 begin
425 lc := @ctlist[idx];
426 appendToGroup(lc.ctl.getHGroup, LayControlIdx(idx), 0);
427 appendToGroup(lc.ctl.getVGroup, LayControlIdx(idx), 1);
428 end;
429 end;
432 // build control and group lists
433 procedure TFlexLayouterBase.setup (root: ControlT);
434 begin
435 clear();
436 if (root = nil) then exit;
437 root.layPrepare;
438 try
439 SetLength(ctlist, 1);
440 ctlist[0].initialize();
441 ctlist[0].myidx := 0;
442 ctlist[0].ctl := root;
443 fixFlags(0);
444 ctlist[0].flags := ctlist[0].flags or FlagLineStart or FlagLineFirst;
445 doChildren(0, root.firstChild);
446 setupGroups();
447 except
448 clear();
449 raise;
450 end;
451 end;
454 // this also sets `tempFlex`
455 procedure TFlexLayouterBase.calcMaxSizeInternal (cidx: LayControlIdx);
456 var
457 lc, c: PLayControl;
458 msz: TLaySize;
459 negw, negh: Boolean;
460 zerow: Boolean;
461 curwdt, curhgt, totalhgt: Integer;
462 doWrap: Boolean;
463 begin
464 if (cidx < 0) or (cidx >= Length(ctlist)) then exit;
466 lc := @ctlist[cidx];
467 msz := lc.ctl.getMaxSize;
468 negw := (lc.startsize.w < 0);
469 negh := (lc.startsize.h < 0);
470 zerow := (lc.startsize.w = 0);
472 lc.tempFlex := lc.ctl.getFlex;
474 for c in forChildren(cidx) do calcMaxSizeInternal(c.myidx);
476 if (lc.horizBox) then
477 begin
478 // horizontal boxes
479 if (negw) then lc.tempFlex := 0; // size is negative: don't expand
480 curwdt := lc.margins.horiz;
481 curhgt := 0;
482 totalhgt := lc.margins.vert;
483 for c in forChildren(cidx) do
484 begin
485 // new line?
486 doWrap := (not c.firstInLine) and (c.lineStart);
487 // need to wrap?
488 if (not doWrap) and zerow and (lc.canWrap) and (c.canWrap) and (msz.w > 0) and (curwdt+c.startsize.w > lc.startsize.w) then doWrap := true;
489 if (doWrap) then
490 begin
491 totalhgt += curhgt;
492 if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
493 curwdt := 0;
494 curhgt := 0;
495 end;
496 curwdt += c.startsize.w;
497 if (curhgt < c.startsize.h) then curhgt := c.startsize.h;
498 end;
499 //writeln('00: ', cidx, ': totalhgt=', totalhgt);
500 totalhgt += curhgt;
501 //writeln('01: ', cidx, ': totalhgt=', totalhgt);
502 if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
503 if (lc.startsize.h < totalhgt) then lc.startsize.h := totalhgt;
504 end
505 else
506 begin
507 // vertical boxes
508 if (negh) then lc.tempFlex := 0; // size is negative: don't expand
509 curhgt := lc.margins.vert;
510 for c in forChildren(cidx) do
511 begin
512 if (lc.startsize.w < c.startsize.w+lc.margins.horiz) then lc.startsize.w := c.startsize.w+lc.margins.horiz;
513 curhgt += c.startsize.h;
514 end;
515 if (lc.startsize.h < curhgt) then lc.startsize.h := curhgt;
516 end;
517 if (lc.startsize.w < 0) then lc.startsize.w := 0;
518 if (lc.startsize.h < 0) then lc.startsize.h := 0;
519 lc.maxsize := msz;
520 if (lc.maxsize.w < lc.startsize.w) then begin if (lc.maxsize.w >= 0) then lc.maxsize.w := lc.startsize.w; end;
521 if (lc.maxsize.h < lc.startsize.h) then begin if (lc.maxsize.h >= 0) then lc.maxsize.h := lc.startsize.h; end;
522 end;
525 procedure TFlexLayouterBase.firstPass ();
526 var
527 f, c: Integer;
528 needRecalcMaxSize: Boolean;
529 gtype: Integer;
530 grp: PLayGroup;
531 maxsz: Integer;
532 cidx: LayControlIdx;
533 ct: PLayControl;
534 mr: TLayMargins;
535 begin
536 // reset all 'laywrap' flags for controls, set initial 'startsize'
537 for f := 0 to High(ctlist) do
538 begin
539 ctlist[f].didWrap := false;
540 ctlist[f].startsize := ctlist[f].ctl.getDefSize;
541 mr := ctlist[f].ctl.getMargins;
542 ctlist[f].margins := mr;
543 //ctlist[f].startsize.w += mr.horiz;
544 //ctlist[f].startsize.h += mr.vert;
545 end;
546 // setup sizes
547 calcMaxSizeInternal(0); // this also sets `tempFlex`
548 //writeln('=== calculated max size (0) ==='); dump();
549 // find max size for group, adjust 'startsize' controls to group max size
550 needRecalcMaxSize := false;
551 for gtype := 0 to 1 do
552 begin
553 for f := 0 to High(groups[gtype]) do
554 begin
555 grp := @groups[gtype][f];
556 maxsz := 0;
557 for c := 0 to High(grp.ctls) do
558 begin
559 cidx := grp.ctls[c];
560 ct := @ctlist[cidx];
561 if (maxsz < ct.startsize[gtype]) then maxsz := ct.startsize[gtype];
562 end;
563 for c := 0 to High(grp.ctls) do
564 begin
565 cidx := grp.ctls[c];
566 ct := @ctlist[cidx];
567 if (maxsz <> ct.startsize[gtype]) then
568 begin
569 needRecalcMaxSize := true;
570 ct.startsize[gtype] := maxsz;
571 end;
572 end;
573 end;
574 end;
575 // recalc maxsize if necessary
576 if (needRecalcMaxSize) then calcMaxSizeInternal(0);
577 // set "desired size" to "start size"
578 for f := 0 to High(ctlist) do ctlist[f].desiredsize := ctlist[f].startsize;
579 // set flags
580 firstTime := true;
581 //writeln('=== calculated max size (final) ==='); dump();
582 end;
585 procedure TFlexLayouterBase.fixLine (me: PLayControl; i0, i1: LayControlIdx; var cury: Integer; var spaceLeft: Single);
586 var
587 flexTotal: Integer = 0; // total sum of flex fields
588 flexBoxCount: Integer = 0; // number of boxes
589 curx: Integer;
590 lc: PLayControl;
591 osz: TLaySize;
592 toadd: Integer;
593 sti0: Integer;
594 lineh: Integer;
595 begin
596 curx := me.margins.left;
597 sti0 := i0;
598 // calc minimal line height, count flexboxes
599 lineh := 0;
600 while (i0 <> i1) do
601 begin
602 lc := @ctlist[i0];
603 lineh := nmax(lineh, lc.startsize.h);
604 if (lc.tempFlex > 0) then begin flexTotal += lc.tempFlex; flexBoxCount += 1; end;
605 i0 := lc.nextSibling;
606 end;
607 // distribute space, expand/align
608 i0 := sti0;
609 while (i0 <> i1) do
610 begin
611 lc := @ctlist[i0];
612 osz := lc.desiredsize;
613 lc.desiredsize := lc.startsize;
614 lc.desiredpos.x := curx;
615 lc.desiredpos.y := cury;
616 curx += lc.desiredsize.w;
617 // fix flexbox size
618 if (lc.tempFlex > 0) and (spaceLeft > 0) then
619 begin
620 toadd := trunc(spaceLeft*lc.tempFlex/flexTotal+0.5);
621 if (toadd > 0) then
622 begin
623 // size changed
624 lc.desiredsize.w += toadd;
625 curx += toadd;
626 // compensate (crudely) rounding errors
627 if (curx > me.desiredsize.w-me.margins.horiz) then begin lc.desiredsize.w -= 1; curx -= 1; end;
628 // relayout children
629 layBox(lc.firstChild);
630 end;
631 end;
632 // expand or align
633 if (lc.expand) then lc.desiredsize.h := nminX(lc.maxsize.h, lineh) // expand
634 else if (lc.alignBottom) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) // bottom align
635 else if (lc.alignCenter) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) div 2; // center
636 if (not osz.equals(lc.desiredsize)) then
637 begin
638 if (lc.inGroup) then groupElementChanged := true;
639 // relayout children
640 layBox(lc.firstChild);
641 end;
642 i0 := lc.nextSibling;
643 end;
644 flexTotal := 0;
645 flexBoxCount := 0;
646 spaceLeft := me.desiredsize.w-me.margins.horiz;
647 cury += lineh;
648 end;
651 // do box layouting; call `layBox()` recursively if necessary
652 procedure TFlexLayouterBase.layBox (boxidx: LayControlIdx);
653 var
654 me: PLayControl;
655 flexTotal: Integer; // total sum of flex fields
656 flexBoxCount: Integer; // number of boxes
657 spaceLeft: Single;
658 cury: Integer;
659 maxwdt: Integer;
660 lineStartIdx: LayControlIdx;
661 lc: PLayControl;
662 doWrap: Boolean;
663 toadd: Integer;
664 osz: TLaySize;
665 begin
666 if (boxidx < 0) or (boxidx >= Length(ctlist)) then exit;
667 me := @ctlist[boxidx];
669 // if we have no children, there's nothing to do
670 if (me.firstChild = -1) then exit;
672 // first, layout all children
673 for lc in forChildren(boxidx) do layBox(lc.myidx);
675 // second, layout lines, distribute flex data
676 if (me.horizBox) then
677 begin
678 // horizontal boxes
679 cury := me.margins.top;
681 fixLine(me, -1, -1, cury, spaceLeft); //HACK!
683 lineStartIdx := me.firstChild;
684 for lc in forChildren(boxidx) do
685 begin
686 // new line?
687 doWrap := (not lc.firstInLine) and (lc.lineStart);
688 // need to wrap?
689 if (not doWrap) and (lc.canWrap) and (lc.canWrap) and (lc.desiredsize.w > 0) and (spaceLeft < lc.desiredsize.w) then doWrap := true;
690 if (doWrap) then
691 begin
692 // new line, fix this one
693 if (not lc.didWrap) then begin wrappingChanged := true; lc.didWrap := true; end;
694 fixLine(me, lineStartIdx, lc.myidx, cury, spaceLeft);
695 lineStartIdx := lc.myidx;
696 end
697 else
698 begin
699 if (lc.didWrap) then begin wrappingChanged := true; lc.didWrap := false; end;
700 end;
701 spaceLeft -= lc.desiredsize.w;
702 //if (maxhgt < lc.desiredsize.h) then maxhgt := lc.desiredsize.h;
703 //if (lc.tempFlex > 0) then begin flexTotal += lc.tempFlex; flexBoxCount += 1; end;
704 end;
705 // fix last line
706 fixLine(me, lineStartIdx, -1, cury, spaceLeft);
707 end
708 else
709 begin
710 // vertical boxes
711 maxwdt := 0;
712 flexTotal := 0;
713 flexBoxCount := 0;
714 spaceLeft := me.desiredsize.h-me.margins.vert;
716 // calc flex
717 for lc in forChildren(boxidx) do
718 begin
719 spaceLeft -= lc.desiredsize.h;
720 if (maxwdt < lc.desiredsize.w) then maxwdt := lc.desiredsize.w;
721 if (lc.tempFlex > 0) then begin flexTotal += lc.tempFlex; flexBoxCount += 1; end;
722 end;
724 // distribute space
725 cury := me.margins.top;
726 //writeln('me: ', boxidx, '; margins: ', me.margins.toString);
727 for lc in forChildren(boxidx) do
728 begin
729 osz := lc.desiredsize;
730 lc.desiredsize := lc.startsize;
731 lc.desiredpos.x := me.margins.left;
732 lc.desiredpos.y := cury;
733 cury += lc.desiredsize.h;
734 // fix flexbox size
735 if (lc.tempFlex > 0) and (spaceLeft > 0) then
736 begin
737 toadd := trunc(spaceLeft*lc.tempFlex/flexTotal+0.5);
738 if (toadd > 0) then
739 begin
740 // size changed
741 lc.desiredsize.h += toadd;
742 cury += toadd;
743 // compensate (crudely) rounding errors
744 if (cury > me.desiredsize.h-me.margins.vert) then begin lc.desiredsize.h -= 1; cury -= 1; end;
745 end;
746 end;
747 // expand or align
748 if (lc.expand) then lc.desiredsize.w := nminX(lc.maxsize.w, me.desiredsize.w-me.margins.vert) // expand
749 else if (lc.alignRight) then lc.desiredpos.x := me.desiredsize.w-me.margins.right-lc.desiredsize.w // right align
750 else if (lc.alignCenter) then lc.desiredpos.x := (me.desiredsize.w-lc.desiredsize.w) div 2; // center
751 if (not osz.equals(lc.desiredsize)) then
752 begin
753 if (lc.inGroup) then groupElementChanged := true;
754 // relayout children
755 layBox(lc.firstChild);
756 end;
757 end;
758 end;
759 end;
762 (*
763 second pass:
764 calcluate desired sizes (process flexes) using 'startsize', set 'desiredsize' and 'desiredpos'
765 if control has children, call 'second pass' recursively with this control as parent
766 flags set:
767 'group-element-changed', if any group element size was changed
768 'wrapping-changed', if not 'firsttime', and wrapping was changed (i.e. first pass will not set the flag)
769 *)
770 procedure TFlexLayouterBase.secondPass ();
771 begin
772 // reset flags
773 groupElementChanged := false;
774 wrappingChanged := false;
776 if (Length(ctlist) > 0) then
777 begin
778 ctlist[0].desiredpos := TLayPos.Create(0, 0);
779 layBox(0);
780 end;
782 // fix 'wrapping-changed' flag
783 if (firstTime) then begin wrappingChanged := false; firstTime := false; end;
784 end;
787 (*
788 third pass:
789 if 'group-element-changed':
790 for each group: adjust controls to max desired size (startsize), set 'temp-flex' flags to 0 for 'em, set 'second-again' flag
791 for other controls: if 'desiredsize' > 'maxsize', set 'startsize' to 'maxsize', set 'temp-flex' flag to 0, set 'second-again' flag
792 if 'second-again' or 'wrapping-changed':
793 reset 'second-again'
794 reset 'wrapping-changed'
795 reset 'firsttime'
796 goto second pass
797 *)
798 procedure TFlexLayouterBase.thirdPass ();
799 var
800 secondAgain: Boolean;
801 gtype: Integer;
802 maxsz: Integer;
803 grp: PLayGroup;
804 f, c: Integer;
805 cidx: LayControlIdx;
806 ct: PLayControl;
807 begin
808 while true do
809 begin
810 secondPass();
811 secondAgain := false;
812 if (groupElementChanged) then
813 begin
814 secondAgain := true;
815 // find max size for group, adjust 'startsize' controls to group max size
816 for gtype := 0 to 1 do
817 begin
818 for f := 0 to High(groups[gtype]) do
819 begin
820 grp := @groups[gtype][f];
821 maxsz := 0;
822 for c := 0 to High(grp.ctls) do
823 begin
824 cidx := grp.ctls[c];
825 ct := @ctlist[cidx];
826 ct.expand := false; // don't expand grouped controls anymore
827 if (maxsz < ct.startsize[gtype]) then maxsz := ct.startsize[gtype];
828 end;
829 for c := 0 to High(grp.ctls) do
830 begin
831 cidx := grp.ctls[c];
832 ct := @ctlist[cidx];
833 ct.startsize[gtype] := maxsz;
834 ct.desiredsize[gtype] := maxsz;
835 ct.tempFlex := 0; // don't change control size anymore
836 end;
837 end;
838 end;
839 end
840 else
841 begin
842 for f := 0 to High(ctlist) do
843 begin
844 ct := @ctlist[f];
845 if (ct.inGroup) then
846 begin
847 ct.expand := false; // don't expand grouped controls anymore
848 ct.tempFlex := 0; // don't change control size anymore
849 end;
850 for c := 0 to 1 do
851 begin
852 if (ct.maxsize[c] < 0) then continue;
853 if (ct.desiredsize[c] > ct.maxsize[c]) then
854 begin
855 //writeln('ctl #', f, '; dimension #', c, ': desired=', ctlist[f].desiredsize[c], '; max=', ctlist[f].maxsize[c]);
856 ct.startsize[c] := ct.maxsize[c];
857 ct.desiredsize[c] := ct.maxsize[c];
858 ct.tempFlex := 0; // don't change control size anymore
859 secondAgain := true;
860 end;
861 end;
862 end;
863 end;
864 if (not secondAgain) and (not wrappingChanged) then break;
865 firstTime := false;
866 end;
867 end;
870 (*
871 fourth pass:
872 set 'actualsize' and 'actualpos' to 'desiredsize' and 'desiredpos'
873 return
874 *)
875 procedure TFlexLayouterBase.fourthPass ();
876 var
877 f: Integer;
878 begin
879 for f := 0 to High(ctlist) do
880 begin
881 ctlist[f].ctl.setActualSizePos(ctlist[f].desiredpos, ctlist[f].desiredsize);
882 end;
883 end;
886 procedure TFlexLayouterBase.layout ();
887 begin
888 firstPass();
889 secondPass();
890 thirdPass();
891 fourthPass();
892 end;
895 procedure TFlexLayouterBase.dumpFlat ();
896 var
897 f: Integer;
898 lc: PLayControl;
899 ds, ms: TLaySize;
900 begin
901 for f := 0 to High(ctlist) do
902 begin
903 lc := @ctlist[f];
904 ds := lc.ctl.getDefSize;
905 ms := lc.ctl.getMaxSize;
906 writeln(lc.myidx, ': startsize:', lc.startsize.toString(), '; desiredsize=', lc.desiredsize.toString(), '; maxsize=', lc.maxsize.toString(), '; tempFlex=', lc.tempFlex, '; flags=', lc.flags,
907 '; parent=', lc.parent, '; next=', lc.nextSibling, '; child=', lc.firstChild, '; ctl.size=', ds.toString(), '; ctl.maxsize=', ms.toString());
908 end;
909 end;
912 procedure TFlexLayouterBase.dumpList (cidx: LayControlIdx; indent: Integer);
913 var
914 lc: PLayControl;
915 f: Integer;
916 begin
917 while (cidx >= 0) do
918 begin
919 lc := @ctlist[cidx];
920 for f := 0 to indent do write(' ');
921 writeln(lc.myidx, ': startsize:', lc.startsize.toString, '; desiredsize=', lc.desiredsize.toString, '; maxsize=', lc.maxsize.toString, '; tempFlex=', lc.tempFlex, '; despos=', lc.desiredpos.toString);
922 dumpList(lc.firstChild, indent+2);
923 cidx := lc.nextSibling;
924 end;
925 end;
928 procedure TFlexLayouterBase.dump ();
929 begin
930 dumpList(0, 0);
931 end;
934 // ////////////////////////////////////////////////////////////////////////// //
935 (*
936 void main () begin
937 auto win := new GuiControl();
938 (win ~= new GuiControl()).mSize := TLaySize(10, 5);
939 (win ~= new GuiControl()).mSize := TLaySize(16, 8);
941 //win.mSize := TLaySize(40, 20);
943 auto lay := TFlexLayouterBase!GuiControl();
944 lay.setup(win);
946 writeln('============================');
947 lay.dumpFlat();
949 writeln('=== initial ===');
950 lay.dump();
952 //lay.calcMaxSizeInternal(0);
953 /*
954 lay.firstPass();
955 writeln('=== after first pass ===');
956 lay.dump();
958 lay.secondPass();
959 writeln('=== after second pass ===');
960 lay.dump();
961 */
962 lay.layout();
963 writeln('=== final ===');
964 lay.dump();
965 *)
966 end.