DEADSOFTWARE

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