From 6964c759ca2e1387aadf68a70d8d838a12223b16 Mon Sep 17 00:00:00 2001 From: Ketmar Dark Date: Sun, 24 Sep 2017 16:35:47 +0300 Subject: [PATCH] Holmes UI: lot of flexbox layouting code fixes --- src/gx/gh_flexlay.pas | 217 ++++++++++---- src/gx/gh_ui.pas | 642 ++++++++++++++++++++++++++++++++++++++-- src/gx/gh_ui_common.pas | 27 ++ src/shared/xparser.pas | 87 +++++- 4 files changed, 874 insertions(+), 99 deletions(-) diff --git a/src/gx/gh_flexlay.pas b/src/gx/gh_flexlay.pas index cc01bb6..3eed2e4 100644 --- a/src/gx/gh_flexlay.pas +++ b/src/gx/gh_flexlay.pas @@ -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; diff --git a/src/gx/gh_ui.pas b/src/gx/gh_ui.pas index 774795d..a8593ad 100644 --- a/src/gx/gh_ui.pas +++ b/src/gx/gh_ui.pas @@ -14,6 +14,7 @@ * along with this program. If not, see . *) {$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. diff --git a/src/gx/gh_ui_common.pas b/src/gx/gh_ui_common.pas index 7143501..0c1aa75 100644 --- a/src/gx/gh_ui_common.pas +++ b/src/gx/gh_ui_common.pas @@ -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. diff --git a/src/shared/xparser.pas b/src/shared/xparser.pas index 7d7a9ed..b985f1a 100644 --- a/src/shared/xparser.pas +++ b/src/shared/xparser.pas @@ -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; -- 2.29.2