1 (* coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
2 * Understanding is not required. Only obedience.
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, version 3 of the License ONLY.
8 * This program is distributed in the hope that it will be useful,
9 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 * GNU General Public License for more details.
13 * You should have received a copy of the GNU General Public License
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 {$INCLUDE ../shared/a_modes.inc}
19 control default size will be increased by margins
20 negative margins are ignored
22 procedure layPrepare (); // called before registering control in layouter
23 function getDefSize (): TLaySize; // default size; <0: use max size
24 function getMargins (): TLayMargins;
25 function getPadding (): TLaySize; // children padding (each non-first child will get this on left/top)
26 function getMaxSize (): TLaySize; // max size; <0: set to some huge value
27 function getFlex (): Integer; // <=0: not flexible
28 function isHorizBox (): Boolean; // horizontal layout for children?
29 function noPad (): Boolean; // ignore padding in box direction for this control
30 function getAlign (): Integer; // aligning in non-main direction: <0: left/up; 0: center; >0: right/down
31 function getExpand (): Boolean; // expanding in non-main direction: `true` will ignore align and eat all available space
32 procedure setActualSizePos (constref apos: TLayPos; constref asize: TLaySize);
33 function getHGroup (): AnsiString; // empty: not grouped
34 function getVGroup (): AnsiString; // empty: not grouped
35 function nextSibling (): ControlT;
36 function firstChild (): ControlT;
45 // ////////////////////////////////////////////////////////////////////////// //
47 generic TFlexLayouterBase
<ControlT
> = class
52 type LayControlIdx
= Integer;
57 FlagHorizBox
= LongWord(1) shl 0; // horizontal layout for children
58 FlagNoPad
= LongWord(1) shl 1;
59 FlagExpand
= LongWord(1) shl 2;
61 FlagInGroupH
= LongWord(1) shl 8; // set if this control is a member of any group
62 FlagInGroupV
= LongWord(1) shl 9; // set if this control is a member of any group
66 PLayControl
= ^TLayControl
;
71 flags
: LongWord; // see above
73 startsize
: TLaySize
; // original size
74 desiredsize
: TLaySize
; // current size
75 maxsize
: TLaySize
; // current maximum size
76 margins
: TLayMargins
; // can never be negative
80 parent
: LayControlIdx
; // = -1;
81 firstChild
: LayControlIdx
; // = -1;
82 nextSibling
: LayControlIdx
; // = -1;
85 procedure initialize (); inline;
87 function horizBox (): Boolean; inline;
88 function inGroup (idx
: Integer): Boolean; inline;
89 function noPad (): Boolean; inline;
91 function getExpand (): Boolean; inline;
92 procedure setExpand (v
: Boolean); inline;
94 function alignLeft (): Boolean; inline;
95 function alignTop (): Boolean; inline;
96 function alignRight (): Boolean; inline;
97 function alignBottom (): Boolean; inline;
98 function alignCenter (): Boolean; inline;
100 function visible (): Boolean; inline;
103 property expand
: Boolean read getExpand write setExpand
;
106 PLayGroup
= ^TLayGroup
;
109 ctls
: array of LayControlIdx
;
112 TLayCtlArray
= array of TLayControl
;
113 TLayGrpArray
= array of TLayGroup
;
116 ctlist
: TLayCtlArray
;
117 groups
: array[0..1] of TLayGrpArray
; // horiz, vert
118 groupElementChanged
: Boolean;
121 procedure firstTimeSetup (cidx
: LayControlIdx
);
122 procedure doChildren (parent
: LayControlIdx
; child
: ControlT
);
123 procedure appendToGroup (const gname
: AnsiString;cidx
: LayControlIdx
;gidx
: Integer);
124 procedure clearGroups ();
125 procedure setupGroups ();
127 procedure distributeChildren (boxidx
: LayControlIdx
; maindir
: Integer);
128 // do box layouting; call `layBox()` recursively if necessary
129 procedure layBox (boxidx
: LayControlIdx
);
131 procedure firstPass ();
132 procedure secondPass ();
133 procedure thirdPass ();
135 procedure dumpList (cidx
: LayControlIdx
; indent
: Integer);
139 TChildrenEnumerator
= record
144 onlyVisible
: Boolean;
146 constructor Create (constref actls
: TLayCtlArray
; acur
: Integer; aonlyvis
: Boolean);
147 function moveNext (): Boolean; inline;
148 function getCurrent (): PLayControl
; inline;
149 function getEnumerator (): TChildrenEnumerator
; inline;
150 property current
: PLayControl read getCurrent
;
154 constructor Create ();
155 destructor Destroy (); override;
160 // build control and group lists
161 procedure setup (root
: ControlT
);
163 function forChildren (cidx
: LayControlIdx
): TChildrenEnumerator
; inline;
164 function forVisibleChildren (cidx
: LayControlIdx
): TChildrenEnumerator
; inline;
168 procedure dumpFlat ();
179 // ////////////////////////////////////////////////////////////////////////// //
180 procedure TFlexLayouterBase
.TLayControl
.initialize (); inline;
182 FillChar(self
, 0, sizeof(self
));
188 function TFlexLayouterBase
.TLayControl
.horizBox (): Boolean; inline; begin result
:= ((flags
and FlagHorizBox
) <> 0); end;
189 function TFlexLayouterBase
.TLayControl
.inGroup (idx
: Integer): Boolean; inline; begin if (idx
= 0) then result
:= ((flags
and FlagInGroupH
) <> 0) else if (idx
= 1) then result
:= ((flags
and FlagInGroupV
) <> 0) else result
:= false; end;
190 function TFlexLayouterBase
.TLayControl
.noPad (): Boolean; inline; begin result
:= ((flags
and FlagNoPad
) <> 0); end;
192 function TFlexLayouterBase
.TLayControl
.getExpand (): Boolean; inline; begin result
:= ((flags
and FlagExpand
) <> 0); end;
193 procedure TFlexLayouterBase
.TLayControl
.setExpand (v
: Boolean); inline; begin if (v
) then flags
:= flags
or FlagExpand
else flags
:= flags
and (not FlagExpand
); end;
195 function TFlexLayouterBase
.TLayControl
.alignLeft (): Boolean; inline; begin result
:= (aligndir
< 0); end;
196 function TFlexLayouterBase
.TLayControl
.alignTop (): Boolean; inline; begin result
:= (aligndir
< 0); end;
197 function TFlexLayouterBase
.TLayControl
.alignRight (): Boolean; inline; begin result
:= (aligndir
> 0); end;
198 function TFlexLayouterBase
.TLayControl
.alignBottom (): Boolean; inline; begin result
:= (aligndir
> 0); end;
199 function TFlexLayouterBase
.TLayControl
.alignCenter (): Boolean; inline; begin result
:= (aligndir
= 0); end;
201 function TFlexLayouterBase
.TLayControl
.visible (): Boolean; inline;
203 result
:= (startsize
.w
<> 0) or (startsize
.h
<> 0);
207 // ////////////////////////////////////////////////////////////////////////// //
208 constructor TFlexLayouterBase
.TChildrenEnumerator
.Create (constref actls
: TLayCtlArray
; acur
: Integer; aonlyvis
: Boolean);
213 onlyVisible
:= aonlyvis
;
216 function TFlexLayouterBase
.TChildrenEnumerator
.moveNext (): Boolean; inline;
222 if (cur
>= 0) and (cur
< Length(ctls
)) then cur
:= ctls
[cur
].firstChild
else cur
:= -1;
227 cur
:= ctls
[cur
].nextSibling
;
229 result
:= (cur
>= 0);
230 if (not result
) or (not onlyVisible
) then break
;
231 if (ctls
[cur
].visible
) then break
;
235 function TFlexLayouterBase
.TChildrenEnumerator
.getCurrent (): PLayControl
; inline;
237 result
:= @ctls
[cur
];
240 function TFlexLayouterBase
.TChildrenEnumerator
.getEnumerator (): TChildrenEnumerator
; inline;
246 // ////////////////////////////////////////////////////////////////////////// //
247 constructor TFlexLayouterBase
.Create ();
252 groupElementChanged
:= false;
256 destructor TFlexLayouterBase
.Destroy ();
263 function TFlexLayouterBase
.forChildren (cidx
: LayControlIdx
): TChildrenEnumerator
; inline;
265 result
:= TChildrenEnumerator
.Create(ctlist
, cidx
, false);
268 function TFlexLayouterBase
.forVisibleChildren (cidx
: LayControlIdx
): TChildrenEnumerator
; inline;
270 result
:= TChildrenEnumerator
.Create(ctlist
, cidx
, true);
274 procedure TFlexLayouterBase
.clear ();
281 procedure TFlexLayouterBase
.doChildren (parent
: LayControlIdx
; child
: ControlT
);
283 cidx
: LayControlIdx
= -1;
286 assert((parent
>= 0) and (parent
< Length(ctlist
)));
287 assert(ctlist
[parent
].firstChild
= -1);
288 while (child
<> nil) do
291 //if (msz.w = 0) or (msz.h = 0) then continue; // hidden controls will have zero maxsize, so skip 'em
292 SetLength(ctlist
, Length(ctlist
)+1);
293 lc
:= @ctlist
[High(ctlist
)];
297 cidx
:= LayControlIdx(High(ctlist
));
298 ctlist
[parent
].firstChild
:= cidx
;
302 ctlist
[cidx
].nextSibling
:= LayControlIdx(High(ctlist
));
303 cidx
:= LayControlIdx(High(ctlist
));
308 doChildren(cidx
, child
.firstChild
);
309 child
:= child
.nextSibling
;
314 procedure TFlexLayouterBase
.appendToGroup (const gname
: AnsiString; cidx
: LayControlIdx
; gidx
: Integer);
319 if (Length(gname
) = 0) then exit
;
320 assert((cidx
>= 0) and (cidx
< Length(ctlist
)));
321 assert((gidx
= 0) or (gidx
= 1));
322 if (gidx
= 0) then gflg
:= FlagInGroupH
else gflg
:= FlagInGroupV
;
323 ctlist
[cidx
].flags
:= ctlist
[cidx
].flags
or gflg
;
324 for f
:= 0 to High(groups
[gidx
]) do
326 if (groups
[gidx
][f
].name
= gname
) then
328 SetLength(groups
[gidx
][f
].ctls
, Length(groups
[gidx
][f
].ctls
)+1);
329 groups
[gidx
][f
].ctls
[High(groups
[gidx
][f
].ctls
)] := cidx
;
334 f
:= Length(groups
[gidx
]);
335 SetLength(groups
[gidx
], f
+1);
336 groups
[gidx
][f
].name
:= gname
;
337 SetLength(groups
[gidx
][f
].ctls
, Length(groups
[gidx
][f
].ctls
)+1);
338 groups
[gidx
][f
].ctls
[High(groups
[gidx
][f
].ctls
)] := cidx
;
342 procedure TFlexLayouterBase
.clearGroups ();
346 for gidx
:= 0 to 1 do
348 for f
:= 0 to High(groups
[gidx
]) do groups
[gidx
][f
].ctls
:= nil;
354 procedure TFlexLayouterBase
.setupGroups ();
357 idx
, gidx
, f
, c
: Integer;
361 for idx
:= 0 to High(ctlist
) do
364 appendToGroup(lc
.ctl
.getHGroup
, LayControlIdx(idx
), 0);
365 appendToGroup(lc
.ctl
.getVGroup
, LayControlIdx(idx
), 1);
367 // if control is only one in a group, mark is as "not grouped"
368 for gidx
:= 0 to 1 do
370 if (gidx
= 0) then gflg
:= LongWord(not FlagInGroupH
) else gflg
:= LongWord(not FlagInGroupV
);
372 while (f
< Length(groups
[gidx
])) do
374 if (Length(groups
[gidx
][f
].ctls
) < 2) then
377 for c
:= 0 to High(groups
[gidx
][f
].ctls
) do
379 lc
:= @ctlist
[groups
[gidx
][f
].ctls
[c
]];
380 lc
.flags
:= lc
.flags
and gflg
;
383 groups
[gidx
][f
].ctls
:= nil;
384 for c
:= f
+1 to High(groups
[gidx
]) do groups
[gidx
][c
-1] := groups
[gidx
][c
];
385 c
:= High(groups
[gidx
]);
386 groups
[gidx
][c
].ctls
:= nil;
387 SetLength(groups
[gidx
], c
);
398 // build control and group lists
399 procedure TFlexLayouterBase
.setup (root
: ControlT
);
402 if (root
= nil) then exit
;
405 SetLength(ctlist
, 1);
406 ctlist
[0].initialize();
407 ctlist
[0].myidx
:= 0;
408 ctlist
[0].ctl
:= root
;
409 doChildren(0, root
.firstChild
);
417 procedure TFlexLayouterBase
.firstTimeSetup (cidx
: LayControlIdx
);
421 assert((cidx
>= 0) and (cidx
< Length(ctlist
)));
424 if (lc
.ctl
.isHorizBox
) then lc
.flags
:= lc
.flags
or FlagHorizBox
;
425 if (lc
.ctl
.getExpand
) then lc
.flags
:= lc
.flags
or FlagExpand
;
426 if (lc
.ctl
.noPad
) then lc
.flags
:= lc
.flags
or FlagNoPad
;
427 lc
.aligndir
:= lc
.ctl
.getAlign
;
428 lc
.startsize
:= lc
.ctl
.getDefSize
;
429 //lc.startsize.w := nmax(0, lc.startsize.w);
430 //lc.startsize.h := nmax(0, lc.startsize.h);
431 lc
.margins
:= lc
.ctl
.getMargins
;
432 lc
.margins
.left
:= nmax(0, lc
.margins
.left
);
433 lc
.margins
.top
:= nmax(0, lc
.margins
.top
);
434 lc
.margins
.right
:= nmax(0, lc
.margins
.right
);
435 lc
.margins
.bottom
:= nmax(0, lc
.margins
.bottom
);
436 lc
.padding
:= lc
.ctl
.getPadding
;
437 lc
.padding
.w
:= nmax(0, lc
.padding
.w
);
438 lc
.padding
.h
:= nmax(0, lc
.padding
.h
);
439 lc
.maxsize
:= TLaySize
.Create(-1, -1);
440 if (lc
.maxsize
.w
>= 0) then lc
.startsize
.w
:= nmin(lc
.maxsize
.w
, lc
.startsize
.w
);
441 if (lc
.maxsize
.h
>= 0) then lc
.startsize
.h
:= nmin(lc
.maxsize
.h
, lc
.startsize
.h
);
442 lc
.desiredsize
:= lc
.startsize
;
443 lc
.tempFlex
:= lc
.ctl
.getFlex
;
447 procedure TFlexLayouterBase
.firstPass ();
452 groupElementChanged
:= false;
454 for f
:= 0 to High(ctlist
) do firstTimeSetup(f
);
455 // if we have any groups, set "group element changed" flag, so third pass will fix 'em
456 for gtype
:= 0 to 1 do
458 if (Length(groups
[gtype
]) > 0) then
460 groupElementChanged
:= true;
467 procedure TFlexLayouterBase
.distributeChildren (boxidx
: LayControlIdx
; maindir
: Integer);
471 marg0
, marg1
, margtotal
: Integer;
472 marg0Op
, marg1Op
, margtotalOp
: Integer;
473 flexTotal
: Integer = 0; // total sum of flex fields
474 spaceLeft
: Integer = 0;
475 dopad
: Boolean = false;
476 prevpad
: Boolean = false;
483 assert((boxidx
>= 0) and (boxidx
< Length(ctlist
)));
484 assert((maindir
= 0) or (maindir
= 1));
485 // cache some parameters
486 me
:= @ctlist
[boxidx
];
487 suppdir
:= 1-maindir
;
488 if (maindir
= 0) then
490 marg0
:= me
.margins
.left
;
491 marg1
:= me
.margins
.right
;
492 marg0Op
:= me
.margins
.top
;
493 marg1Op
:= me
.margins
.bottom
;
497 marg0
:= me
.margins
.top
;
498 marg1
:= me
.margins
.bottom
;
499 marg0Op
:= me
.margins
.left
;
500 marg1Op
:= me
.margins
.right
;
502 margtotal
:= marg0
+marg1
;
503 margtotalOp
:= marg0Op
+marg1Op
;
505 pad
:= nmax(0, me
.padding
[maindir
]);
506 // calc required space, count flexes
507 for lc
in forVisibleChildren(boxidx
) do
509 if (lc
.tempFlex
> 0) then flexTotal
+= lc
.tempFlex
;
510 spaceLeft
+= nmax(0, lc
.desiredsize
[maindir
]);
511 // insert padding if both current and previous children allow padding
512 dopad
:= (not lc
.noPad
);
513 if (prevpad
) and (dopad
) then spaceLeft
+= pad
;
515 maxdim
:= nmax(maxdim
, lc
.desiredsize
[suppdir
]);
518 spaceLeft
+= margtotal
;
519 maxdim
+= margtotalOp
;
521 me
.desiredsize
[maindir
] := nmax(spaceLeft
, me
.desiredsize
[maindir
]);
522 me
.desiredsize
[suppdir
] := nmax(maxdim
, me
.desiredsize
[suppdir
]);
523 // calculate free space
524 spaceLeft
:= me
.desiredsize
[maindir
]-spaceLeft
;
525 // distribute children
529 for lc
in forVisibleChildren(boxidx
) do
531 osz
:= lc
.desiredsize
;
533 // insert padding if both current and previous children allow padding
534 dopad
:= (not lc
.noPad
);
535 if (prevpad
) and (dopad
) then curpos
+= pad
;
537 lc
.desiredpos
[maindir
] := curpos
;
538 if (lc
.desiredsize
[maindir
] < 0) then lc
.desiredsize
[maindir
] := 0;
539 curpos
+= lc
.desiredsize
[maindir
];
541 //writeln(':lcidx=', lc.myidx, '; tempFlex=', lc.tempFlex, '; spaceLeft=', spaceLeft);
542 if (spaceLeft
> 0) and (lc
.tempFlex
> 0) then
544 toadd
:= trunc(spaceLeft
*lc
.tempFlex
/flexTotal
+0.5);
548 // compensate (crudely) rounding errors
549 if (curpos
+toadd
> me
.desiredsize
[maindir
]-margtotal
) then toadd
-= 1;
550 //writeln('***curpos=', curpos, '; toadd=', toadd, '; spaceLeft=', spaceLeft);
552 lc
.desiredsize
[maindir
] := lc
.desiredsize
[maindir
]+toadd
;
556 // secondary direction: expand or align
557 if (lc
.desiredsize
[suppdir
] < 0) then lc
.desiredsize
[suppdir
] := 0;
558 lc
.desiredpos
[suppdir
] := marg0Op
; // left/top align
559 if (lc
.expand
) then lc
.desiredsize
[suppdir
] := me
.desiredsize
[suppdir
]-margtotalOp
// expand
560 else if (lc
.aligndir
> 0) then lc
.desiredpos
[suppdir
] := me
.desiredsize
[suppdir
]-marg1Op
-lc
.desiredsize
[suppdir
] // right/bottom align
561 else if (lc
.aligndir
= 0) then lc
.desiredpos
[suppdir
] := (me
.desiredsize
[suppdir
]-lc
.desiredsize
[suppdir
]) div 2; // center
562 lc
.desiredsize
[suppdir
] := nmax(lc
.desiredsize
[suppdir
], osz
[suppdir
]);
563 // relayout children if size was changed
564 if (not osz
.equals(lc
.desiredsize
)) then
566 if (lc
.inGroup(0)) or (lc
.inGroup(1)) then groupElementChanged
:= true;
573 // do box layouting; call `layBox()` recursively if necessary
574 procedure TFlexLayouterBase
.layBox (boxidx
: LayControlIdx
);
580 if (boxidx
< 0) or (boxidx
>= Length(ctlist
)) then exit
;
581 me
:= @ctlist
[boxidx
];
582 // if we have no children, there's nothing to do
583 if (me
.firstChild
<> -1) then
587 osz
:= me
.desiredsize
;
588 // layout all children
589 for lc
in forVisibleChildren(boxidx
) do layBox(lc
.myidx
);
590 // distribute children
591 if (me
.horizBox
) then distributeChildren(me
.myidx
, 0) else distributeChildren(me
.myidx
, 1);
592 // relayout children if size was changed
593 if (osz
.equals(me
.desiredsize
)) then break
;
594 if (me
.inGroup(0)) or (me
.inGroup(1)) then groupElementChanged
:= true;
600 procedure TFlexLayouterBase
.secondPass ();
602 secondAgain
: Boolean;
610 loopsLeft
: Integer = 64;
612 while (loopsLeft
> 0) do
616 secondAgain
:= false;
617 if (groupElementChanged
) then
620 groupElementChanged
:= false;
622 for gtype
:= 0 to 1 do
624 for f
:= 0 to High(groups
[gtype
]) do
626 grp
:= @groups
[gtype
][f
];
628 for c
:= 0 to High(grp
.ctls
) do
632 maxsz
:= nmax(maxsz
, ct
.desiredsize
[gtype
]);
634 for c
:= 0 to High(grp
.ctls
) do
638 ct
.desiredsize
[gtype
] := maxsz
;
643 // don't change group control sizes anymore
644 for f
:= 0 to High(ctlist
) do
647 if (ct
.parent
<> -1) then
649 if (ctlist
[ct
.parent
].horizBox
) then maindir
:= 0 else maindir
:= 1;
653 maindir
:= 0; // arbitrary
655 if (ct
.inGroup(maindir
)) then ct
.tempFlex
:= 0; // don't change control size anymore
656 if (ct
.inGroup(1-maindir
)) then ct
.expand
:= false; // don't expand grouped controls anymore
658 if (not secondAgain
) then break
;
663 procedure TFlexLayouterBase
.thirdPass ();
667 for f
:= 0 to High(ctlist
) do
669 ctlist
[f
].ctl
.setActualSizePos(ctlist
[f
].desiredpos
, ctlist
[f
].desiredsize
);
674 procedure TFlexLayouterBase
.layout ();
676 if (Length(ctlist
) = 0) then exit
;
677 ctlist
[0].desiredpos
:= TLayPos
.Create(0, 0);
679 //writeln('============== AFTER FIRST PASS =============='); dump();
681 //writeln('============== AFTER SECOND PASS =============='); dump();
686 procedure TFlexLayouterBase
.dumpFlat ();
692 for f
:= 0 to High(ctlist
) do
695 ds
:= lc
.ctl
.getDefSize
;
696 ms
:= lc
.ctl
.getMaxSize
;
697 writeln(lc
.myidx
, ': startsize:', lc
.startsize
.toString(), '; desiredsize=', lc
.desiredsize
.toString(), '; maxsize=', lc
.maxsize
.toString(), '; tempFlex=', lc
.tempFlex
, '; flags=', lc
.flags
,
698 '; parent=', lc
.parent
, '; next=', lc
.nextSibling
, '; child=', lc
.firstChild
, '; ctl.size=', ds
.toString(), '; ctl.maxsize=', ms
.toString());
703 procedure TFlexLayouterBase
.dumpList (cidx
: LayControlIdx
; indent
: Integer);
711 for f
:= 0 to indent
do write(' ');
712 if (not lc
.visible
) then write('!');
713 write(lc
.myidx
, ': ');
714 if (lc
.ctl
.id
<> '') then write('<', lc
.ctl
.className
, '> {', lc
.ctl
.id
, '} ') else write('<', lc
.ctl
.className
, '> ');
715 writeln('startsize:', lc
.startsize
.toString
, '; desiredsize=', lc
.desiredsize
.toString
, '; maxsize=', lc
.maxsize
.toString
, '; tempFlex=', lc
.tempFlex
, '; despos=', lc
.desiredpos
.toString
);
716 dumpList(lc
.firstChild
, indent
+2);
717 cidx
:= lc
.nextSibling
;
722 procedure TFlexLayouterBase
.dump ();