DEADSOFTWARE

Holmes UI: lot of flexbox layouting code fixes
authorKetmar Dark <ketmar@ketmar.no-ip.org>
Sun, 24 Sep 2017 13:35:47 +0000 (16:35 +0300)
committerKetmar Dark <ketmar@ketmar.no-ip.org>
Sun, 24 Sep 2017 13:38:01 +0000 (16:38 +0300)
src/gx/gh_flexlay.pas
src/gx/gh_ui.pas
src/gx/gh_ui_common.pas
src/shared/xparser.pas

index cc01bb69fa685eb0750cfe1bffcf289513763a55..3eed2e410e14dd7b1f8c94026aabc540518be32c 100644 (file)
@@ -5,13 +5,13 @@ unit gh_flexlay;
 first pass:
   set all 'temp-flex' flags for controls to 'flex'
   reset all 'laywrap' flags for controls
-  build group arrays; for each group: find max size for group, adjust 'wantsize' controls to group max size
+  build group arrays; for each group: find max size for group, adjust 'startsize' controls to group max size
   call 'calc max size' for top-level control
   flags set:
     'firsttime'
 
 second pass:
-  calcluate desired sizes (process flexes) using 'wantsize', set 'desiredsize' and 'desiredpos'
+  calcluate desired sizes (process flexes) using 'startsize', set 'desiredsize' and 'desiredpos'
     if control has children, call 'second pass' recursively with this control as parent
   flags set:
     'group-element-changed', if any group element size was changed
@@ -19,8 +19,8 @@ second pass:
 
 third pass:
   if 'group-element-changed':
-    for each group: adjust controls to max desired size (wantsize), set 'temp-flex' flags to 0 for 'em, set 'second-again' flag
-  for other controls: if 'desiredsize' > 'maxsize', set 'wantsize' to 'maxsize', set 'temp-flex' flag to 0, set 'second-again' flag
+    for each group: adjust controls to max desired size (startsize), set 'temp-flex' flags to 0 for 'em, set 'second-again' flag
+  for other controls: if 'desiredsize' > 'maxsize', set 'startsize' to 'maxsize', set 'temp-flex' flag to 0, set 'second-again' flag
   if 'second-again' or 'wrapping-changed':
     reset 'second-again'
     reset 'wrapping-changed'
@@ -32,17 +32,17 @@ fourth pass:
   return
 
 calc max size:
-  set 'wantsize' to max(size, maxsize, 0)
+  set 'startsize' to max(size, maxsize, 0)
   if 'size' is negative:
     set 'temp-flex' flag to 0
   if has children:
     call 'calc max size' for each child
-    set 'desiredmax' to 'wantsize'
+    set 'desiredmax' to 'startsize'
     do lines, don't distribute space (i.e. calc only wrapping),
       for each complete line, set 'desiredmax' to max(desiredmax, linesize)
     if 'maxsize' >= 0:
       set 'desiredmax' to min(desiredmax, maxsize)
-    set 'wantsize' to 'desiredmax'
+    set 'startsize' to 'desiredmax'
   return
 
 
@@ -62,8 +62,12 @@ for wrapping:
 
 
 (*
+  control default size will be increased by margins
+  negative margins are ignored
 ControlT:
+  procedure layPrepare (); // called before registering control in layouter
   function getDefSize (): TLaySize; // default size; <0: use max size
+  function getMargins (): TLayMargins;
   function getMaxSize (): TLaySize; // max size; <0: set to some huge value
   function getFlex (): Integer; // <=0: not flexible
   function isHorizBox (): Boolean; // horizontal layout for children?
@@ -114,7 +118,10 @@ type
         tempFlex: Integer;
         flags: LongWord; // see below
         aligndir: Integer;
-        wantsize, desiredsize, maxsize: TLaySize;
+        startsize: TLaySize; // current
+        desiredsize: TLaySize;
+        maxsize: TLaySize;
+        margins: TLayMargins; // can never be negative
         desiredpos: TLayPos;
         ctl: ControlT;
         parent: LayControlIdx; // = -1;
@@ -327,8 +334,10 @@ begin
   assert(ctlist[parent].firstChild = -1);
   while (child <> nil) do
   begin
+    child.layPrepare;
     SetLength(ctlist, Length(ctlist)+1);
     lc := @ctlist[High(ctlist)];
+    lc.initialize();
     if (cidx = -1) then
     begin
       cidx := LayControlIdx(High(ctlist));
@@ -396,8 +405,10 @@ procedure TFlexLayouterBase.setup (root: ControlT);
 begin
   clear();
   if (root = nil) then exit;
+  root.layPrepare;
   try
     SetLength(ctlist, 1);
+    ctlist[0].initialize();
     ctlist[0].myidx := 0;
     ctlist[0].ctl := root;
     fixFlags(0);
@@ -416,7 +427,7 @@ procedure TFlexLayouterBase.calcMaxSizeInternal (cidx: LayControlIdx);
 var
   lc, c: PLayControl;
   msz: TLaySize;
-  negw{, negh}: Boolean;
+  negw, negh: Boolean;
   curwdt, curhgt, totalhgt: Integer;
   doWrap: Boolean;
 begin
@@ -424,14 +435,8 @@ begin
 
   lc := @ctlist[cidx];
   msz := lc.ctl.getMaxSize;
-  //lc.wantsize := lc.ctl.getDefSize;
-  negw := (lc.wantsize.w <= 0);
-  //negh := (lc.wantsize.h <= 0);
-
-  //if (lc.wantsize.w < msz.w) lc.wantsize.w := msz.w;
-  //if (lc.wantsize.h < msz.h) lc.wantsize.h := msz.h;
-
-  //writeln('calcsize #', cidx, '; wantsize=', lc.wantsize, '; ctl.maxsize=', msz);
+  negw := (lc.startsize.w <= 0);
+  negh := (lc.startsize.h <= 0);
 
   lc.tempFlex := lc.ctl.getFlex;
 
@@ -441,45 +446,46 @@ begin
   begin
     // horizontal boxes
     if (negw) then lc.tempFlex := 0; // size is negative: don't expand
-    curwdt := 0;
-    curhgt := 0;
+    curwdt := lc.margins.horiz;
+    curhgt := lc.margins.vert;
     totalhgt := 0;
     for c in forChildren(cidx) do
     begin
       // new line?
       doWrap := (not c.firstInLine) and (c.lineStart);
       // need to wrap?
-      if (not doWrap) and (lc.canWrap) and (c.canWrap) and (msz.w > 0) and (curwdt+c.wantsize.w > lc.wantsize.w) then doWrap := true;
+      if (not doWrap) and (lc.canWrap) and (c.canWrap) and (msz.w > 0) and (curwdt+c.startsize.w > lc.startsize.w) then doWrap := true;
       if (doWrap) then
       begin
         totalhgt += curhgt;
-        if (lc.wantsize.w < curwdt) then lc.wantsize.w := curwdt;
+        if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
         curwdt := 0;
         curhgt := 0;
       end;
-      curwdt += c.wantsize.w;
-      if (curhgt < c.wantsize.h) then curhgt := c.wantsize.h;
+      curwdt += c.startsize.w;
+      if (curhgt < c.startsize.h) then curhgt := c.startsize.h;
     end;
     totalhgt += curhgt;
-    if (lc.wantsize.w < curwdt) then lc.wantsize.w := curwdt;
-    if (lc.wantsize.h < totalhgt) then lc.wantsize.h := totalhgt;
+    if (lc.startsize.w < curwdt) then lc.startsize.w := curwdt;
+    if (lc.startsize.h < totalhgt) then lc.startsize.h := totalhgt;
   end
   else
   begin
     // vertical boxes
-    curhgt := 0;
+    if (negh) then lc.tempFlex := 0; // size is negative: don't expand
+    curhgt := lc.margins.vert;
     for c in forChildren(cidx) do
     begin
-      if (lc.wantsize.w < c.wantsize.w) then lc.wantsize.w := c.wantsize.w;
-      curhgt += c.wantsize.h;
+      if (lc.startsize.w < c.startsize.w+lc.margins.horiz) then lc.startsize.w := c.startsize.w+lc.margins.horiz;
+      curhgt += c.startsize.h;
     end;
-    if (lc.wantsize.h < curhgt) then lc.wantsize.h := curhgt;
+    if (lc.startsize.h < curhgt) then lc.startsize.h := curhgt;
   end;
-  if (lc.wantsize.w < 1) then lc.wantsize.w := 1;
-  if (lc.wantsize.h < 1) then lc.wantsize.h := 1;
+  if (lc.startsize.w < 0) then lc.startsize.w := 0;
+  if (lc.startsize.h < 0) then lc.startsize.h := 0;
   lc.maxsize := msz;
-  if (lc.maxsize.w < lc.wantsize.w) then lc.maxsize.w := lc.wantsize.w;
-  if (lc.maxsize.h < lc.wantsize.h) then lc.maxsize.h := lc.wantsize.h;
+  if (lc.maxsize.w < lc.startsize.w) then lc.maxsize.w := lc.startsize.w;
+  if (lc.maxsize.h < lc.startsize.h) then lc.maxsize.h := lc.startsize.h;
 end;
 
 
@@ -491,16 +497,21 @@ var
   grp: PLayGroup;
   maxsz: Integer;
   cidx: LayControlIdx;
+  mr: TLayMargins;
 begin
-  // reset all 'laywrap' flags for controls, set initial 'wantsize'
+  // reset all 'laywrap' flags for controls, set initial 'startsize'
   for f := 0 to High(ctlist) do
   begin
     ctlist[f].didWrap := false;
-    ctlist[f].wantsize := ctlist[f].ctl.getDefSize;
+    ctlist[f].startsize := ctlist[f].ctl.getDefSize;
+    mr := ctlist[f].ctl.getMargins;
+    ctlist[f].margins := mr;
+    ctlist[f].startsize.w += mr.horiz;
+    ctlist[f].startsize.h += mr.vert;
   end;
   // setup sizes
   calcMaxSizeInternal(0); // this also sets `tempFlex`
-  // find max size for group, adjust 'wantsize' controls to group max size
+  // find max size for group, adjust 'startsize' controls to group max size
   needRecalcMaxSize := false;
   for gtype := 0 to 1 do
   begin
@@ -511,23 +522,27 @@ begin
       for c := 0 to High(grp.ctls) do
       begin
         cidx := grp.ctls[c];
-        if (maxsz < ctlist[cidx].wantsize[gtype]) then maxsz := ctlist[cidx].wantsize[gtype];
+        if (maxsz < ctlist[cidx].startsize[gtype]) then maxsz := ctlist[cidx].startsize[gtype];
       end;
       for c := 0 to High(grp.ctls) do
       begin
         cidx := grp.ctls[c];
-        if (maxsz <> ctlist[cidx].wantsize[gtype]) then
+        if (maxsz <> ctlist[cidx].startsize[gtype]) then
         begin
           needRecalcMaxSize := true;
-          ctlist[cidx].wantsize[gtype] := maxsz;
+          ctlist[cidx].startsize[gtype] := maxsz;
         end;
       end;
     end;
   end;
   // recalc maxsize if necessary
   if (needRecalcMaxSize) then calcMaxSizeInternal(0);
+  // set "desired size" to "start size"
+  for f := 0 to High(ctlist) do ctlist[f].desiredsize := ctlist[f].startsize;
   // set flags
   firstTime := true;
+  //writeln('=== calculated max size ===');
+  //dump();
 end;
 
 
@@ -537,13 +552,26 @@ var
   lc: PLayControl;
   osz: TLaySize;
   toadd: Integer;
+  sti0: Integer;
+  lineh: Integer;
 begin
-  curx := 0;
+  curx := me.margins.left;
+  sti0 := i0;
+  // calc minimal line height
+  lineh := 0;
+  while (i0 <> i1) do
+  begin
+    lc := @ctlist[i0];
+    lineh := nmax(lineh, lc.startsize.h);
+    i0 := lc.nextSibling;
+  end;
+  // distribute space, expand/align
+  i0 := sti0;
   while (i0 <> i1) do
   begin
     lc := @ctlist[i0];
     osz := lc.desiredsize;
-    lc.desiredsize := lc.wantsize;
+    lc.desiredsize := lc.startsize;
     lc.desiredpos.x := curx;
     lc.desiredpos.y := cury;
     curx += lc.desiredsize.w;
@@ -557,17 +585,26 @@ begin
         lc.desiredsize.w += toadd;
         curx += toadd;
         // compensate (crudely) rounding errors
-        if (curx > me.desiredsize.w) then begin lc.desiredsize.w -= 1; curx -= 1; end;
+        if (curx > me.desiredsize.w-me.margins.horiz) then begin lc.desiredsize.w -= 1; curx -= 1; end;
         // relayout children
         layBox(lc.firstChild);
       end;
     end;
-    if (lc.inGroup) and (not lc.desiredsize.equals(osz)) then groupElementChanged := true;
+    // expand or align
+         if (lc.expand) then lc.desiredsize.h := nmin(lc.maxsize.h, lineh) // expand
+    else if (lc.aligndir > 0) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) // bottom align
+    else if (lc.aligndir = 0) then lc.desiredpos.y := cury+(lineh-lc.desiredsize.h) div 2; // center
+    if (not osz.equals(lc.desiredsize)) then
+    begin
+      if (lc.inGroup) then groupElementChanged := true;
+      // relayout children
+      layBox(lc.firstChild);
+    end;
     i0 := lc.nextSibling;
   end;
   flexTotal := 0;
   flexBoxCount := 0;
-  spaceLeft := me.wantsize.w;
+  spaceLeft := me.desiredsize.w-me.margins.horiz;
 end;
 
 
@@ -584,22 +621,22 @@ var
   lc: PLayControl;
   doWrap: Boolean;
   toadd: Integer;
+  osz: TLaySize;
 begin
   if (boxidx < 0) or (boxidx >= Length(ctlist)) then exit;
   me := @ctlist[boxidx];
 
-  // if we have no children, just set desired size and exit
-  me.desiredsize := me.wantsize;
+  // if we have no children, there's nothing to do
   if (me.firstChild = -1) then exit;
 
-  // first, layout all children; also, gather some flex data
+  // first, layout all children
   for lc in forChildren(boxidx) do layBox(lc.myidx);
 
   // second, layout lines, distribute flex data
   if (me.horizBox) then
   begin
     // horizontal boxes
-    cury := 0;
+    cury := me.margins.top;
     maxhgt := 0;
 
     fixLine(me, -1, -1, cury, spaceLeft, flexTotal, flexBoxCount); //HACK!
@@ -649,7 +686,7 @@ begin
     maxwdt := 0;
     flexTotal := 0;
     flexBoxCount := 0;
-    spaceLeft := me.wantsize.h;
+    spaceLeft := me.desiredsize.h-me.margins.vert;
 
     // calc flex
     for lc in forChildren(boxidx) do
@@ -664,11 +701,13 @@ begin
     end;
 
     // distribute space
-    cury := 0;
+    cury := me.margins.top;
+    //writeln('me: ', boxidx, '; margins: ', me.margins.toString);
     for lc in forChildren(boxidx) do
     begin
-      lc.desiredsize := lc.wantsize;
-      lc.desiredpos.x := 0;
+      osz := lc.desiredsize;
+      lc.desiredsize := lc.startsize;
+      lc.desiredpos.x := me.margins.left;
       lc.desiredpos.y := cury;
       cury += lc.desiredsize.h;
       // fix flexbox size
@@ -681,11 +720,19 @@ begin
           lc.desiredsize.h += toadd;
           cury += toadd;
           // compensate (crudely) rounding errors
-          if (cury > me.desiredsize.h) then begin lc.desiredsize.h -= 1; cury -= 1; end;
-          // relayout children
-          layBox(lc.firstChild);
+          if (cury > me.desiredsize.h-me.margins.vert) then begin lc.desiredsize.h -= 1; cury -= 1; end;
         end;
       end;
+      // expand or align
+           if (lc.expand) then lc.desiredsize.w := nmin(lc.maxsize.w, me.desiredsize.w-me.margins.vert) // expand
+      else if (lc.aligndir > 0) then lc.desiredpos.x := me.desiredsize.w-me.margins.right-lc.desiredsize.w // right align
+      else if (lc.aligndir = 0) then lc.desiredpos.x := (me.desiredsize.w-me.margins.horiz-lc.desiredsize.w) div 2; // center
+      if (not osz.equals(lc.desiredsize)) then
+      begin
+        if (lc.inGroup) then groupElementChanged := true;
+        // relayout children
+        layBox(lc.firstChild);
+      end;
     end;
   end;
 end;
@@ -693,7 +740,7 @@ end;
 
 (*
 second pass:
-  calcluate desired sizes (process flexes) using 'wantsize', set 'desiredsize' and 'desiredpos'
+  calcluate desired sizes (process flexes) using 'startsize', set 'desiredsize' and 'desiredpos'
     if control has children, call 'second pass' recursively with this control as parent
   flags set:
     'group-element-changed', if any group element size was changed
@@ -719,8 +766,8 @@ end;
 (*
 third pass:
   if 'group-element-changed':
-    for each group: adjust controls to max desired size (wantsize), set 'temp-flex' flags to 0 for 'em, set 'second-again' flag
-  for other controls: if 'desiredsize' > 'maxsize', set 'wantsize' to 'maxsize', set 'temp-flex' flag to 0, set 'second-again' flag
+    for each group: adjust controls to max desired size (startsize), set 'temp-flex' flags to 0 for 'em, set 'second-again' flag
+  for other controls: if 'desiredsize' > 'maxsize', set 'startsize' to 'maxsize', set 'temp-flex' flag to 0, set 'second-again' flag
   if 'second-again' or 'wrapping-changed':
     reset 'second-again'
     reset 'wrapping-changed'
@@ -730,17 +777,61 @@ third pass:
 procedure TFlexLayouterBase.thirdPass ();
 var
   secondAgain: Boolean;
+  gtype: Integer;
+  maxsz: Integer;
+  grp: PLayGroup;
+  f, c: Integer;
+  cidx: LayControlIdx;
 begin
   while true do
   begin
+    secondPass();
     secondAgain := false;
     if (groupElementChanged) then
     begin
-      // do it
+      secondAgain := true;
+      // find max size for group, adjust 'startsize' controls to group max size
+      for gtype := 0 to 1 do
+      begin
+        for f := 0 to High(groups[gtype]) do
+        begin
+          grp := @groups[gtype][f];
+          maxsz := 0;
+          for c := 0 to High(grp.ctls) do
+          begin
+            cidx := grp.ctls[c];
+            if (maxsz < ctlist[cidx].startsize[gtype]) then maxsz := ctlist[cidx].startsize[gtype];
+          end;
+          for c := 0 to High(grp.ctls) do
+          begin
+            cidx := grp.ctls[c];
+            ctlist[cidx].startsize[gtype] := maxsz;
+            ctlist[cidx].desiredsize[gtype] := maxsz;
+            ctlist[cidx].tempFlex := 0; // don't change control size anymore
+          end;
+        end;
+      end;
+    end
+    else
+    begin
+      for f := 0 to High(ctlist) do
+      begin
+        for c := 0 to 1 do
+        begin
+          if (ctlist[f].maxsize[c] <= 0) then continue;
+          if (ctlist[f].desiredsize[c] > ctlist[f].maxsize[c]) then
+          begin
+            //writeln('ctl #', f, '; dimension #', c, ': desired=', ctlist[f].desiredsize[c], '; max=', ctlist[f].maxsize[c]);
+            ctlist[f].startsize[c] := ctlist[f].maxsize[c];
+            ctlist[f].desiredsize[c] := ctlist[f].maxsize[c];
+            ctlist[f].tempFlex := 0; // don't change control size anymore
+            secondAgain := true;
+          end;
+        end;
+      end;
     end;
     if (not secondAgain) and (not wrappingChanged) then break;
     firstTime := false;
-    secondPass();
   end;
 end;
 
@@ -781,7 +872,7 @@ begin
     lc := @ctlist[f];
     ds := lc.ctl.getDefSize;
     ms := lc.ctl.getMaxSize;
-    writeln(lc.myidx, ': wantsize:', lc.wantsize.toString(), '; desiredsize=', lc.desiredsize.toString(), '; maxsize=', lc.maxsize.toString(), '; tempFlex=', lc.tempFlex, '; flags=', lc.flags,
+    writeln(lc.myidx, ': startsize:', lc.startsize.toString(), '; desiredsize=', lc.desiredsize.toString(), '; maxsize=', lc.maxsize.toString(), '; tempFlex=', lc.tempFlex, '; flags=', lc.flags,
       '; parent=', lc.parent, '; next=', lc.nextSibling, '; child=', lc.firstChild, '; ctl.size=', ds.toString(), '; ctl.maxsize=', ms.toString());
   end;
 end;
@@ -795,8 +886,8 @@ begin
   while (cidx >= 0) do
   begin
     lc := @ctlist[cidx];
-    for f := 0 to High(indent) do write(' ');
-    writeln(lc.myidx, ': wantsize:', lc.wantsize.toString, '; desiredsize=', lc.desiredsize.toString, '; maxsize=', lc.maxsize.toString, '; tempFlex=', lc.tempFlex, '; despos=', lc.desiredpos.toString);
+    for f := 0 to indent do write(' ');
+    writeln(lc.myidx, ': startsize:', lc.startsize.toString, '; desiredsize=', lc.desiredsize.toString, '; maxsize=', lc.maxsize.toString, '; tempFlex=', lc.tempFlex, '; despos=', lc.desiredpos.toString);
     dumpList(lc.firstChild, indent+2);
     cidx := lc.nextSibling;
   end;
index 774795d13daeecdcb197f396d602cd24b285f71b..a8593adacd9a4111c9e29fe2d9ae5b162094f131 100644 (file)
@@ -14,6 +14,7 @@
  * along with this program.  If not, see <http://www.gnu.org/licenses/>.
  *)
 {$INCLUDE ../shared/a_modes.inc}
+{$M+}
 unit gh_ui;
 
 interface
@@ -22,11 +23,14 @@ uses
   SysUtils, Classes,
   GL, GLExt, SDL2,
   gh_ui_common,
-  sdlcarcass, glgfx;
+  sdlcarcass, glgfx,
+  xparser;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
 type
+  THControlClass = class of THControl;
+
   THControl = class
   public
     type TActionCB = procedure (me: THControl; uinfo: Integer);
@@ -82,8 +86,6 @@ type
   private
     mDefSize: TLaySize; // default size
     mMaxSize: TLaySize; // maximum size
-    mActSize: TLaySize; // actual (calculated) size
-    mActPos: TLayPos; // actual (calculated) position
     mFlex: Integer;
     mHoriz: Boolean;
     mCanWrap: Boolean;
@@ -92,13 +94,16 @@ type
     mVGroup: AnsiString;
     mAlign: Integer;
     mExpand: Boolean;
+    mLayDefSize: TLaySize;
+    mLayMaxSize: TLaySize;
 
   public
     // layouter interface
     function getDefSize (): TLaySize; inline; // default size; <0: use max size
-    procedure setDefSize (const sz: TLaySize); inline; // default size; <0: use max size
+    //procedure setDefSize (const sz: TLaySize); inline; // default size; <0: use max size
+    function getMargins (): TLayMargins; inline;
     function getMaxSize (): TLaySize; inline; // max size; <0: set to some huge value
-    procedure setMaxSize (const sz: TLaySize); inline; // max size; <0: set to some huge value
+    //procedure setMaxSize (const sz: TLaySize); inline; // max size; <0: set to some huge value
     function getFlex (): Integer; inline; // <=0: not flexible
     function isHorizBox (): Boolean; inline; // horizontal layout for children?
     procedure setHorizBox (v: Boolean); inline; // horizontal layout for children?
@@ -110,15 +115,19 @@ type
     procedure setAlign (v: Integer); inline; // aligning in non-main direction: <0: left/up; 0: center; >0: right/down
     function getExpand (): Boolean; inline; // expanding in non-main direction: `true` will ignore align and eat all available space
     procedure setExpand (v: Boolean); inline; // expanding in non-main direction: `true` will ignore align and eat all available space
-    procedure setActualSizePos (constref apos: TLayPos; constref asize: TLaySize); inline;
     function getHGroup (): AnsiString; inline; // empty: not grouped
     procedure setHGroup (const v: AnsiString); inline; // empty: not grouped
     function getVGroup (): AnsiString; inline; // empty: not grouped
     procedure setVGroup (const v: AnsiString); inline; // empty: not grouped
 
+    procedure setActualSizePos (constref apos: TLayPos; constref asize: TLaySize); inline;
+
+    procedure layPrepare (); virtual; // called before registering control in layouter
+
+  public
     property flex: Integer read mFlex write mFlex;
-    property flDefaultSize: TLaySize read getDefSize write setDefSize;
-    property flMaxSize: TLaySize read getMaxSize write setMaxSize;
+    property flDefaultSize: TLaySize read mDefSize write mDefSize;
+    property flMaxSize: TLaySize read mMaxSize write mMaxSize;
     property flHoriz: Boolean read isHorizBox write setHorizBox;
     property flCanWrap: Boolean read canWrap write setCanWrap;
     property flLineStart: Boolean read isLineStart write setLineStart;
@@ -126,10 +135,31 @@ type
     property flExpand: Boolean read getExpand write setExpand;
     property flHGroup: AnsiString read getHGroup write setHGroup;
     property flVGroup: AnsiString read getVGroup write setVGroup;
-    property flActualSize: TLaySize read mActSize write mActSize;
-    property flActualPos: TLayPos read mActPos write mActPos;
+
+  protected
+    function parsePos (par: TTextParser): TLayPos;
+    function parseSize (par: TTextParser): TLaySize;
+    function parseBool (par: TTextParser): Boolean;
+    function parseAnyAlign (par: TTextParser): Integer;
+    function parseHAlign (par: TTextParser): Integer;
+    function parseVAlign (par: TTextParser): Integer;
+    procedure parseTextAlign (par: TTextParser; var h, v: Integer);
+    procedure parseChildren (par: TTextParser); // par should be on '{'; final '}' is eaten
 
   public
+    // par is on property data
+    // there may be more data in text stream, don't eat it!
+    // return `true` if property name is valid and value was parsed
+    // return `false` if property name is invalid; don't advance parser in this case
+    // throw on property data errors
+    function parseProperty (const prname: AnsiString; par: TTextParser): Boolean; virtual;
+
+    // par should be on '{'; final '}' is eaten
+    procedure parseProperties (par: TTextParser);
+
+  public
+    constructor Create ();
+    constructor Create (aparent: THControl);
     constructor Create (ax, ay, aw, ah: Integer; aparent: THControl=nil);
     destructor Destroy (); override;
 
@@ -180,6 +210,7 @@ type
     mDragStartX, mDragStartY: Integer;
     mWaitingClose: Boolean;
     mInClose: Boolean;
+    mFreeOnClose: Boolean; // default: false
 
   protected
     procedure blurred (); override;
@@ -190,6 +221,8 @@ type
   public
     constructor Create (const atitle: AnsiString; ax, ay: Integer; aw: Integer=-1; ah: Integer=-1);
 
+    function parseProperty (const prname: AnsiString; par: TTextParser): Boolean; override;
+
     procedure centerInScreen ();
 
     // `sx` and `sy` are screen coordinates
@@ -198,6 +231,9 @@ type
 
     function keyEvent (var ev: THKeyEvent): Boolean; override; // returns `true` if event was eaten
     function mouseEvent (var ev: THMouseEvent): Boolean; override; // returns `true` if event was eaten
+
+  public
+    property freeOnClose: Boolean read mFreeOnClose write mFreeOnClose;
   end;
 
 
@@ -251,20 +287,72 @@ type
     function keyEvent (var ev: THKeyEvent): Boolean; override;
   end;
 
+  // ////////////////////////////////////////////////////////////////////// //
+  THCtlBox = class(THControl)
+  private
+    mHasFrame: Boolean;
+    mCaption: AnsiString;
+
+  public
+    constructor Create (ahoriz: Boolean; aparent: THControl=nil);
+    //destructor Destroy (); override;
+
+    function parseProperty (const prname: AnsiString; par: TTextParser): Boolean; override;
+
+    procedure drawControl (sx, sy: Integer); override;
+
+    function mouseEvent (var ev: THMouseEvent): Boolean; override;
+    function keyEvent (var ev: THKeyEvent): Boolean; override;
+  end;
+
+  THCtlHBox = class(THCtlBox)
+  public
+    constructor Create (aparent: THControl=nil);
+  end;
+
+  THCtlVBox = class(THCtlBox)
+  public
+    constructor Create (aparent: THControl=nil);
+  end;
+
+
+  THCtlTextLabel = class(THControl)
+  private
+    mText: AnsiString;
+    mHAlign: Integer; // -1: left; 0: center; 1: right; default: left
+    mVAlign: Integer; // -1: top; 0: center; 1: bottom; default: center
+
+  public
+    constructor Create (const atext: AnsiString; aparent: THControl=nil);
+    //destructor Destroy (); override;
+
+    function parseProperty (const prname: AnsiString; par: TTextParser): Boolean; override;
+
+    procedure drawControl (sx, sy: Integer); override;
 
+    function mouseEvent (var ev: THMouseEvent): Boolean; override;
+    function keyEvent (var ev: THKeyEvent): Boolean; override;
+  end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
 function uiMouseEvent (ev: THMouseEvent): Boolean;
 function uiKeyEvent (ev: THKeyEvent): Boolean;
 procedure uiDraw ();
 
+
+// ////////////////////////////////////////////////////////////////////////// //
 procedure uiAddWindow (ctl: THControl);
-procedure uiRemoveWindow (ctl: THControl);
+procedure uiRemoveWindow (ctl: THControl); // will free window if `mFreeOnClose` is `true`
 function uiVisibleWindow (ctl: THControl): Boolean;
 
 
+// ////////////////////////////////////////////////////////////////////////// //
 // do layouting
 procedure uiLayoutCtl (ctl: THControl);
 
 
+// ////////////////////////////////////////////////////////////////////////// //
 var
   gh_ui_scale: Single = 1.0;
 
@@ -272,7 +360,42 @@ var
 implementation
 
 uses
-  gh_flexlay;
+  gh_flexlay,
+  utils;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+var
+  knownCtlClasses: array of record
+    klass: THControlClass;
+    name: AnsiString;
+  end = nil;
+
+
+procedure registerCtlClass (aklass: THControlClass; const aname: AnsiString);
+begin
+  assert(aklass <> nil);
+  assert(Length(aname) > 0);
+  SetLength(knownCtlClasses, Length(knownCtlClasses)+1);
+  knownCtlClasses[High(knownCtlClasses)].klass := aklass;
+  knownCtlClasses[High(knownCtlClasses)].name := aname;
+end;
+
+
+function findCtlClass (const aname: AnsiString): THControlClass;
+var
+  f: Integer;
+begin
+  for f := 0 to High(knownCtlClasses) do
+  begin
+    if (strEquCI1251(aname, knownCtlClasses[f].name)) then
+    begin
+      result := knownCtlClasses[f].klass;
+      exit;
+    end;
+  end;
+  result := nil;
+end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
@@ -287,7 +410,29 @@ begin
   lay := TFlexLayouter.Create();
   try
     lay.setup(ctl);
+    //lay.layout();
+
+    writeln('============================');
+    lay.dumpFlat();
+
+    writeln('=== initial ===');
+    lay.dump();
+
+    //lay.calcMaxSizeInternal(0);
+    {
+    lay.firstPass();
+    writeln('=== after first pass ===');
+    lay.dump();
+
+    lay.secondPass();
+    writeln('=== after second pass ===');
+    lay.dump();
+    }
+
     lay.layout();
+    writeln('=== final ===');
+    lay.dump();
+
   finally
     FreeAndNil(lay);
   end;
@@ -374,6 +519,7 @@ var
 begin
   if (ctl = nil) then exit;
   ctl := ctl.topLevel;
+  if not (ctl is THTopWindow) then exit; // alas
   for f := 0 to High(uiTopList) do
   begin
     if (uiTopList[f] = ctl) then
@@ -395,13 +541,13 @@ begin
 end;
 
 
-// won't free object
 procedure uiRemoveWindow (ctl: THControl);
 var
   f, c: Integer;
 begin
   if (ctl = nil) then exit;
   ctl := ctl.topLevel;
+  if not (ctl is THTopWindow) then exit; // alas
   for f := 0 to High(uiTopList) do
   begin
     if (uiTopList[f] = ctl) then
@@ -411,7 +557,11 @@ begin
       SetLength(uiTopList, Length(uiTopList)-1);
       if (ctl is THTopWindow) then
       begin
-        if assigned(THTopWindow(ctl).closeCB) then THTopWindow(ctl).closeCB(ctl, 0);
+        try
+          if assigned(THTopWindow(ctl).closeCB) then THTopWindow(ctl).closeCB(ctl, 0);
+        finally
+          if (THTopWindow(ctl).mFreeOnClose) then FreeAndNil(ctl);
+        end;
       end;
       exit;
     end;
@@ -426,6 +576,7 @@ begin
   result := false;
   if (ctl = nil) then exit;
   ctl := ctl.topLevel;
+  if not (ctl is THTopWindow) then exit; // alas
   for f := 0 to High(uiTopList) do
   begin
     if (uiTopList[f] = ctl) then begin result := true; exit; end;
@@ -434,13 +585,13 @@ end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
-constructor THControl.Create (ax, ay, aw, ah: Integer; aparent: THControl=nil);
+constructor THControl.Create ();
 begin
-  mParent := aparent;
-  mX := ax;
-  mY := ay;
-  mWidth := aw;
-  mHeight := ah;
+  mParent := nil;
+  mX := 0;
+  mY := 0;
+  mWidth := 64;
+  mHeight := 8;
   mFrameWidth := 0;
   mFrameHeight := 0;
   mEnabled := true;
@@ -454,10 +605,8 @@ begin
   mDrawShadow := false;
   actionCB := nil;
   // layouter interface
-  mDefSize := TLaySize.Create(64, 10); // default size
+  mDefSize := TLaySize.Create(64, 8); // default size
   mMaxSize := TLaySize.Create(-1, -1); // maximum size
-  mActSize := TLaySize.Create(0, 0); // actual (calculated) size
-  mActPos := TLayPos.Create(0, 0); // actual (calculated) position
   mFlex := 0;
   mHoriz := true;
   mCanWrap := false;
@@ -469,6 +618,23 @@ begin
 end;
 
 
+constructor THControl.Create (ax, ay, aw, ah: Integer; aparent: THControl=nil);
+begin
+  Create(aparent);
+  mX := ax;
+  mY := ay;
+  mWidth := aw;
+  mHeight := ah;
+end;
+
+
+constructor THControl.Create (aparent: THControl);
+begin
+  Create();
+  mParent := aparent;
+end;
+
+
 destructor THControl.Destroy ();
 var
   f, c: Integer;
@@ -494,10 +660,9 @@ begin
 end;
 
 
-function THControl.getDefSize (): TLaySize; inline; begin result := mDefSize; end;
-procedure THControl.setDefSize (const sz: TLaySize); inline; begin mDefSize := sz; end;
-function THControl.getMaxSize (): TLaySize; inline; begin result := mMaxSize; end;
-procedure THControl.setMaxSize (const sz: TLaySize); inline; begin mMaxSize := sz; end;
+// ////////////////////////////////////////////////////////////////////////// //
+function THControl.getDefSize (): TLaySize; inline; begin result := mLayDefSize; end;
+function THControl.getMaxSize (): TLaySize; inline; begin result := mLayMaxSize; end;
 function THControl.getFlex (): Integer; inline; begin result := mFlex; end;
 function THControl.isHorizBox (): Boolean; inline; begin result := mHoriz; end;
 procedure THControl.setHorizBox (v: Boolean); inline; begin mHoriz := v; end;
@@ -509,13 +674,233 @@ function THControl.getAlign (): Integer; inline; begin result := mAlign; end;
 procedure THControl.setAlign (v: Integer); inline; begin mAlign := v; end;
 function THControl.getExpand (): Boolean; inline; begin result := mExpand; end;
 procedure THControl.setExpand (v: Boolean); inline; begin mExpand := v; end;
-procedure THControl.setActualSizePos (constref apos: TLayPos; constref asize: TLaySize); inline; begin mActPos := apos; mActSize := asize; end;
 function THControl.getHGroup (): AnsiString; inline; begin result := mHGroup; end;
 procedure THControl.setHGroup (const v: AnsiString); inline; begin mHGroup := v; end;
 function THControl.getVGroup (): AnsiString; inline; begin result := mVGroup; end;
 procedure THControl.setVGroup (const v: AnsiString); inline; begin mVGroup := v; end;
 
+function THControl.getMargins (): TLayMargins; inline;
+begin
+  result := TLayMargins.Create(mFrameHeight, mFrameWidth, mFrameHeight, mFrameWidth);
+end;
+
+procedure THControl.setActualSizePos (constref apos: TLayPos; constref asize: TLaySize); inline; begin
+  if (mParent <> nil) then
+  begin
+    mX := apos.x;
+    mY := apos.y;
+  end;
+  mWidth := asize.w;
+  mHeight := asize.h;
+end;
+
+procedure THControl.layPrepare ();
+begin
+  mLayDefSize := mDefSize;
+  mLayMaxSize := mMaxSize;
+  if (mLayMaxSize.w >= 0) then mLayMaxSize.w += mFrameWidth*2;
+  if (mLayMaxSize.h >= 0) then mLayMaxSize.h += mFrameHeight*2;
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+function THControl.parsePos (par: TTextParser): TLayPos;
+var
+  ech: AnsiChar = ')';
+begin
+  if (par.eatDelim('[')) then ech := ']' else par.expectDelim('(');
+  result.x := par.expectInt();
+  par.eatDelim(','); // optional comma
+  result.y := par.expectInt();
+  par.eatDelim(','); // optional comma
+  par.expectDelim(ech);
+end;
+
+function THControl.parseSize (par: TTextParser): TLaySize;
+var
+  ech: AnsiChar = ')';
+begin
+  if (par.eatDelim('[')) then ech := ']' else par.expectDelim('(');
+  result.h := par.expectInt();
+  par.eatDelim(','); // optional comma
+  result.w := par.expectInt();
+  par.eatDelim(','); // optional comma
+  par.expectDelim(ech);
+end;
+
+function THControl.parseBool (par: TTextParser): Boolean;
+begin
+  result :=
+    par.eatIdOrStr('true', false) or
+    par.eatIdOrStr('yes', false) or
+    par.eatIdOrStr('tan', false);
+  if not result then
+  begin
+    if (not par.eatIdOrStr('false', false)) and (not par.eatIdOrStr('no', false)) and (not par.eatIdOrStr('ona', false)) then
+    begin
+      par.error('boolean value expected');
+    end;
+  end;
+end;
+
+function THControl.parseAnyAlign (par: TTextParser): Integer;
+begin
+       if (par.eatIdOrStr('left', false)) or (par.eatIdOrStr('top', false)) then result := -1
+  else if (par.eatIdOrStr('right', false)) or (par.eatIdOrStr('bottom', false)) then result := 1
+  else if (par.eatIdOrStr('center', false)) then result := 0
+  else par.error('invalid align value');
+end;
+
+function THControl.parseHAlign (par: TTextParser): Integer;
+begin
+       if (par.eatIdOrStr('left', false)) then result := -1
+  else if (par.eatIdOrStr('right', false)) then result := 1
+  else if (par.eatIdOrStr('center', false)) then result := 0
+  else par.error('invalid horizontal align value');
+end;
+
+function THControl.parseVAlign (par: TTextParser): Integer;
+begin
+       if (par.eatIdOrStr('top', false)) then result := -1
+  else if (par.eatIdOrStr('bottom', false)) then result := 1
+  else if (par.eatIdOrStr('center', false)) then result := 0
+  else par.error('invalid vertical align value');
+end;
+
+procedure THControl.parseTextAlign (par: TTextParser; var h, v: Integer);
+var
+  wasH: Boolean = false;
+  wasV: Boolean = false;
+begin
+  while true do
+  begin
+    if (par.eatIdOrStr('left', false)) then
+    begin
+      if wasH then par.error('too many align directives');
+      wasH := true;
+      h := -1;
+      continue;
+    end;
+    if (par.eatIdOrStr('right', false)) then
+    begin
+      if wasH then par.error('too many align directives');
+      wasH := true;
+      h := 1;
+      continue;
+    end;
+    if (par.eatIdOrStr('hcenter', false)) then
+    begin
+      if wasH then par.error('too many align directives');
+      wasH := true;
+      h := 0;
+      continue;
+    end;
+    if (par.eatIdOrStr('top', false)) then
+    begin
+      if wasV then par.error('too many align directives');
+      wasV := true;
+      v := -1;
+      continue;
+    end;
+    if (par.eatIdOrStr('bottom', false)) then
+    begin
+      if wasV then par.error('too many align directives');
+      wasV := true;
+      v := 1;
+      continue;
+    end;
+    if (par.eatIdOrStr('vcenter', false)) then
+    begin
+      if wasV then par.error('too many align directives');
+      wasV := true;
+      v := 0;
+      continue;
+    end;
+    if (par.eatIdOrStr('center', false)) then
+    begin
+      if wasV or wasH then par.error('too many align directives');
+      wasV := true;
+      wasH := true;
+      h := 0;
+      v := 0;
+      continue;
+    end;
+    break;
+  end;
+  if not wasV and not wasH then par.error('invalid align value');
+end;
+
+// par should be on '{'; final '}' is eaten
+procedure THControl.parseProperties (par: TTextParser);
+var
+  pn: AnsiString;
+begin
+  if (not par.eatDelim('{')) then exit;
+  while (not par.eatDelim('}')) do
+  begin
+    if (par.tokType <> par.TTId) and (par.tokType <> par.TTStr) then par.error('property name expected');
+    pn := par.tokStr;
+    par.skipToken();
+    par.eatDelim(':'); // optional
+    if not parseProperty(pn, par) then par.errorfmt('invalid property name ''%s''', [pn]);
+    par.eatDelim(','); // optional
+  end;
+end;
+
+// par should be on '{'
+procedure THControl.parseChildren (par: TTextParser);
+var
+  cc: THControlClass;
+  ctl: THControl;
+begin
+  par.expectDelim('{');
+  while (not par.eatDelim('}')) do
+  begin
+    if (par.tokType <> par.TTId) then par.error('control name expected');
+    cc := findCtlClass(par.tokStr);
+    if (cc = nil) then par.errorfmt('unknown control name: ''%s''', [par.tokStr]);
+    //writeln('children for <', par.tokStr, '>: <', cc.className, '>');
+    par.skipToken();
+    par.eatDelim(':'); // optional
+    ctl := cc.Create(nil);
+    try
+      ctl.parseProperties(par);
+    except
+      FreeAndNil(ctl);
+      raise;
+    end;
+    //writeln(': ', ctl.mDefSize.toString);
+    appendChild(ctl);
+    par.eatDelim(','); // optional
+  end;
+end;
+
+
+function THControl.parseProperty (const prname: AnsiString; par: TTextParser): Boolean;
+begin
+  result := true;
+  if (strEquCI1251(prname, 'flex')) then begin flex := par.expectInt(); exit; end;
+  // sizes
+  if (strEquCI1251(prname, 'defsize')) then begin mDefSize := parseSize(par); exit; end;
+  if (strEquCI1251(prname, 'maxsize')) then begin mMaxSize := parseSize(par); exit; end;
+  if (strEquCI1251(prname, 'wrap')) then begin mCanWrap := parseBool(par); exit; end;
+  if (strEquCI1251(prname, 'linestart')) then begin mLineStart := parseBool(par); exit; end;
+  if (strEquCI1251(prname, 'expand')) then begin mExpand := parseBool(par); exit; end;
+  // align
+  if (strEquCI1251(prname, 'align')) then begin mAlign := parseAnyAlign(par); exit; end;
+  if (strEquCI1251(prname, 'hgroup')) then begin mHGroup := par.expectStrOrId(true); exit; end; // allow empty strings
+  if (strEquCI1251(prname, 'vgroup')) then begin mVGroup := par.expectStrOrId(true); exit; end; // allow empty strings
+  // other
+  if (strEquCI1251(prname, 'canfocus')) then begin mCanFocus := parseBool(par); exit; end;
+  if (strEquCI1251(prname, 'enabled')) then begin mEnabled := parseBool(par); exit; end;
+  if (strEquCI1251(prname, 'disabled')) then begin mEnabled := not parseBool(par); exit; end;
+  if (strEquCI1251(prname, 'escclose')) then begin mEscClose := not parseBool(par); exit; end;
+  if (strEquCI1251(prname, 'eatkeys')) then begin mEatKeys := not parseBool(par); exit; end;
+  result := false;
+end;
+
 
+// ////////////////////////////////////////////////////////////////////////// //
 procedure THControl.activated ();
 begin
 end;
@@ -961,6 +1346,32 @@ begin
 end;
 
 
+function THTopWindow.parseProperty (const prname: AnsiString; par: TTextParser): Boolean;
+begin
+  if (strEquCI1251(prname, 'title')) or (strEquCI1251(prname, 'caption')) then
+  begin
+    mTitle := par.expectStrOrId(true);
+    result := true;
+    exit;
+  end;
+  if (strEquCI1251(prname, 'children')) then
+  begin
+    parseChildren(par);
+    result := true;
+    exit;
+  end;
+  if (strEquCI1251(prname, 'orientation')) or (strEquCI1251(prname, 'orient')) then
+  begin
+         if (par.eatIdOrStr('horizontal', false)) or (par.eatIdOrStr('horiz', false)) then mHoriz := true
+    else if (par.eatIdOrStr('vertical', false)) or (par.eatIdOrStr('vert', false)) then mHoriz := false
+    else par.error('`horizontal` or `vertical` expected');
+    result := true;
+    exit;
+  end;
+  result := inherited parseProperty(prname, par);
+end;
+
+
 procedure THTopWindow.centerInScreen ();
 begin
   if (mWidth > 0) and (mHeight > 0) then
@@ -1356,4 +1767,179 @@ begin
 end;
 
 
+// ////////////////////////////////////////////////////////////////////////// //
+constructor THCtlBox.Create (ahoriz: Boolean; aparent: THControl=nil);
+begin
+  inherited Create(aparent);
+  mHoriz := ahoriz;
+end;
+
+
+function THCtlBox.parseProperty (const prname: AnsiString; par: TTextParser): Boolean;
+begin
+  if (strEquCI1251(prname, 'orientation')) or (strEquCI1251(prname, 'orient')) then
+  begin
+         if (par.eatIdOrStr('horizontal', false)) or (par.eatIdOrStr('horiz', false)) then mHoriz := true
+    else if (par.eatIdOrStr('vertical', false)) or (par.eatIdOrStr('vert', false)) then mHoriz := false
+    else par.error('`horizontal` or `vertical` expected');
+    result := true;
+    exit;
+  end;
+  if (strEquCI1251(prname, 'frame')) then
+  begin
+    mHasFrame := parseBool(par);
+    if (mHasFrame) then begin mFrameWidth := 8; mFrameHeight := 8; end else begin mFrameWidth := 0; mFrameHeight := 0; end;
+    result := true;
+    exit;
+  end;
+  if (strEquCI1251(prname, 'title')) or (strEquCI1251(prname, 'caption')) then
+  begin
+    mCaption := par.expectStrOrId(true);
+    mDefSize := TLaySize.Create(Length(mCaption)*8+2+8*2, 8*2+2);
+    result := true;
+    exit;
+  end;
+  if (strEquCI1251(prname, 'children')) then
+  begin
+    parseChildren(par);
+    result := true;
+    exit;
+  end;
+  result := inherited parseProperty(prname, par);
+end;
+
+
+procedure THCtlBox.drawControl (sx, sy: Integer);
+var
+  r, g, b: Integer;
+  tx: Integer;
+begin
+  if focused then begin r := 255; g := 255; b := 255; end else begin r := 255; g := 255; b := 0; end;
+  if mHasFrame then
+  begin
+    // draw frame
+    drawRectUI(mX+3, mY+3, mWidth-6, mHeight-6, r, g, b);
+  end;
+  if (Length(mCaption) > 0) then
+  begin
+    setScissor(mFrameWidth+1, 0, mWidth-mFrameWidth-2, 8);
+    tx := mX+((mWidth-Length(mCaption)*8) div 2)-1;
+    if mHasFrame then fillRect(tx, mY, Length(mCaption)*8+2, 8, 0, 0, 128);
+    drawText8(tx+1, mY, mCaption, r, g, b);
+  end;
+end;
+
+function THCtlBox.mouseEvent (var ev: THMouseEvent): Boolean;
+var
+  lx, ly: Integer;
+begin
+  result := inherited mouseEvent(ev);
+  lx := ev.x;
+  ly := ev.y;
+  if not result and toLocal(lx, ly) then
+  begin
+    result := true;
+  end;
+end;
+
+
+//TODO: navigation with arrow keys, according to box orientation
+function THCtlBox.keyEvent (var ev: THKeyEvent): Boolean;
+begin
+  result := inherited keyEvent(ev);
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor THCtlHBox.Create (aparent: THControl=nil);
+begin
+  inherited Create(true, aparent);
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor THCtlVBox.Create (aparent: THControl=nil);
+begin
+  inherited Create(false, aparent);
+end;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor THCtlTextLabel.Create (const atext: AnsiString; aparent: THControl=nil);
+begin
+  inherited Create(aparent);
+  mHAlign := -1;
+  mVAlign := 0;
+  mText := atext;
+  mDefSize := TLaySize.Create(Length(atext)*8, 8);
+end;
+
+
+function THCtlTextLabel.parseProperty (const prname: AnsiString; par: TTextParser): Boolean;
+begin
+  if (strEquCI1251(prname, 'title')) or (strEquCI1251(prname, 'caption')) then
+  begin
+    mText := par.expectStrOrId(true);
+    mDefSize := TLaySize.Create(Length(mText)*8, 8);
+    result := true;
+    exit;
+  end;
+  if (strEquCI1251(prname, 'textalign')) then
+  begin
+    parseTextAlign(par, mHAlign, mVAlign);
+    result := true;
+    exit;
+  end;
+  result := inherited parseProperty(prname, par);
+end;
+
+
+procedure THCtlTextLabel.drawControl (sx, sy: Integer);
+var
+  xpos, ypos: Integer;
+begin
+  // debug
+  fillRect(sx, sy, mWidth, mHeight, 96, 96, 0);
+  drawRectUI(sx, sy, mWidth, mHeight, 96, 96, 96);
+
+  if (Length(mText) > 0) then
+  begin
+         if (mHAlign < 0) then xpos := 0
+    else if (mHAlign > 0) then xpos := mWidth-Length(mText)*8
+    else xpos := (mWidth-Length(mText)*8) div 2;
+
+         if (mVAlign < 0) then ypos := 0
+    else if (mVAlign > 0) then ypos := mHeight-8
+    else ypos := (mHeight-8) div 2;
+
+    drawText8(sx+xpos, sy+ypos, mText, 255, 255, 255);
+  end;
+end;
+
+
+function THCtlTextLabel.mouseEvent (var ev: THMouseEvent): Boolean;
+var
+  lx, ly: Integer;
+begin
+  result := inherited mouseEvent(ev);
+  lx := ev.x;
+  ly := ev.y;
+  if not result and toLocal(lx, ly) then
+  begin
+    result := true;
+  end;
+end;
+
+
+function THCtlTextLabel.keyEvent (var ev: THKeyEvent): Boolean;
+begin
+  result := inherited keyEvent(ev);
+end;
+
+
+initialization
+  registerCtlClass(THCtlBox, 'box');
+  registerCtlClass(THCtlHBox, 'hbox');
+  registerCtlClass(THCtlVBox, 'vbox');
+  registerCtlClass(THCtlTextLabel, 'label');
 end.
index 7143501e947dc2e78f7e8463a6aa380c59e92e93..0c1aa7596868bfbe5aee5b11704379f74be8ffbc 100644 (file)
@@ -58,6 +58,18 @@ type
     property item[idx: Integer]: Integer read getIdx write setIdx; default;
   end;
 
+  TLayMargins = record
+  public
+    top, right, bottom, left: Integer;
+
+  public
+    constructor Create (atop, aright, abottom, aleft: Integer);
+
+    function toString (): AnsiString;
+
+    function horiz (): Integer; inline;
+    function vert (): Integer; inline;
+  end;
 
 implementation
 
@@ -78,5 +90,20 @@ procedure TLayPos.setIdx (idx, v: Integer); inline; begin if (idx = 0) then x :=
 function TLayPos.toString (): AnsiString; begin result := formatstrf('(%d,%d)', [x, y]); end;
 function TLayPos.equals (constref a: TLayPos): Boolean; inline; begin result := (x = a.x) and (y = a.y); end;
 
+constructor TLayMargins.Create (atop, aright, abottom, aleft: Integer);
+begin
+  if (atop < 0) then atop := 0;
+  if (aright < 0) then aright := 0;
+  if (abottom < 0) then abottom := 0;
+  if (aleft < 0) then aleft := 0;
+  left := aleft;
+  right := aright;
+  top := atop;
+  bottom := abottom;
+end;
+function TLayMargins.toString (): AnsiString; begin result := formatstrf('(%s,%s,%s,%s)', [top, right, bottom, left]); end;
+function TLayMargins.horiz (): Integer; inline; begin result := left+right; end;
+function TLayMargins.vert (): Integer; inline; begin result := top+bottom; end;
+
 
 end.
index 7d7a9ed5d3bbdeb237376833203c68374a90986c..b985f1a30a781724b03a058c5b644621b9d0c851 100644 (file)
@@ -20,11 +20,22 @@ unit xparser;
 interface
 
 uses
-  Classes{$IFDEF USE_MEMPOOL}, mempool{$ENDIF};
+  SysUtils, Classes{$IFDEF USE_MEMPOOL}, mempool{$ENDIF};
 
 
 // ////////////////////////////////////////////////////////////////////////// //
 type
+  TTextParser = class;
+
+  TParserException = class(Exception)
+  public
+    tokLine, tokCol: Integer;
+
+  public
+    constructor Create (pr: TTextParser; const amsg: AnsiString);
+    constructor CreateFmt (pr: TTextParser; const afmt: AnsiString; const args: array of const);
+  end;
+
   TTextParser = class{$IFDEF USE_MEMPOOL}(TPoolObject){$ENDIF}
   public
     const
@@ -81,6 +92,9 @@ type
     constructor Create (aopts: TOptions=[TOption.SignedNumbers]);
     destructor Destroy (); override;
 
+    procedure error (const amsg: AnsiString); noreturn;
+    procedure errorfmt (const afmt: AnsiString; const args: array of const); noreturn;
+
     function isEOF (): Boolean; inline;
 
     function skipChar (): Boolean; // returns `false` on eof
@@ -93,8 +107,9 @@ type
     {$ENDIF}
 
     function expectId (): AnsiString;
-    procedure expectId (const aid: AnsiString);
-    function eatId (const aid: AnsiString): Boolean;
+    procedure expectId (const aid: AnsiString; caseSens: Boolean=true);
+    function eatId (const aid: AnsiString; caseSens: Boolean=true): Boolean;
+    function eatIdOrStr (const aid: AnsiString; caseSens: Boolean=true): Boolean;
 
     function expectStr (allowEmpty: Boolean=false): AnsiString;
     function expectInt (): Integer;
@@ -235,7 +250,21 @@ type
 implementation
 
 uses
-  SysUtils, utils;
+  utils;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+constructor TParserException.Create (pr: TTextParser; const amsg: AnsiString);
+begin
+  if (pr <> nil) then begin tokLine := pr.tokLine; tokCol := pr.tokCol; end;
+  inherited Create(amsg);
+end;
+
+constructor TParserException.CreateFmt (pr: TTextParser; const afmt: AnsiString; const args: array of const);
+begin
+  if (pr <> nil) then begin tokLine := pr.tokLine; tokCol := pr.tokCol; end;
+  inherited Create(formatstrf(afmt, args));
+end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
@@ -261,6 +290,18 @@ begin
 end;
 
 
+procedure TTextParser.error (const amsg: AnsiString); noreturn;
+begin
+  raise TParserException.Create(self, amsg);
+end;
+
+
+procedure TTextParser.errorfmt (const afmt: AnsiString; const args: array of const); noreturn;
+begin
+  raise TParserException.CreateFmt(self, afmt, args);
+end;
+
+
 function TTextParser.isEOF (): Boolean; inline; begin result := (mCurChar = #0); end;
 
 
@@ -617,16 +658,46 @@ begin
 end;
 
 
-procedure TTextParser.expectId (const aid: AnsiString);
+procedure TTextParser.expectId (const aid: AnsiString; caseSens: Boolean=true);
 begin
-  if (mTokType <> TTId) or (mTokStr <> aid) then raise Exception.Create('identifier '''+aid+''' expected');
+  if caseSens then
+  begin
+    if (mTokType <> TTId) or (mTokStr <> aid) then raise Exception.Create('identifier '''+aid+''' expected');
+  end
+  else
+  begin
+    if (mTokType <> TTId) or (not strEquCI1251(mTokStr, aid)) then raise Exception.Create('identifier '''+aid+''' expected');
+  end;
   skipToken();
 end;
 
 
-function TTextParser.eatId (const aid: AnsiString): Boolean;
+function TTextParser.eatId (const aid: AnsiString; caseSens: Boolean=true): Boolean;
 begin
-  result := (mTokType = TTId) and (mTokStr = aid);
+  if caseSens then
+  begin
+    result := (mTokType = TTId) and (mTokStr = aid);
+  end
+  else
+  begin
+    result := (mTokType = TTId) and strEquCI1251(mTokStr, aid);
+  end;
+  if result then skipToken();
+end;
+
+
+function TTextParser.eatIdOrStr (const aid: AnsiString; caseSens: Boolean=true): Boolean;
+begin
+  if caseSens then
+  begin
+    result := (mTokType = TTId) and (mTokStr = aid);
+    if not result then result := (mTokType = TTStr) and (mTokStr = aid);
+  end
+  else
+  begin
+    result := (mTokType = TTId) and strEquCI1251(mTokStr, aid);
+    if not result then result := (mTokType = TTStr) and strEquCI1251(mTokStr, aid);
+  end;
   if result then skipToken();
 end;