DEADSOFTWARE

changed license to GPLv3 only; sorry, no trust to FSF anymore
[d2df-sdl.git] / src / flexui / fui_ctls.pas
index b7d5bfa2a27dca30cc392399161d64abca808b53..12f710c21bbc3aae1845bc6b1c68bc8d8e5fd79e 100644 (file)
@@ -3,8 +3,7 @@
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
+ * the Free Software Foundation, version 3 of the License ONLY.
  *
  * This program is distributed in the hope that it will be useful,
  * but WITHOUT ANY WARRANTY; without even the implied warranty of
@@ -64,17 +63,16 @@ type
     mCancel: Boolean;
     mDefault: Boolean;
     // colors
+    mStyleLoaded: Boolean;
     mCtl4Style: AnsiString;
     mBackColor: array[0..ClrIdxMax] of TGxRGBA;
     mTextColor: array[0..ClrIdxMax] of TGxRGBA;
     mFrameColor: array[0..ClrIdxMax] of TGxRGBA;
     mFrameTextColor: array[0..ClrIdxMax] of TGxRGBA;
     mFrameIconColor: array[0..ClrIdxMax] of TGxRGBA;
-    mDarken: array[0..ClrIdxMax] of Integer; // -1: none
-
-  private
-    scis: TScissorSave;
-    scallowed: Boolean;
+    mSBarFullColor: array[0..ClrIdxMax] of TGxRGBA;
+    mSBarEmptyColor: array[0..ClrIdxMax] of TGxRGBA;
+    mDarken: array[0..ClrIdxMax] of Integer; // >255: none
 
   protected
     procedure updateStyle (); virtual;
@@ -110,15 +108,15 @@ type
 
     procedure calcFullClientSize ();
 
+    procedure drawFrame (gx, gy, resx, thalign: Integer; const text: AnsiString; dbl: Boolean);
+
+  protected
+    var savedClip: TGxRect; // valid only in `draw*()` calls
     //WARNING! do not call scissor functions outside `.draw*()` API!
     // set scissor to this rect (in local coords)
-    procedure setScissor (lx, ly, lw, lh: Integer);
-    // reset scissor to whole control
-    procedure resetScissor (fullArea: Boolean); inline; // "full area" means "with frame"
-
-    // DO NOT USE!
-    // set scissor to this rect (in global coords)
-    procedure setScissorGLInternal (x, y, w, h: Integer);
+    procedure setScissor (lx, ly, lw, lh: Integer); // valid only in `draw*()` calls
+    procedure resetScissor (); inline; // only client area, w/o frame
+    procedure resetScissorNC (); inline; // full drawing area, with frame
 
   public
     actionCB: TActionCB;
@@ -129,8 +127,6 @@ type
     mMaxSize: TLaySize; // maximum size
     mFlex: Integer;
     mHoriz: Boolean;
-    mCanWrap: Boolean;
-    mLineStart: Boolean;
     mHGroup: AnsiString;
     mVGroup: AnsiString;
     mAlign: Integer;
@@ -151,9 +147,7 @@ type
     //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?
-    function canWrap (): Boolean; inline; // for horizontal boxes: can wrap children? for child: `false` means 'nonbreakable at *next* ctl'
     function noPad (): Boolean; inline; // ignore padding in box direction for this control
-    function isLineStart (): Boolean; inline; // `true` if this ctl should start a new line; ignored for vertical boxes
     function getAlign (): 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
     function getHGroup (): AnsiString; inline; // empty: not grouped
@@ -169,8 +163,6 @@ type
     property flMaxSize: TLaySize read mMaxSize write mMaxSize;
     property flPadding: TLaySize read mPadding write mPadding;
     property flHoriz: Boolean read mHoriz write mHoriz;
-    property flCanWrap: Boolean read mCanWrap write mCanWrap;
-    property flLineStart: Boolean read mLineStart write mLineStart;
     property flAlign: Integer read mAlign write mAlign;
     property flExpand: Boolean read mExpand write mExpand;
     property flHGroup: AnsiString read mHGroup write mHGroup;
@@ -237,10 +229,15 @@ type
 
     procedure doAction (); virtual; // so user controls can override it
 
-    procedure mouseEvent (var ev: THMouseEvent); virtual; // returns `true` if event was eaten
-    procedure keyEvent (var ev: THKeyEvent); virtual; // returns `true` if event was eaten
-    procedure keyEventPre (var ev: THKeyEvent); virtual; // will be called before dispatching the event
-    procedure keyEventPost (var ev: THKeyEvent); virtual; // will be called after if nobody processed the event
+    procedure onEvent (var ev: TFUIEvent); virtual; // general dispatcher
+
+    procedure mouseEvent (var ev: TFUIEvent); virtual;
+    procedure mouseEventSink (var ev: TFUIEvent); virtual;
+    procedure mouseEventBubble (var ev: TFUIEvent); virtual;
+
+    procedure keyEvent (var ev: TFUIEvent); virtual;
+    procedure keyEventSink (var ev: TFUIEvent); virtual;
+    procedure keyEventBubble (var ev: TFUIEvent); virtual;
 
     function prevSibling (): TUIControl;
     function nextSibling (): TUIControl;
@@ -257,7 +254,7 @@ type
     procedure close (); // this closes *top-level* control
 
   public
-    property id: AnsiString read mId;
+    property id: AnsiString read mId write mId;
     property styleId: AnsiString read mStyleId;
     property scrollX: Integer read mScrollX write mScrollX;
     property scrollY: Integer read mScrollY write mScrollY;
@@ -279,7 +276,7 @@ type
 
   TUITopWindow = class(TUIControl)
   private
-    type TXMode = (None, Drag, Scroll);
+    type TXMode = (None, Drag, VScroll, HScroll);
 
   private
     mTitle: AnsiString;
@@ -313,8 +310,8 @@ type
     procedure drawControl (gx, gy: Integer); override;
     procedure drawControlPost (gx, gy: Integer); override;
 
-    procedure keyEvent (var ev: THKeyEvent); override; // returns `true` if event was eaten
-    procedure mouseEvent (var ev: THMouseEvent); override; // returns `true` if event was eaten
+    procedure keyEventBubble (var ev: TFUIEvent); override; // returns `true` if event was eaten
+    procedure mouseEvent (var ev: TFUIEvent); override; // returns `true` if event was eaten
 
   public
     property freeOnClose: Boolean read mFreeOnClose write mFreeOnClose;
@@ -341,8 +338,8 @@ type
 
     procedure drawControl (gx, gy: Integer); override;
 
-    procedure mouseEvent (var ev: THMouseEvent); override;
-    procedure keyEvent (var ev: THKeyEvent); override;
+    procedure mouseEvent (var ev: TFUIEvent); override;
+    procedure keyEvent (var ev: TFUIEvent); override;
 
   public
     property caption: AnsiString read mCaption write setCaption;
@@ -370,8 +367,6 @@ type
     procedure AfterConstruction (); override; // so it will be correctly initialized when created from parser
 
     function parseProperty (const prname: AnsiString; par: TTextParser): Boolean; override;
-
-    procedure drawControl (gx, gy: Integer); override;
   end;
 
   // ////////////////////////////////////////////////////////////////////// //
@@ -381,17 +376,9 @@ type
 
     function parseProperty (const prname: AnsiString; par: TTextParser): Boolean; override;
 
-    procedure drawControl (gx, gy: Integer); override;
-  end;
-
-  TUIHLine = class(TUILine)
-  public
-    procedure AfterConstruction (); override; // so it will be correctly initialized when created from parser
-  end;
+    procedure layPrepare (); override; // called before registering control in layouter
 
-  TUIVLine = class(TUILine)
-  public
-    procedure AfterConstruction (); override; // so it will be correctly initialized when created from parser
+    procedure drawControl (gx, gy: Integer); override;
   end;
 
   // ////////////////////////////////////////////////////////////////////// //
@@ -446,8 +433,8 @@ type
 
     procedure drawControl (gx, gy: Integer); override;
 
-    procedure mouseEvent (var ev: THMouseEvent); override;
-    procedure keyEventPost (var ev: THKeyEvent); override;
+    procedure mouseEvent (var ev: TFUIEvent); override;
+    procedure keyEventBubble (var ev: TFUIEvent); override;
 
   public
     property text: AnsiString read mText write setText;
@@ -457,16 +444,42 @@ type
 
   // ////////////////////////////////////////////////////////////////////// //
   TUIButton = class(TUITextLabel)
+  protected
+    mSkipLayPrepare: Boolean;
+    mShadowSize: Integer;
+    mAddMarkers: Boolean;
+    mHideMarkers: Boolean;
+    mPushed: Boolean;
+
   protected
     procedure setText (const s: AnsiString); override;
 
+    procedure cacheStyle (root: TUIStyle); override;
+
+    procedure blurred (); override;
+
   public
     procedure AfterConstruction (); override; // so it will be correctly initialized when created from parser
 
+    procedure layPrepare (); override; // called before registering control in layouter
+
     procedure drawControl (gx, gy: Integer); override;
 
-    procedure mouseEvent (var ev: THMouseEvent); override;
-    procedure keyEvent (var ev: THKeyEvent); override;
+    procedure mouseEvent (var ev: TFUIEvent); override;
+    procedure keyEvent (var ev: TFUIEvent); override;
+  end;
+
+  // ////////////////////////////////////////////////////////////////////// //
+  TUIButtonRound = class(TUIButton)
+  protected
+    procedure setText (const s: AnsiString); override;
+
+  public
+    procedure AfterConstruction (); override; // so it will be correctly initialized when created from parser
+
+    procedure layPrepare (); override; // called before registering control in layouter
+
+    procedure drawControl (gx, gy: Integer); override;
   end;
 
   // ////////////////////////////////////////////////////////////////////// //
@@ -474,8 +487,7 @@ type
   protected
     mBoolVar: PBoolean;
     mChecked: Boolean;
-    mCheckedStr: AnsiString;
-    mUncheckedStr: AnsiString;
+    mIcon: TGxContext.TMarkIcon;
     mSwitchColor: array[0..ClrIdxMax] of TGxRGBA;
 
   protected
@@ -493,8 +505,8 @@ type
 
     procedure drawControl (gx, gy: Integer); override;
 
-    procedure mouseEvent (var ev: THMouseEvent); override;
-    procedure keyEvent (var ev: THKeyEvent); override;
+    procedure mouseEvent (var ev: TFUIEvent); override;
+    procedure keyEvent (var ev: TFUIEvent); override;
 
     procedure setVar (pvar: PBoolean);
 
@@ -532,16 +544,21 @@ type
 
 
 // ////////////////////////////////////////////////////////////////////////// //
-procedure uiMouseEvent (var evt: THMouseEvent);
-procedure uiKeyEvent (var evt: THKeyEvent);
+procedure uiDispatchEvent (var evt: TFUIEvent);
 procedure uiDraw ();
 
+procedure uiFocus ();
+procedure uiBlur ();
+
 
 // ////////////////////////////////////////////////////////////////////////// //
 procedure uiAddWindow (ctl: TUIControl);
 procedure uiRemoveWindow (ctl: TUIControl); // will free window if `mFreeOnClose` is `true`
 function uiVisibleWindow (ctl: TUIControl): Boolean;
 
+// this can return `nil` or disabled control
+function uiGetFocusedCtl (): TUIControl;
+
 procedure uiUpdateStyles ();
 
 
@@ -550,9 +567,15 @@ procedure uiUpdateStyles ();
 procedure uiLayoutCtl (ctl: TUIControl);
 
 
+// ////////////////////////////////////////////////////////////////////////// //
+procedure uiInitialize ();
+procedure uiDeinitialize ();
+
+
 // ////////////////////////////////////////////////////////////////////////// //
 var
   fuiRenderScale: Single = 1.0;
+  uiContext: TGxContext = nil;
 
 
 implementation
@@ -562,6 +585,26 @@ uses
   utils;
 
 
+var
+  uiInsideDispatcher: Boolean = false;
+  uiTopList: array of TUIControl = nil;
+  uiGrabCtl: TUIControl = nil;
+
+
+// ////////////////////////////////////////////////////////////////////////// //
+procedure uiDeinitialize ();
+begin
+  FreeAndNil(uiContext);
+end;
+
+
+procedure uiInitialize ();
+begin
+  if (uiContext <> nil) then raise Exception.Create('FlexUI already initialized');
+  uiContext := TGxContext.Create();
+end;
+
+
 // ////////////////////////////////////////////////////////////////////////// //
 var
   ctlsToKill: array of TUIControl = nil;
@@ -592,6 +635,7 @@ begin
   begin
     ctl := ctlsToKill[f];
     if (ctl = nil) then break;
+    if (uiGrabCtl <> nil) and (ctl.isMyChild(uiGrabCtl)) then uiGrabCtl := nil; // just in case
     ctlsToKill[f] := nil;
     FreeAndNil(ctl);
   end;
@@ -644,6 +688,7 @@ begin
   if (ctl = nil) then exit;
   lay := TFlexLayouter.Create();
   try
+    if (not ctl.mStyleLoaded) then ctl.updateStyle();
     if (ctl is TUITopWindow) and (TUITopWindow(ctl).fitToScreen) then TUITopWindow(ctl).flFitToScreen();
 
     lay.setup(ctl);
@@ -691,11 +736,6 @@ end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
-var
-  uiTopList: array of TUIControl = nil;
-  uiGrabCtl: TUIControl = nil;
-
-
 procedure uiUpdateStyles ();
 var
   ctl: TUIControl;
@@ -704,73 +744,180 @@ begin
 end;
 
 
-procedure uiMouseEvent (var evt: THMouseEvent);
+procedure uiDispatchEvent (var evt: TFUIEvent);
 var
-  ev: THMouseEvent;
-  f, c: Integer;
-  lx, ly: Integer;
-  ctmp: TUIControl;
-begin
-  processKills();
-  if (evt.eaten) or (evt.cancelled) then exit;
-  ev := evt;
-  ev.x := trunc(ev.x/fuiRenderScale);
-  ev.y := trunc(ev.y/fuiRenderScale);
-  ev.dx := trunc(ev.dx/fuiRenderScale); //FIXME
-  ev.dy := trunc(ev.dy/fuiRenderScale); //FIXME
-  try
+  ev: TFUIEvent;
+  destCtl: TUIControl;
+
+  procedure doSink (ctl: TUIControl);
+  begin
+    if (ctl = nil) or (not ev.alive) then exit;
+    if (ctl.mParent <> nil) then
+    begin
+      doSink(ctl.mParent);
+      if (not ev.alive) then exit;
+    end;
+    //if (ctl = destCtl) then writeln(' SINK: MINE! <', ctl.className, '>');
+    ev.setSinking();
+    ctl.onEvent(ev);
+    if (ctl = destCtl) and (ev.alive) then
+    begin
+      ev.setMine();
+      ctl.onEvent(ev);
+    end;
+  end;
+
+  procedure dispatchTo (ctl: TUIControl);
+  begin
+    if (ctl = nil) then exit;
+    destCtl := ctl;
+    // sink
+    doSink(ctl);
+    // bubble
+    //ctl := ctl.mParent; // 'cause "mine" is processed in `doSink()`
+    while (ctl <> nil) and (ev.alive) do
+    begin
+      ev.setBubbling();
+      ctl.onEvent(ev);
+      ctl := ctl.mParent;
+    end;
+  end;
+
+  procedure doMouseEvent ();
+  var
+    doUngrab: Boolean;
+    ctl: TUIControl;
+    win: TUIControl;
+    lx, ly: Integer;
+    f, c: Integer;
+  begin
+    // pass mouse events to control with grab, if there is any
     if (uiGrabCtl <> nil) then
     begin
-      uiGrabCtl.mouseEvent(ev);
-      if (ev.release) and ((ev.bstate and (not ev.but)) = 0) then uiGrabCtl := nil;
+      //writeln('GRABBED: ', uiGrabCtl.className);
+      doUngrab := (ev.release) and ((ev.bstate and (not ev.but)) = 0);
+      dispatchTo(uiGrabCtl);
+      //FIXME: create API to get grabs, so control can regrab itself event on release
+      if (doUngrab) and (uiGrabCtl = destCtl) then uiGrabCtl := nil;
       ev.eat();
       exit;
     end;
-    if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then uiTopList[High(uiTopList)].mouseEvent(ev);
-    if (not ev.eaten) and (not ev.cancelled) and (ev.press) then
+    // get top window
+    if (Length(uiTopList) > 0) then win := uiTopList[High(uiTopList)] else win := nil;
+    // check if we're still in top window
+    if (ev.press) and (win <> nil) and (not win.toLocal(0, 0, lx, ly)) then
     begin
-      for f := High(uiTopList) downto 0 do
+      // we have other windows too; check for window switching
+      for f := High(uiTopList)-1 downto 0 do
       begin
-        if uiTopList[f].toLocal(ev.x, ev.y, lx, ly) then
+        if (uiTopList[f].enabled) and (uiTopList[f].toLocal(ev.x, ev.y, lx, ly)) then
         begin
-          if (uiTopList[f].enabled) and (f <> High(uiTopList)) then
-          begin
-            uiTopList[High(uiTopList)].blurred();
-            ctmp := uiTopList[f];
-            uiGrabCtl := nil;
-            for c := f+1 to High(uiTopList) do uiTopList[c-1] := uiTopList[c];
-            uiTopList[High(uiTopList)] := ctmp;
-            ctmp.activated();
-            ctmp.mouseEvent(ev);
-          end;
-          ev.eat();
-          exit;
+          // switch
+          win.blurred();
+          win := uiTopList[f];
+          for c := f+1 to High(uiTopList) do uiTopList[c-1] := uiTopList[c];
+          uiTopList[High(uiTopList)] := win;
+          win.activated();
+          break;
         end;
       end;
     end;
-  finally
-    if (ev.eaten) then evt.eat();
-    if (ev.cancelled) then evt.cancel();
+    // dispatch event
+    if (win <> nil) and (win.toLocal(ev.x, ev.y, lx, ly)) then
+    begin
+      ctl := win.controlAtXY(ev.x, ev.y); // don't allow disabled controls
+      if (ctl = nil) or (not ctl.canFocus) or (not ctl.enabled) then ctl := win;
+      // pass focus to another event and set grab, if necessary
+      if (ev.press) then
+      begin
+        // pass focus, if necessary
+        if (win.mFocused <> ctl) then
+        begin
+          if (win.mFocused <> nil) then win.mFocused.blurred();
+          uiGrabCtl := ctl;
+          win.mFocused := ctl;
+          if (ctl <> win) then ctl.activated();
+        end
+        else
+        begin
+          uiGrabCtl := ctl;
+        end;
+      end;
+      dispatchTo(ctl);
+    end;
   end;
-end;
 
-
-procedure uiKeyEvent (var evt: THKeyEvent);
 var
-  ev: THKeyEvent;
+  svx, svy, svdx, svdy: Integer;
+  svscale: Single;
+  odp: Boolean;
 begin
   processKills();
-  if (evt.eaten) or (evt.cancelled) then exit;
+  if (not evt.alive) then exit;
+  odp := uiInsideDispatcher;
+  uiInsideDispatcher := true;
+  //writeln('ENTER: FUI DISPATCH');
   ev := evt;
-  ev.x := trunc(ev.x/fuiRenderScale);
-  ev.y := trunc(ev.y/fuiRenderScale);
+  // normalize mouse coordinates
+  svscale := fuiRenderScale;
+  ev.x := trunc(ev.x/svscale);
+  ev.y := trunc(ev.y/svscale);
+  ev.dx := trunc(ev.dx/svscale); //FIXME
+  ev.dy := trunc(ev.dy/svscale); //FIXME
+  svx := ev.x;
+  svy := ev.y;
+  svdx := ev.dx;
+  svdy := ev.dy;
   try
-    if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then uiTopList[High(uiTopList)].keyEvent(ev);
-    //if (ev.release) then begin ev.eat(); exit; end;
+    // "event grab" eats only mouse events
+    if (ev.mouse) then
+    begin
+      // we need to so some special processing here
+      doMouseEvent();
+    end
+    else
+    begin
+      // simply dispatch to focused control
+      dispatchTo(uiGetFocusedCtl);
+    end;
   finally
-    if (ev.eaten) then evt.eat();
-    if (ev.cancelled) then evt.cancel();
+    uiInsideDispatcher := odp;
+    if (ev.x = svx) and (ev.y = svy) and (ev.dx = svdx) and (ev.dy = svdy) then
+    begin
+      // due to possible precision loss
+      svx := evt.x;
+      svy := evt.y;
+      svdx := evt.dx;
+      svdy := evt.dy;
+      evt := ev;
+      evt.x := svx;
+      evt.y := svy;
+      evt.dx := svdx;
+      evt.dy := svdy;
+    end
+    else
+    begin
+      // scale back
+      evt := ev;
+      evt.x := trunc(evt.x*svscale);
+      evt.y := trunc(evt.y*svscale);
+      evt.dx := trunc(evt.dx*svscale);
+      evt.dy := trunc(evt.dy*svscale);
+    end;
   end;
+  processKills();
+  //writeln('EXIT: FUI DISPATCH');
+end;
+
+procedure uiFocus ();
+begin
+  if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then uiTopList[High(uiTopList)].activated();
+end;
+
+
+procedure uiBlur ();
+begin
+  if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then uiTopList[High(uiTopList)].blurred();
 end;
 
 
@@ -780,7 +927,9 @@ var
   ctl: TUIControl;
 begin
   processKills();
-  gxBeginUIDraw(fuiRenderScale);
+  //if (uiContext = nil) then uiContext := TGxContext.Create();
+  gxSetContext(uiContext, fuiRenderScale);
+  uiContext.resetClip();
   try
     for f := 0 to High(uiTopList) do
     begin
@@ -789,11 +938,22 @@ begin
       if (f <> High(uiTopList)) then
       begin
         cidx := ctl.getColorIndex;
-        if (ctl.mDarken[cidx] > 0) then darkenRect(ctl.x0, ctl.y0, ctl.width, ctl.height, ctl.mDarken[cidx]);
+        uiContext.darkenRect(ctl.x0, ctl.y0, ctl.width, ctl.height, ctl.mDarken[cidx]);
       end;
     end;
   finally
-    gxEndUIDraw();
+    gxSetContext(nil);
+  end;
+end;
+
+
+function uiGetFocusedCtl (): TUIControl;
+begin
+  result := nil;
+  if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then
+  begin
+    result := uiTopList[High(uiTopList)].mFocused;
+    if (result = nil) then result := uiTopList[High(uiTopList)];
   end;
 end;
 
@@ -811,7 +971,7 @@ begin
     begin
       if (f <> High(uiTopList)) then
       begin
-        uiTopList[High(uiTopList)].blurred();
+        if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then uiTopList[High(uiTopList)].blurred();
         for c := f+1 to High(uiTopList) do uiTopList[c-1] := uiTopList[c];
         uiTopList[High(uiTopList)] := ctl;
         ctl.activated();
@@ -819,10 +979,10 @@ begin
       exit;
     end;
   end;
-  if (Length(uiTopList) > 0) then uiTopList[High(uiTopList)].blurred();
+  if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then uiTopList[High(uiTopList)].blurred();
   SetLength(uiTopList, Length(uiTopList)+1);
   uiTopList[High(uiTopList)] := ctl;
-  ctl.updateStyle();
+  if (not ctl.mStyleLoaded) then ctl.updateStyle();
   ctl.activated();
 end;
 
@@ -841,6 +1001,7 @@ begin
       ctl.blurred();
       for c := f+1 to High(uiTopList) do uiTopList[c-1] := uiTopList[c];
       SetLength(uiTopList, Length(uiTopList)-1);
+      if (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then uiTopList[High(uiTopList)].activated();
       if (ctl is TUITopWindow) then
       begin
         try
@@ -884,7 +1045,7 @@ begin
   mX := 0;
   mY := 0;
   mWidth := 64;
-  mHeight := 8;
+  mHeight := uiContext.charHeight(' ');
   mFrameWidth := 0;
   mFrameHeight := 0;
   mEnabled := true;
@@ -892,32 +1053,50 @@ begin
   mChildren := nil;
   mFocused := nil;
   mEscClose := false;
-  scallowed := false;
   mDrawShadow := false;
   actionCB := nil;
   // layouter interface
-  //mDefSize := TLaySize.Create(64, 8); // default size
-  mDefSize := TLaySize.Create(0, 0); // default size
+  //mDefSize := TLaySize.Create(64, uiContext.charHeight(' ')); // default size
+  mDefSize := TLaySize.Create(0, 0); // default size: hidden control
   mMaxSize := TLaySize.Create(-1, -1); // maximum size
   mPadding := TLaySize.Create(0, 0);
   mNoPad := false;
   mFlex := 0;
   mHoriz := true;
-  mCanWrap := false;
-  mLineStart := false;
   mHGroup := '';
   mVGroup := '';
   mStyleId := '';
   mCtl4Style := '';
   mAlign := -1; // left/top
   mExpand := false;
+  mStyleLoaded := false;
 end;
 
 
 destructor TUIControl.Destroy ();
 var
   f, c: Integer;
+  doActivateOtherWin: Boolean = false;
 begin
+  if (uiInsideDispatcher) then raise Exception.Create('FlexUI: cannot destroy objects in event dispatcher');
+  if (uiGrabCtl = self) then uiGrabCtl := nil;
+  // just in case, check if this is top-level shit
+  for f := 0 to High(uiTopList) do
+  begin
+    if (uiTopList[f] = self) then
+    begin
+      if (uiGrabCtl <> nil) and (isMyChild(uiGrabCtl)) then uiGrabCtl := nil;
+      for c := f+1 to High(uiTopList) do uiTopList[c-1] := uiTopList[c];
+      SetLength(uiTopList, Length(uiTopList)-1);
+      doActivateOtherWin := true;
+      break;
+    end;
+  end;
+  if (doActivateOtherWin) and (Length(uiTopList) > 0) and (uiTopList[High(uiTopList)].enabled) then
+  begin
+    uiTopList[High(uiTopList)].activated();
+  end;
+  // other checks
   if (mParent <> nil) then
   begin
     setFocused(false);
@@ -969,6 +1148,7 @@ begin
   if (stl = nil) then stl := uiFindStyle(''); // default
   cacheStyle(stl);
   for ctl in mChildren do ctl.updateStyle();
+  mStyleLoaded := true;
 end;
 
 procedure TUIControl.cacheStyle (root: TUIStyle);
@@ -983,21 +1163,27 @@ begin
   mFrameColor[ClrIdxActive] := root.get('frame-color', 'active', cst).asRGBADef(TGxRGBA.Create(255, 255, 255));
   mFrameTextColor[ClrIdxActive] := root.get('frame-text-color', 'active', cst).asRGBADef(TGxRGBA.Create(255, 255, 255));
   mFrameIconColor[ClrIdxActive] := root.get('frame-icon-color', 'active', cst).asRGBADef(TGxRGBA.Create(0, 255, 0));
-  mDarken[ClrIdxActive] := root.get('darken', 'active', cst).asInt(-1);
+  mSBarFullColor[ClrIdxActive] := root.get('scrollbar-full-color', 'active', cst).asRGBADef(TGxRGBA.Create(255, 255, 255));
+  mSBarEmptyColor[ClrIdxActive] := root.get('scrollbar-empty-color', 'active', cst).asRGBADef(TGxRGBA.Create(128, 128, 128));
+  mDarken[ClrIdxActive] := root.get('darken', 'active', cst).asInt(666);
   // disabled
   mBackColor[ClrIdxDisabled] := root.get('back-color', 'disabled', cst).asRGBADef(TGxRGBA.Create(0, 0, 128));
   mTextColor[ClrIdxDisabled] := root.get('text-color', 'disabled', cst).asRGBADef(TGxRGBA.Create(127, 127, 127));
   mFrameColor[ClrIdxDisabled] := root.get('frame-color', 'disabled', cst).asRGBADef(TGxRGBA.Create(127, 127, 127));
   mFrameTextColor[ClrIdxDisabled] := root.get('frame-text-color', 'disabled', cst).asRGBADef(TGxRGBA.Create(127, 127, 127));
   mFrameIconColor[ClrIdxDisabled] := root.get('frame-icon-color', 'disabled', cst).asRGBADef(TGxRGBA.Create(0, 127, 0));
-  mDarken[ClrIdxDisabled] := root.get('darken', 'disabled', cst).asInt(-1);
+  mSBarFullColor[ClrIdxDisabled] := root.get('scrollbar-full-color', 'disabled', cst).asRGBADef(TGxRGBA.Create(127, 127, 127));
+  mSBarEmptyColor[ClrIdxDisabled] := root.get('scrollbar-empty-color', 'disabled', cst).asRGBADef(TGxRGBA.Create(98, 98, 98));
+  mDarken[ClrIdxDisabled] := root.get('darken', 'disabled', cst).asInt(666);
   // inactive
   mBackColor[ClrIdxInactive] := root.get('back-color', 'inactive', cst).asRGBADef(TGxRGBA.Create(0, 0, 128));
   mTextColor[ClrIdxInactive] := root.get('text-color', 'inactive', cst).asRGBADef(TGxRGBA.Create(255, 255, 255));
   mFrameColor[ClrIdxInactive] := root.get('frame-color', 'inactive', cst).asRGBADef(TGxRGBA.Create(255, 255, 255));
   mFrameTextColor[ClrIdxInactive] := root.get('frame-text-color', 'inactive', cst).asRGBADef(TGxRGBA.Create(255, 255, 255));
   mFrameIconColor[ClrIdxInactive] := root.get('frame-icon-color', 'inactive', cst).asRGBADef(TGxRGBA.Create(0, 255, 0));
-  mDarken[ClrIdxInactive] := root.get('darken', 'inactive', cst).asInt(-1);
+  mSBarFullColor[ClrIdxInactive] := root.get('scrollbar-full-color', 'inactive', cst).asRGBADef(TGxRGBA.Create(255, 255, 255));
+  mSBarEmptyColor[ClrIdxInactive] := root.get('scrollbar-empty-color', 'inactive', cst).asRGBADef(TGxRGBA.Create(128, 128, 128));
+  mDarken[ClrIdxInactive] := root.get('darken', 'inactive', cst).asInt(666);
 end;
 
 
@@ -1007,9 +1193,7 @@ function TUIControl.getMaxSize (): TLaySize; inline; begin result := mLayMaxSize
 function TUIControl.getPadding (): TLaySize; inline; begin result := mPadding; end;
 function TUIControl.getFlex (): Integer; inline; begin result := mFlex; end;
 function TUIControl.isHorizBox (): Boolean; inline; begin result := mHoriz; end;
-function TUIControl.canWrap (): Boolean; inline; begin result := mCanWrap; end;
 function TUIControl.noPad (): Boolean; inline; begin result := mNoPad; end;
-function TUIControl.isLineStart (): Boolean; inline; begin result := mLineStart; end;
 function TUIControl.getAlign (): Integer; inline; begin result := mAlign; end;
 function TUIControl.getExpand (): Boolean; inline; begin result := mExpand; end;
 function TUIControl.getHGroup (): AnsiString; inline; begin result := mHGroup; end;
@@ -1026,14 +1210,23 @@ begin
   end;
   mWidth := asize.w;
   mHeight := asize.h;
+  if (mLayMaxSize.w >= 0) then mWidth := nmin(mWidth, mLayMaxSize.w);
+  if (mLayMaxSize.h >= 0) then mHeight := nmin(mHeight, mLayMaxSize.h);
 end;
 
 procedure TUIControl.layPrepare ();
 begin
   mLayDefSize := mDefSize;
-  mLayMaxSize := mMaxSize;
-  if (mLayMaxSize.w >= 0) then mLayMaxSize.w += mFrameWidth*2;
-  if (mLayMaxSize.h >= 0) then mLayMaxSize.h += mFrameHeight*2;
+  if (mLayDefSize.w <> 0) or (mLayDefSize.h <> 0) then
+  begin
+    mLayMaxSize := mMaxSize;
+    if (mLayMaxSize.w >= 0) then begin mLayDefSize.w += mFrameWidth*2; mLayMaxSize.w += mFrameWidth*2; end;
+    if (mLayMaxSize.h >= 0) then begin mLayDefSize.h += mFrameHeight*2; mLayMaxSize.h += mFrameHeight*2; end;
+  end
+  else
+  begin
+    mLayMaxSize := TLaySize.Create(0, 0);
+  end;
 end;
 
 
@@ -1274,8 +1467,6 @@ begin
   if (strEquCI1251(prname, 'padding')) then begin mPadding := parsePadding(par); exit; end;
   if (strEquCI1251(prname, 'nopad')) then begin mNoPad := true; exit; end;
   // flags
-  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;
@@ -1497,7 +1688,7 @@ begin
     mParent.getDrawRect(gx, gy, wdt, hgt);
     if (wdt > 0) and (hgt > 0) then
     begin
-      if not intersectRect(gx, gy, wdt, hgt, cgx, cgy, mWidth, mHeight) then
+      if (not intersectRect(gx, gy, wdt, hgt, cgx, cgy, mWidth, mHeight)) then
       begin
         wdt := 0;
         hgt := 0;
@@ -1838,45 +2029,40 @@ end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
-procedure TUIControl.setScissorGLInternal (x, y, w, h: Integer);
-begin
-  if not scallowed then exit;
-  x := trunc(x*fuiRenderScale);
-  y := trunc(y*fuiRenderScale);
-  w := trunc(w*fuiRenderScale);
-  h := trunc(h*fuiRenderScale);
-  scis.combineRect(x, y, w, h);
-end;
-
 procedure TUIControl.setScissor (lx, ly, lw, lh: Integer);
 var
   gx, gy, wdt, hgt, cgx, cgy: Integer;
 begin
-  if not scallowed then exit;
-
-  if not intersectRect(lx, ly, lw, lh, 0, 0, mWidth, mHeight) then
+  if (not intersectRect(lx, ly, lw, lh, 0, 0, mWidth, mHeight)) then
   begin
-    scis.combineRect(0, 0, 0, 0);
+    uiContext.clip := TGxRect.Create(0, 0, 0, 0);
     exit;
   end;
 
   getDrawRect(gx, gy, wdt, hgt);
+
   toGlobal(lx, ly, cgx, cgy);
-  if not intersectRect(gx, gy, wdt, hgt, cgx, cgy, lw, lh) then
+  if (not intersectRect(gx, gy, wdt, hgt, cgx, cgy, lw, lh)) then
   begin
-    scis.combineRect(0, 0, 0, 0);
+    uiContext.clip := TGxRect.Create(0, 0, 0, 0);
     exit;
   end;
 
-  setScissorGLInternal(gx, gy, wdt, hgt);
+  uiContext.clip := savedClip;
+  uiContext.combineClip(TGxRect.Create(gx, gy, wdt, hgt));
+  //uiContext.clip := TGxRect.Create(gx, gy, wdt, hgt);
+end;
+
+procedure TUIControl.resetScissorNC (); inline;
+begin
+  setScissor(0, 0, mWidth, mHeight);
 end;
 
-procedure TUIControl.resetScissor (fullArea: Boolean); inline;
+procedure TUIControl.resetScissor (); inline;
 begin
-  if not scallowed then exit;
-  if (fullArea) then
+  if ((mFrameWidth <= 0) and (mFrameHeight <= 0)) then
   begin
-    setScissor(0, 0, mWidth, mHeight);
+    resetScissorNC();
   end
   else
   begin
@@ -1886,163 +2072,214 @@ end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
+procedure TUIControl.drawFrame (gx, gy, resx, thalign: Integer; const text: AnsiString; dbl: Boolean);
+var
+  cidx, tx, tw: Integer;
+begin
+  if (mFrameWidth < 1) or (mFrameHeight < 1) then exit;
+  cidx := getColorIndex;
+  uiContext.color := mFrameColor[cidx];
+  case mFrameHeight of
+    8:
+      begin
+        if dbl then
+        begin
+          uiContext.rect(gx+3, gy+3, mWidth-6, mHeight-6);
+          uiContext.rect(gx+5, gy+5, mWidth-10, mHeight-10);
+        end
+        else
+        begin
+          uiContext.rect(gx+4, gy+4, mWidth-8, mHeight-8);
+        end;
+      end;
+    14:
+      begin
+        if dbl then
+        begin
+          uiContext.rect(gx+3, gy+3+3, mWidth-6, mHeight-6-6);
+          uiContext.rect(gx+5, gy+5+3, mWidth-10, mHeight-10-6);
+        end
+        else
+        begin
+          uiContext.rect(gx+4, gy+4+3, mWidth-8, mHeight-8-6);
+        end;
+      end;
+    16:
+      begin
+        if dbl then
+        begin
+          uiContext.rect(gx+3, gy+3+4, mWidth-6, mHeight-6-8);
+          uiContext.rect(gx+5, gy+5+4, mWidth-10, mHeight-10-8);
+        end
+        else
+        begin
+          uiContext.rect(gx+4, gy+4+4, mWidth-8, mHeight-8-8);
+        end;
+      end;
+    else
+      begin
+        //TODO!
+        if dbl then
+        begin
+        end
+        else
+        begin
+        end;
+      end;
+  end;
+
+  // title
+  if (Length(text) > 0) then
+  begin
+    if (resx < 0) then resx := 0;
+    tw := uiContext.textWidth(text);
+    setScissor(mFrameWidth+resx, 0, mWidth-mFrameWidth*2-resx, mFrameHeight);
+         if (thalign < 0) then tx := gx+resx+mFrameWidth+2
+    else if (thalign > 0) then tx := gx+mWidth-mFrameWidth-1-tw
+    else tx := (gx+resx+mFrameWidth)+(mWidth-mFrameWidth*2-resx-tw) div 2;
+    uiContext.color := mBackColor[cidx];
+    uiContext.fillRect(tx-2, gy, tw+4, mFrameHeight);
+    uiContext.color := mFrameTextColor[cidx];
+    uiContext.drawText(tx, gy, text);
+  end;
+end;
+
+
 procedure TUIControl.draw ();
 var
   f: Integer;
   gx, gy: Integer;
+
 begin
-  if (mWidth < 1) or (mHeight < 1) then exit;
+  if (mWidth < 1) or (mHeight < 1) or (uiContext = nil) or (not uiContext.active) then exit;
   toGlobal(0, 0, gx, gy);
 
-  scis.save(true); // scissoring enabled
+  savedClip := uiContext.clip;
   try
-    scallowed := true;
-    resetScissor(true); // full area
+    resetScissorNC();
     drawControl(gx, gy);
-    resetScissor(false); // client area
+    resetScissor();
     for f := 0 to High(mChildren) do mChildren[f].draw();
-    resetScissor(true); // full area
+    resetScissorNC();
     drawControlPost(gx, gy);
   finally
-    scis.restore();
-    scallowed := false;
+    uiContext.clip := savedClip;
   end;
 end;
 
 procedure TUIControl.drawControl (gx, gy: Integer);
 begin
-  //if (mParent = nil) then darkenRect(gx, gy, mWidth, mHeight, 64);
 end;
 
 procedure TUIControl.drawControlPost (gx, gy: Integer);
 begin
-  // shadow
-  if mDrawShadow and (mWidth > 0) and (mHeight > 0) then
+  // shadow for top-level controls
+  if (mParent = nil) and (mDrawShadow) and (mWidth > 0) and (mHeight > 0) then
   begin
-    setScissorGLInternal(gx+8, gy+8, mWidth, mHeight);
-    darkenRect(gx+mWidth, gy+8, 8, mHeight, 128);
-    darkenRect(gx+8, gy+mHeight, mWidth-8, 8, 128);
+    uiContext.resetClip();
+    uiContext.darkenRect(gx+mWidth, gy+8, 8, mHeight, 128);
+    uiContext.darkenRect(gx+8, gy+mHeight, mWidth-8, 8, 128);
   end;
 end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
-procedure TUIControl.mouseEvent (var ev: THMouseEvent);
-var
-  ctl: TUIControl;
+procedure TUIControl.onEvent (var ev: TFUIEvent);
 begin
-  if (not enabled) then exit;
-  if (mWidth < 1) or (mHeight < 1) then exit;
-  ctl := controlAtXY(ev.x, ev.y);
-  if (ctl = nil) then exit;
-  if (ctl.canFocus) and (ev.press) then
+  if (not ev.alive) or (not enabled) then exit;
+  //if (ev.mine) then writeln(' MINE: <', className, '>');
+  if (ev.key) then
   begin
-    if (ctl <> topLevel.mFocused) then ctl.setFocused(true);
-    uiGrabCtl := ctl;
+         if (ev.sinking) then keyEventSink(ev)
+    else if (ev.bubbling) then keyEventBubble(ev)
+    else if (ev.mine) then keyEvent(ev);
+  end
+  else if (ev.mouse) then
+  begin
+         if (ev.sinking) then mouseEventSink(ev)
+    else if (ev.bubbling) then mouseEventBubble(ev)
+    else if (ev.mine) then mouseEvent(ev);
   end;
-  if (ctl <> self) then ctl.mouseEvent(ev);
-  //ev.eat();
 end;
 
 
-procedure TUIControl.keyEvent (var ev: THKeyEvent);
+procedure TUIControl.mouseEventSink (var ev: TFUIEvent);
+begin
+end;
 
-  function doPreKey (ctl: TUIControl): Boolean;
-  begin
-    if (not ctl.enabled) then begin result := false; exit; end;
-    ctl.keyEventPre(ev);
-    result := (ev.eaten) or (ev.cancelled); // stop if event was consumed
-  end;
+procedure TUIControl.mouseEventBubble (var ev: TFUIEvent);
+begin
+end;
+
+procedure TUIControl.mouseEvent (var ev: TFUIEvent);
+begin
+end;
 
-  function doPostKey (ctl: TUIControl): Boolean;
-  begin
-    if (not ctl.enabled) then begin result := false; exit; end;
-    ctl.keyEventPost(ev);
-    result := (ev.eaten) or (ev.cancelled); // stop if event was consumed
-  end;
 
+procedure TUIControl.keyEventSink (var ev: TFUIEvent);
 var
   ctl: TUIControl;
 begin
   if (not enabled) then exit;
-  if (ev.eaten) or (ev.cancelled) then exit;
-  // call pre-key
-  if (mParent = nil) then
-  begin
-    forEachControl(doPreKey);
-    if (ev.eaten) or (ev.cancelled) then exit;
-  end;
-  // focused control should process keyboard first
-  if (topLevel.mFocused <> self) and isMyChild(topLevel.mFocused) and (topLevel.mFocused.enabled) then
+  if (not ev.alive) then exit;
+  // for top-level controls
+  if (mParent <> nil) then exit;
+  if (mEscClose) and (ev = 'Escape') then
   begin
-    // bubble keyboard event
-    ctl := topLevel.mFocused;
-    while (ctl <> nil) and (ctl <> self) do
+    if (not assigned(closeRequestCB)) or (closeRequestCB(self)) then
     begin
-      ctl.keyEvent(ev);
-      if (ev.eaten) or (ev.cancelled) then exit;
-      ctl := ctl.mParent;
+      uiRemoveWindow(self);
     end;
+    ev.eat();
+    exit;
   end;
-  // for top-level controls
-  if (mParent = nil) then
+  if (ev = 'Enter') or (ev = 'C-Enter') then
   begin
-    if (ev = 'S-Tab') then
-    begin
-      ctl := findPrevFocus(mFocused, true);
-      if (ctl <> nil) and (ctl <> mFocused) then ctl.setFocused(true);
-      ev.eat();
-      exit;
-    end;
-    if (ev = 'Tab') then
+    ctl := findDefaulControl();
+    if (ctl <> nil) then
     begin
-      ctl := findNextFocus(mFocused, true);
-      if (ctl <> nil) and (ctl <> mFocused) then ctl.setFocused(true);
       ev.eat();
+      ctl.doAction();
       exit;
     end;
-    if (ev = 'Enter') or (ev = 'C-Enter') then
-    begin
-      ctl := findDefaulControl();
-      if (ctl <> nil) then
-      begin
-        ev.eat();
-        ctl.doAction();
-        exit;
-      end;
-    end;
-    if (ev = 'Escape') then
-    begin
-      ctl := findCancelControl();
-      if (ctl <> nil) then
-      begin
-        ev.eat();
-        ctl.doAction();
-        exit;
-      end;
-    end;
-    if mEscClose and (ev = 'Escape') then
+  end;
+  if (ev = 'Escape') then
+  begin
+    ctl := findCancelControl();
+    if (ctl <> nil) then
     begin
-      if (not assigned(closeRequestCB)) or (closeRequestCB(self)) then
-      begin
-        uiRemoveWindow(self);
-      end;
       ev.eat();
+      ctl.doAction();
       exit;
     end;
-    // call post-keys
-    if (ev.eaten) or (ev.cancelled) then exit;
-    forEachControl(doPostKey);
   end;
 end;
 
-
-procedure TUIControl.keyEventPre (var ev: THKeyEvent);
+procedure TUIControl.keyEventBubble (var ev: TFUIEvent);
+var
+  ctl: TUIControl;
 begin
+  if (not enabled) then exit;
+  if (not ev.alive) then exit;
+  // for top-level controls
+  if (mParent <> nil) then exit;
+  if (ev = 'S-Tab') then
+  begin
+    ctl := findPrevFocus(mFocused, true);
+    if (ctl <> nil) and (ctl <> mFocused) then ctl.setFocused(true);
+    ev.eat();
+    exit;
+  end;
+  if (ev = 'Tab') then
+  begin
+    ctl := findNextFocus(mFocused, true);
+    if (ctl <> nil) and (ctl <> mFocused) then ctl.setFocused(true);
+    ev.eat();
+    exit;
+  end;
 end;
 
-
-procedure TUIControl.keyEventPost (var ev: THKeyEvent);
+procedure TUIControl.keyEvent (var ev: TFUIEvent);
 begin
 end;
 
@@ -2060,12 +2297,15 @@ begin
   inherited;
   mFitToScreen := true;
   mFrameWidth := 8;
-  mFrameHeight := 8;
-  if (mWidth < mFrameWidth*2+3*8) then mWidth := mFrameWidth*2+3*8;
+  mFrameHeight := uiContext.charHeight(#184);
+  if (mWidth < mFrameWidth*2+uiContext.iconWinWidth(TGxContext.TWinIcon.Close)) then mWidth := mFrameWidth*2+uiContext.iconWinWidth(TGxContext.TWinIcon.Close);
   if (mHeight < mFrameHeight*2) then mHeight := mFrameHeight*2;
   if (Length(mTitle) > 0) then
   begin
-    if (mWidth < Length(mTitle)*8+mFrameWidth*2+3*8) then mWidth := Length(mTitle)*8+mFrameWidth*2+3*8;
+    if (mWidth < uiContext.textWidth(mTitle)+mFrameWidth*2+uiContext.iconWinWidth(TGxContext.TWinIcon.Close)) then
+    begin
+      mWidth := uiContext.textWidth(mTitle)+mFrameWidth*2+uiContext.iconWinWidth(TGxContext.TWinIcon.Close);
+    end;
   end;
   mCanFocus := false;
   mDragScroll := TXMode.None;
@@ -2074,6 +2314,8 @@ begin
   mInClose := false;
   closeCB := nil;
   mCtl4Style := 'window';
+  mDefSize.w := nmax(1, mDefSize.w);
+  mDefSize.h := nmax(1, mDefSize.h);
 end;
 
 
@@ -2124,62 +2366,57 @@ begin
 end;
 
 
+// ////////////////////////////////////////////////////////////////////////// //
 procedure TUITopWindow.drawControl (gx, gy: Integer);
 begin
-  fillRect(gx, gy, mWidth, mHeight, mBackColor[getColorIndex]);
+  uiContext.color := mBackColor[getColorIndex];
+  uiContext.fillRect(gx, gy, mWidth, mHeight);
 end;
 
-
 procedure TUITopWindow.drawControlPost (gx, gy: Integer);
 var
-  cidx: Integer;
-  tx, hgt, sbhgt: Integer;
+  cidx, iwdt, ihgt: Integer;
+  ybot, xend, vhgt, vwdt: Integer;
 begin
   cidx := getColorIndex;
+  iwdt := uiContext.iconWinWidth(TGxContext.TWinIcon.Close);
   if (mDragScroll = TXMode.Drag) then
   begin
-    drawRectUI(gx+4, gy+4, mWidth-8, mHeight-8, mFrameColor[cidx]);
+    drawFrame(gx, gy, iwdt, 0, mTitle, false);
   end
   else
   begin
-    drawRectUI(gx+3, gy+3, mWidth-6, mHeight-6, mFrameColor[cidx]);
-    drawRectUI(gx+5, gy+5, mWidth-10, mHeight-10, mFrameColor[cidx]);
+    ihgt := uiContext.iconWinHeight(TGxContext.TWinIcon.Close);
+    drawFrame(gx, gy, iwdt, 0, mTitle, true);
     // vertical scroll bar
-    hgt := mHeight-mFrameHeight*2;
-    if (hgt > 0) and (mFullSize.h > hgt) then
-    begin
-      //writeln(mTitle, ': height=', mHeight-mFrameHeight*2, '; fullsize=', mFullSize.toString);
-      sbhgt := mHeight-mFrameHeight*2+2;
-      fillRect(gx+mWidth-mFrameWidth+1, gy+7, mFrameWidth-3, sbhgt, mFrameColor[cidx]);
-      hgt += mScrollY;
-      if (hgt > mFullSize.h) then hgt := mFullSize.h;
-      hgt := sbhgt*hgt div mFullSize.h;
-      if (hgt > 0) then
-      begin
-        setScissor(mWidth-mFrameWidth+1, 7, mFrameWidth-3, sbhgt);
-        darkenRect(gx+mWidth-mFrameWidth+1, gy+7+hgt, mFrameWidth-3, sbhgt, 128);
-      end;
+    vhgt := mHeight-mFrameHeight*2;
+    if (mFullSize.h > vhgt) then
+    begin
+      ybot := mScrollY+vhgt;
+      resetScissorNC();
+      uiContext.drawVSBar(gx+mWidth-mFrameWidth+1, gy+mFrameHeight-1, mFrameWidth-3, vhgt+2, ybot, 0, mFullSize.h, mSBarFullColor[cidx], mSBarEmptyColor[cidx]);
+    end;
+    // horizontal scroll bar
+    vwdt := mWidth-mFrameWidth*2;
+    if (mFullSize.w > vwdt) then
+    begin
+      xend := mScrollX+vwdt;
+      resetScissorNC();
+      uiContext.drawHSBar(gx+mFrameWidth+1, gy+mHeight-mFrameHeight+1, vwdt-2, mFrameHeight-3, xend, 0, mFullSize.w, mSBarFullColor[cidx], mSBarEmptyColor[cidx]);
     end;
     // frame icon
-    setScissor(mFrameWidth, 0, 3*8, 8);
-    fillRect(gx+mFrameWidth, gy, 3*8, 8, mBackColor[cidx]);
-    drawText8(gx+mFrameWidth, gy, '[ ]', mFrameColor[cidx]);
-    if mInClose then drawText8(gx+mFrameWidth+7, gy, '#', mFrameIconColor[cidx])
-    else drawText8(gx+mFrameWidth+7, gy, '*', mFrameIconColor[cidx]);
+    setScissor(mFrameWidth, 0, iwdt, ihgt);
+    uiContext.color := mBackColor[cidx];
+    uiContext.fillRect(gx+mFrameWidth, gy, iwdt, ihgt);
+    uiContext.color := mFrameIconColor[cidx];
+    uiContext.drawIconWin(TGxContext.TWinIcon.Close, gx+mFrameWidth, gy, mInClose);
   end;
-  // title
-  if (Length(mTitle) > 0) then
-  begin
-    setScissor(mFrameWidth+3*8, 0, mWidth-mFrameWidth*2-3*8, 8);
-    tx := (gx+3*8)+((mWidth-3*8)-Length(mTitle)*8) div 2;
-    fillRect(tx-3, gy, Length(mTitle)*8+3+2, 8, mBackColor[cidx]);
-    drawText8(tx, gy, mTitle, mFrameTextColor[cidx]);
-  end;
-  // shadow
+  // shadow (no need to reset scissor, as draw should do it)
   inherited drawControlPost(gx, gy);
 end;
 
 
+// ////////////////////////////////////////////////////////////////////////// //
 procedure TUITopWindow.activated ();
 begin
   if (mFocused = nil) or (mFocused = self) then
@@ -2201,10 +2438,10 @@ begin
 end;
 
 
-procedure TUITopWindow.keyEvent (var ev: THKeyEvent);
+procedure TUITopWindow.keyEventBubble (var ev: TFUIEvent);
 begin
   inherited keyEvent(ev);
-  if (ev.eaten) or (ev.cancelled) or (not enabled) {or (not getFocused)} then exit;
+  if (not ev.alive) or (not enabled) {or (not getFocused)} then exit;
   if (ev = 'M-F3') then
   begin
     if (not assigned(closeRequestCB)) or (closeRequestCB(self)) then
@@ -2217,10 +2454,11 @@ begin
 end;
 
 
-procedure TUITopWindow.mouseEvent (var ev: THMouseEvent);
+procedure TUITopWindow.mouseEvent (var ev: TFUIEvent);
 var
   lx, ly: Integer;
-  hgt, sbhgt: Integer;
+  vhgt, ytop: Integer;
+  vwdt, xend: Integer;
 begin
   if (not enabled) then exit;
   if (mWidth < 1) or (mHeight < 1) then exit;
@@ -2236,26 +2474,23 @@ begin
     exit;
   end;
 
-  if (mDragScroll = TXMode.Scroll) then
+  if (mDragScroll = TXMode.VScroll) then
   begin
-    // check for vertical scrollbar
     ly := ev.y-mY;
-    if (ly < 7) then
-    begin
-      mScrollY := 0;
-    end
-    else
-    begin
-      sbhgt := mHeight-mFrameHeight*2+2;
-      hgt := mHeight-mFrameHeight*2;
-      if (hgt > 0) and (mFullSize.h > hgt) then
-      begin
-        hgt := (mFullSize.h*(ly-7) div (sbhgt-1))-(mHeight-mFrameHeight*2);
-        mScrollY := nmax(0, hgt);
-        hgt := mHeight-mFrameHeight*2;
-        if (mScrollY+hgt > mFullSize.h) then mScrollY := nmax(0, mFullSize.h-hgt);
-      end;
-    end;
+    vhgt := mHeight-mFrameHeight*2;
+    ytop := uiContext.sbarPos(ly, mFrameHeight-1, vhgt+2, 0, mFullSize.h)-vhgt;
+    mScrollY := nmax(0, ytop);
+    if (ev.release) and (ev.but = ev.Left) then mDragScroll := TXMode.None;
+    ev.eat();
+    exit;
+  end;
+
+  if (mDragScroll = TXMode.HScroll) then
+  begin
+    lx := ev.x-mX;
+    vwdt := mWidth-mFrameWidth*2;
+    xend := uiContext.sbarPos(lx, mFrameWidth+1, vwdt-2, 0, mFullSize.w)-vwdt;
+    mScrollX := nmax(0, xend);
     if (ev.release) and (ev.but = ev.Left) then mDragScroll := TXMode.None;
     ev.eat();
     exit;
@@ -2265,10 +2500,10 @@ begin
   begin
     if (ev.press) then
     begin
-      if (ly < 8) then
+      if (ly < mFrameHeight) then
       begin
         uiGrabCtl := self;
-        if (lx >= mFrameWidth) and (lx < mFrameWidth+3*8) then
+        if (lx >= mFrameWidth) and (lx < mFrameWidth+uiContext.iconWinWidth(TGxContext.TWinIcon.Close)) then
         begin
           //uiRemoveWindow(self);
           mWaitingClose := true;
@@ -2284,17 +2519,30 @@ begin
         exit;
       end;
       // check for vertical scrollbar
-      if (lx >= mWidth-mFrameWidth+1) and (ly >= 7) and (ly < mHeight-mFrameHeight+1) then
+      if (lx >= mWidth-mFrameWidth+1) and (ly >= mFrameHeight-1) and (ly < mHeight-mFrameHeight+2) then
+      begin
+        vhgt := mHeight-mFrameHeight*2;
+        if (mFullSize.h > vhgt) then
+        begin
+          uiGrabCtl := self;
+          mDragScroll := TXMode.VScroll;
+          ev.eat();
+          ytop := uiContext.sbarPos(ly, mFrameHeight-1, vhgt+2, 0, mFullSize.h)-vhgt;
+          mScrollY := nmax(0, ytop);
+          exit;
+        end;
+      end;
+      // check for horizontal scrollbar
+      if (ly >= mHeight-mFrameHeight+1) and (lx >= mFrameWidth+1) and (lx < mWidth-mFrameWidth-1) then
       begin
-        sbhgt := mHeight-mFrameHeight*2+2;
-        hgt := mHeight-mFrameHeight*2;
-        if (hgt > 0) and (mFullSize.h > hgt) then
+        vwdt := mWidth-mFrameWidth*2;
+        if (mFullSize.w > vwdt) then
         begin
-          hgt := (mFullSize.h*(ly-7) div (sbhgt-1))-(mHeight-mFrameHeight*2);
-          mScrollY := nmax(0, hgt);
           uiGrabCtl := self;
-          mDragScroll := TXMode.Scroll;
+          mDragScroll := TXMode.HScroll;
           ev.eat();
+          xend := uiContext.sbarPos(lx, mFrameWidth+1, vwdt-2, 0, mFullSize.w)-vwdt;
+          mScrollX := nmax(0, xend);
           exit;
         end;
       end;
@@ -2314,7 +2562,7 @@ begin
     begin
       if mWaitingClose then
       begin
-        if (lx >= mFrameWidth) and (lx < mFrameWidth+3*8) then
+        if (lx >= mFrameWidth) and (lx < mFrameWidth+uiContext.iconWinWidth(TGxContext.TWinIcon.Close)) then
         begin
           if (not assigned(closeRequestCB)) or (closeRequestCB(self)) then
           begin
@@ -2332,7 +2580,7 @@ begin
     begin
       if mWaitingClose then
       begin
-        mInClose := (lx >= mFrameWidth) and (lx < mFrameWidth+3*8);
+        mInClose := (lx >= mFrameWidth) and (lx < mFrameWidth+uiContext.iconWinWidth(TGxContext.TWinIcon.Close));
         ev.eat();
         exit;
       end;
@@ -2362,20 +2610,21 @@ begin
   mCanFocus := false;
   mHAlign := -1; // left
   mCtl4Style := 'box';
+  mDefSize := TLaySize.Create(-1, -1);
 end;
 
 
 procedure TUIBox.setCaption (const acap: AnsiString);
 begin
   mCaption := acap;
-  mDefSize := TLaySize.Create(Length(mCaption)*8+3, 8);
+  mDefSize := TLaySize.Create(uiContext.textWidth(mCaption)+3, uiContext.textHeight(mCaption));
 end;
 
 
 procedure TUIBox.setHasFrame (v: Boolean);
 begin
   mHasFrame := v;
-  if (mHasFrame) then begin mFrameWidth := 8; mFrameHeight := 8; end else begin mFrameWidth := 0; mFrameHeight := 0; end;
+  if (mHasFrame) then begin mFrameWidth := 8; mFrameHeight := uiContext.charHeight(#184); end else begin mFrameWidth := 0; mFrameHeight := 0; end;
   if (mHasFrame) then mNoPad := true;
 end;
 
@@ -2420,49 +2669,53 @@ end;
 procedure TUIBox.drawControl (gx, gy: Integer);
 var
   cidx: Integer;
-  xpos: Integer;
+  //xpos: Integer;
 begin
   cidx := getColorIndex;
-  fillRect(gx, gy, mWidth, mHeight, mBackColor[cidx]);
-  if mHasFrame then
+  uiContext.color := mBackColor[cidx];
+  uiContext.fillRect(gx, gy, mWidth, mHeight);
+  if (mHasFrame) then
   begin
     // draw frame
-    drawRectUI(gx+3, gy+3, mWidth-6, mHeight-6, mFrameColor[cidx]);
+    drawFrame(gx, gy, 0, mHAlign, mCaption, false);
   end;
-  // draw caption
-  if (Length(mCaption) > 0) then
+  // no frame -- no caption
+  {
+  else if (Length(mCaption) > 0) then
   begin
+    // draw caption
          if (mHAlign < 0) then xpos := 3
-    else if (mHAlign > 0) then xpos := mWidth-mFrameWidth*2-Length(mCaption)*8
-    else xpos := (mWidth-mFrameWidth*2-Length(mCaption)*8) div 2;
+    else if (mHAlign > 0) then xpos := mWidth-mFrameWidth*2-uiContext.textWidth(mCaption)
+    else xpos := (mWidth-mFrameWidth*2-uiContext.textWidth(mCaption)) div 2;
     xpos += gx+mFrameWidth;
 
-    setScissor(mFrameWidth+1, 0, mWidth-mFrameWidth-2, 8);
-    if mHasFrame then fillRect(xpos-3, gy, Length(mCaption)*8+4, 8, mBackColor[cidx]);
-    drawText8(xpos, gy, mCaption, mFrameTextColor[cidx]);
+    setScissor(mFrameWidth+1, 0, mWidth-mFrameWidth-2, uiContext.textHeight(mCaption));
+    uiContext.color := mFrameTextColor[cidx];
+    uiContext.drawText(xpos, gy, mCaption);
   end;
+  }
 end;
 
 
-procedure TUIBox.mouseEvent (var ev: THMouseEvent);
+procedure TUIBox.mouseEvent (var ev: TFUIEvent);
 var
   lx, ly: Integer;
 begin
   inherited mouseEvent(ev);
-  if (not ev.eaten) and (not ev.cancelled) and (enabled) and toLocal(ev.x, ev.y, lx, ly) then
+  if (ev.alive) and (enabled) and toLocal(ev.x, ev.y, lx, ly) then
   begin
     ev.eat();
   end;
 end;
 
 
-procedure TUIBox.keyEvent (var ev: THKeyEvent);
+procedure TUIBox.keyEvent (var ev: TFUIEvent);
 var
   dir: Integer = 0;
   cur, ctl: TUIControl;
 begin
   inherited keyEvent(ev);
-  if (ev.eaten) or (ev.cancelled) or (not ev.press) or (not enabled) or (not getActive) then exit;
+  if (not ev.alive) or (not ev.press) or (not enabled) or (not getActive) then exit;
   if (Length(mChildren) = 0) then exit;
        if (mHoriz) and (ev = 'Left') then dir := -1
   else if (mHoriz) and (ev = 'Right') then dir := 1
@@ -2516,6 +2769,7 @@ begin
   mCanFocus := false;
   mNoPad := true;
   mCtl4Style := 'span';
+  mDefSize := TLaySize.Create(-1, -1);
 end;
 
 
@@ -2526,11 +2780,6 @@ begin
 end;
 
 
-procedure TUISpan.drawControl (gx, gy: Integer);
-begin
-end;
-
-
 // ////////////////////////////////////////////////////////////////////// //
 procedure TUILine.AfterConstruction ();
 begin
@@ -2539,6 +2788,7 @@ begin
   mExpand := true;
   mCanFocus := false;
   mCtl4Style := 'line';
+  mDefSize := TLaySize.Create(-1, -1);
 end;
 
 
@@ -2549,37 +2799,31 @@ begin
 end;
 
 
-procedure TUILine.drawControl (gx, gy: Integer);
-var
-  cidx: Integer;
+procedure TUILine.layPrepare ();
 begin
-  cidx := getColorIndex;
-  if mHoriz then
+  inherited layPrepare();
+  if (mParent <> nil) then mHoriz := not mParent.mHoriz;
+  if (mHoriz) then
   begin
-    drawHLine(gx, gy+(mHeight div 2), mWidth, mTextColor[cidx]);
+    if (mLayDefSize.w < 0) then mLayDefSize.w := 1;
+    if (mLayDefSize.h < 0) then mLayDefSize.h := 7;
   end
   else
   begin
-    drawVLine(gx+(mWidth div 2), gy, mHeight, mTextColor[cidx]);
+    if (mLayDefSize.w < 0) then mLayDefSize.w := 7;
+    if (mLayDefSize.h < 0) then mLayDefSize.h := 1;
   end;
 end;
 
 
-// ////////////////////////////////////////////////////////////////////////// //
-procedure TUIHLine.AfterConstruction ();
-begin
-  inherited;
-  mHoriz := true;
-  mDefSize.h := 7;
-end;
-
-
-// ////////////////////////////////////////////////////////////////////////// //
-procedure TUIVLine.AfterConstruction ();
+procedure TUILine.drawControl (gx, gy: Integer);
+var
+  cidx: Integer;
 begin
-  inherited;
-  mHoriz := false;
-  mDefSize.w := 7;
+  cidx := getColorIndex;
+  uiContext.color := mTextColor[cidx];
+  if mHoriz then uiContext.hline(gx, gy+(mHeight div 2), mWidth)
+  else uiContext.vline(gx+(mWidth div 2), gy, mHeight);
 end;
 
 
@@ -2593,7 +2837,6 @@ begin
   mHoriz := true; // nobody cares
   mHeader := false;
   mLine := false;
-  mDefSize.h := 8;
   mCtl4Style := 'static';
 end;
 
@@ -2601,7 +2844,7 @@ end;
 procedure TUIStaticText.setText (const atext: AnsiString);
 begin
   mText := atext;
-  mDefSize := TLaySize.Create(Length(mText)*8, 8);
+  mDefSize := TLaySize.Create(uiContext.textWidth(mText), uiContext.textHeight(mText));
 end;
 
 
@@ -2639,29 +2882,29 @@ procedure TUIStaticText.drawControl (gx, gy: Integer);
 var
   xpos, ypos: Integer;
   cidx: Integer;
-  clr: TGxRGBA;
 begin
   cidx := getColorIndex;
-  fillRect(gx, gy, mWidth, mHeight, mBackColor[cidx]);
+  uiContext.color := mBackColor[cidx];
+  uiContext.fillRect(gx, gy, mWidth, mHeight);
 
        if (mHAlign < 0) then xpos := 0
-  else if (mHAlign > 0) then xpos := mWidth-Length(mText)*8
-  else xpos := (mWidth-Length(mText)*8) div 2;
+  else if (mHAlign > 0) then xpos := mWidth-uiContext.textWidth(mText)
+  else xpos := (mWidth-uiContext.textWidth(mText)) div 2;
 
   if (Length(mText) > 0) then
   begin
-    if (mHeader) then clr := mFrameTextColor[cidx] else clr := mTextColor[cidx];
+    if (mHeader) then uiContext.color := mFrameTextColor[cidx] else uiContext.color := mTextColor[cidx];
 
          if (mVAlign < 0) then ypos := 0
-    else if (mVAlign > 0) then ypos := mHeight-8
-    else ypos := (mHeight-8) div 2;
+    else if (mVAlign > 0) then ypos := mHeight-uiContext.textHeight(mText)
+    else ypos := (mHeight-uiContext.textHeight(mText)) div 2;
 
-    drawText8(gx+xpos, gy+ypos, mText, clr);
+    uiContext.drawText(gx+xpos, gy+ypos, mText);
   end;
 
   if (mLine) then
   begin
-    if (mHeader) then clr := mFrameColor[cidx] else clr := mTextColor[cidx];
+    if (mHeader) then uiContext.color := mFrameColor[cidx] else uiContext.color := mTextColor[cidx];
 
          if (mVAlign < 0) then ypos := 0
     else if (mVAlign > 0) then ypos := mHeight-1
@@ -2670,12 +2913,12 @@ begin
 
     if (Length(mText) = 0) then
     begin
-      drawHLine(gx, ypos, mWidth, clr);
+      uiContext.hline(gx, ypos, mWidth);
     end
     else
     begin
-      drawHLine(gx, ypos, xpos-1, clr);
-      drawHLine(gx+xpos+Length(mText)*8, ypos, mWidth, clr);
+      uiContext.hline(gx, ypos, xpos-1);
+      uiContext.hline(gx+xpos+uiContext.textWidth(mText), ypos, mWidth);
     end;
   end;
 end;
@@ -2688,7 +2931,6 @@ begin
   mHAlign := -1;
   mVAlign := 0;
   mCanFocus := false;
-  mDefSize := TLaySize.Create(Length(mText)*8, 8);
   mCtl4Style := 'label';
   mLinkId := '';
 end;
@@ -2730,7 +2972,7 @@ begin
         if (mHotChar = #0) then
         begin
           mHotChar := s[f];
-          mHotOfs := Length(mText)*8;
+          mHotOfs := Length(mText);
         end;
         mText += s[f];
       end;
@@ -2742,7 +2984,13 @@ begin
       Inc(f);
     end;
   end;
-  mDefSize := TLaySize.Create(Length(mText)*8, 8);
+  // fix hotchar offset
+  if (mHotChar <> #0) and (mHotOfs > 0) then
+  begin
+    mHotOfs := uiContext.textWidth(Copy(mText, 1, mHotOfs+1))-uiContext.charWidth(mText[mHotOfs+1]);
+  end;
+  // fix size
+  mDefSize := TLaySize.Create(uiContext.textWidth(mText), uiContext.textHeight(mText));
 end;
 
 
@@ -2776,33 +3024,36 @@ var
   cidx: Integer;
 begin
   cidx := getColorIndex;
-  fillRect(gx, gy, mWidth, mHeight, mBackColor[cidx]);
+  uiContext.color := mBackColor[cidx];
+  uiContext.fillRect(gx, gy, mWidth, mHeight);
   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;
+    else if (mHAlign > 0) then xpos := mWidth-uiContext.textWidth(mText)
+    else xpos := (mWidth-uiContext.textWidth(mText)) div 2;
 
          if (mVAlign < 0) then ypos := 0
-    else if (mVAlign > 0) then ypos := mHeight-8
-    else ypos := (mHeight-8) div 2;
+    else if (mVAlign > 0) then ypos := mHeight-uiContext.textHeight(mText)
+    else ypos := (mHeight-uiContext.textHeight(mText)) div 2;
 
-    drawText8(gx+xpos, gy+ypos, mText, mTextColor[cidx]);
+    uiContext.color := mTextColor[cidx];
+    uiContext.drawText(gx+xpos, gy+ypos, mText);
 
     if (Length(mLinkId) > 0) and (mHotChar <> #0) and (mHotChar <> ' ') then
     begin
-      drawText8(gx+xpos+8+mHotOfs, gy+ypos, mHotChar, mHotColor[cidx]);
+      uiContext.color := mHotColor[cidx];
+      uiContext.drawChar(gx+xpos+mHotOfs, gy+ypos, mHotChar);
     end;
   end;
 end;
 
 
-procedure TUITextLabel.mouseEvent (var ev: THMouseEvent);
+procedure TUITextLabel.mouseEvent (var ev: TFUIEvent);
 var
   lx, ly: Integer;
 begin
   inherited mouseEvent(ev);
-  if (not ev.eaten) and (not ev.cancelled) and (enabled) and toLocal(ev.x, ev.y, lx, ly) then
+  if (ev.alive) and (enabled) and toLocal(ev.x, ev.y, lx, ly) then
   begin
     ev.eat();
   end;
@@ -2828,11 +3079,11 @@ begin
 end;
 
 
-procedure TUITextLabel.keyEventPost (var ev: THKeyEvent);
+procedure TUITextLabel.keyEventBubble (var ev: TFUIEvent);
 begin
   if (not enabled) then exit;
   if (mHotChar = #0) then exit;
-  if (ev.eaten) or (ev.cancelled) or (not ev.press) then exit;
+  if (not ev.alive) or (not ev.press) then exit;
   if (ev.kstate <> ev.ModAlt) then exit;
   if (not ev.isHot(mHotChar)) then exit;
   ev.eat();
@@ -2845,51 +3096,193 @@ end;
 procedure TUIButton.AfterConstruction ();
 begin
   inherited;
-  mHAlign := -1;
+  mHAlign := 0;
   mVAlign := 0;
+  mShadowSize := 0;
   mCanFocus := true;
-  mDefSize := TLaySize.Create(Length(mText)*8+8, 10);
+  mDefSize := TLaySize.Create(uiContext.textWidth(mText)+uiContext.textWidth('[  ]'), uiContext.textHeight(mText));
   mCtl4Style := 'button';
+  mSkipLayPrepare := false;
+  mAddMarkers := false;
+  mHideMarkers := false;
+end;
+
+
+procedure TUIButton.cacheStyle (root: TUIStyle);
+var
+  sz: Integer = 0;
+begin
+  inherited cacheStyle(root);
+  // shadow size
+  sz := nmax(0, root.get('shadow-size', 'active', mCtl4Style).asInt(0));
+  sz := nmax(sz, root.get('shadow-size', 'disabled', mCtl4Style).asInt(0));
+  sz := nmax(sz, root.get('shadow-size', 'inactive', mCtl4Style).asInt(0));
+  mShadowSize := sz;
+  // markers mode
+  mAddMarkers := root.get('add-markers', 'active', mCtl4Style).asBool(false);
+  mAddMarkers := mAddMarkers or root.get('add-markers', 'disabled', mCtl4Style).asBool(false);
+  mAddMarkers := mAddMarkers or root.get('add-markers', 'inactive', mCtl4Style).asBool(false);
+  // hide markers?
+  mHideMarkers := root.get('hide-markers', 'active', mCtl4Style).asBool(false);
+  mHideMarkers := mHideMarkers or root.get('hide-markers', 'disabled', mCtl4Style).asBool(false);
+  mHideMarkers := mHideMarkers or root.get('hide-markers', 'inactive', mCtl4Style).asBool(false);
 end;
 
 
 procedure TUIButton.setText (const s: AnsiString);
 begin
   inherited setText(s);
-  mDefSize := TLaySize.Create(Length(mText)*8+8*2, 10);
+  if (mHideMarkers) then
+  begin
+    mDefSize := TLaySize.Create(uiContext.textWidth(mText)+10, uiContext.textHeight(mText));
+  end
+  else if (mAddMarkers) then
+  begin
+    mDefSize := TLaySize.Create(uiContext.textWidth(mText)+uiContext.textWidth('[<>]'), uiContext.textHeight(mText));
+  end
+  else
+  begin
+    mDefSize := TLaySize.Create(uiContext.textWidth(mText)+uiContext.textWidth('<>'), uiContext.textHeight(mText));
+  end;
+end;
+
+
+procedure TUIButton.layPrepare ();
+var
+  ods: TLaySize;
+  ww: Integer;
+begin
+  if (not mSkipLayPrepare) then
+  begin
+    ods := mDefSize;
+    if (ods.w <> 0) or (ods.h <> 0) then
+    begin
+      mDefSize := TLaySize.Create(uiContext.textWidth(mText), uiContext.textHeight(mText));
+      if (mHideMarkers) then
+      begin
+        ww := 10;
+      end
+      else if (mAddMarkers) then
+      begin
+             if (mDefault) then ww := uiContext.textWidth('[<  >]')
+        else if (mCancel) then ww := uiContext.textWidth('[{  }]')
+        else ww := uiContext.textWidth('[  ]');
+      end
+      else
+      begin
+        ww := nmax(0, uiContext.textWidth('<  >'));
+        ww := nmax(ww, uiContext.textWidth('{  }'));
+        ww := nmax(ww, uiContext.textWidth('[  ]'));
+      end;
+      mDefSize.w += ww+mShadowSize;
+      mDefSize.h += mShadowSize;
+    end;
+  end
+  else
+  begin
+    ods := TLaySize.Create(0, 0); // fpc is dumb!
+  end;
+  inherited layPrepare();
+  if (not mSkipLayPrepare) then mDefSize := ods;
+end;
+
+
+procedure TUIButton.blurred ();
+begin
+  mPushed := false;
 end;
 
 
 procedure TUIButton.drawControl (gx, gy: Integer);
 var
-  xpos, ypos: Integer;
+  wdt, hgt: Integer;
+  xpos, ypos, xofsl, xofsr, sofs: Integer;
   cidx: Integer;
+  lch, rch: AnsiChar;
+  lstr, rstr: AnsiString;
 begin
   cidx := getColorIndex;
 
-  fillRect(gx+1, gy, mWidth-2, mHeight, mBackColor[cidx]);
-  fillRect(gx, gy+1, 1, mHeight-2, mBackColor[cidx]);
-  fillRect(gx+mWidth-1, gy+1, 1, mHeight-2, mBackColor[cidx]);
+  wdt := mWidth-mShadowSize;
+  hgt := mHeight-mShadowSize;
+  if (mPushed) {or (cidx = ClrIdxActive)} then
+  begin
+    sofs := mShadowSize;
+    gx += mShadowSize;
+    gy += mShadowSize;
+  end
+  else
+  begin
+    sofs := 0;
+    if (mShadowSize > 0) then
+    begin
+      uiContext.darkenRect(gx+mShadowSize, gy+hgt, wdt, mShadowSize, 96);
+      uiContext.darkenRect(gx+wdt, gy+mShadowSize, mShadowSize, hgt-mShadowSize, 96);
+    end;
+  end;
+
+  uiContext.color := mBackColor[cidx];
+  uiContext.fillRect(gx, gy, wdt, hgt);
 
-  if (Length(mText) > 0) then
+       if (mVAlign < 0) then ypos := 0
+  else if (mVAlign > 0) then ypos := hgt-uiContext.textHeight(mText)
+  else ypos := (hgt-uiContext.textHeight(mText)) div 2;
+  ypos += gy;
+
+  uiContext.color := mTextColor[cidx];
+
+  if (mHideMarkers) 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;
+    xofsl := 5;
+    xofsr := 5;
+  end
+  else
+  begin
+    if (mAddMarkers) then
+    begin
+           if (mDefault) then begin lstr := '[< '; rstr := ' >]'; end
+      else if (mCancel) then begin lstr := '[{ '; rstr := ' }]'; end
+      else begin lstr := '[ '; rstr := ' ]'; end;
+      xofsl := uiContext.textWidth(lstr);
+      xofsr := uiContext.textWidth(rstr);
+      uiContext.drawText(gx, ypos, lstr);
+      uiContext.drawText(gx+wdt-uiContext.textWidth(rstr), ypos, rstr);
+    end
+    else
+    begin
+      xofsl := nmax(0, uiContext.textWidth('< '));
+      xofsl := nmax(xofsl, uiContext.textWidth('{ '));
+      xofsl := nmax(xofsl, uiContext.textWidth('[ '));
+      xofsr := nmax(0, uiContext.textWidth(' >'));
+      xofsr := nmax(xofsr, uiContext.textWidth(' }'));
+      xofsr := nmax(xofsr, uiContext.textWidth(' ]'));
+           if (mDefault) then begin lch := '<'; rch := '>'; end
+      else if (mCancel) then begin lch := '{'; rch := '}'; end
+      else begin lch := '['; rch := ']'; end;
+      uiContext.drawChar(gx, ypos, lch);
+      uiContext.drawChar(gx+wdt-uiContext.charWidth(rch), ypos, rch);
+    end;
+  end;
 
-         if (mVAlign < 0) then ypos := 0
-    else if (mVAlign > 0) then ypos := mHeight-8
-    else ypos := (mHeight-8) div 2;
+  if (Length(mText) > 0) then
+  begin
+    if (mHAlign < 0) then xpos := 0
+    else begin xpos := wdt-xofsl-xofsr-uiContext.textWidth(mText); if (mHAlign = 0) then xpos := xpos div 2; end;
+    xpos += xofsl;
 
-    setScissor(8, 0, mWidth-16, mHeight);
-    drawText8(gx+xpos+8, gy+ypos, mText, mTextColor[cidx]);
+    setScissor(sofs+xofsl, sofs, wdt-xofsl-xofsr, hgt);
+    uiContext.drawText(gx+xpos, ypos, mText);
 
-    if (mHotChar <> #0) and (mHotChar <> ' ') then drawText8(gx+xpos+8+mHotOfs, gy+ypos, mHotChar, mHotColor[cidx]);
+    if (mHotChar <> #0) and (mHotChar <> ' ') then
+    begin
+      uiContext.color := mHotColor[cidx];
+      uiContext.drawChar(gx+xpos+mHotOfs, ypos, mHotChar);
+    end;
   end;
 end;
 
 
-procedure TUIButton.mouseEvent (var ev: THMouseEvent);
+procedure TUIButton.mouseEvent (var ev: TFUIEvent);
 var
   lx, ly: Integer;
 begin
@@ -2897,32 +3290,119 @@ begin
   if (uiGrabCtl = self) then
   begin
     ev.eat();
-    if (ev = '-lmb') and focused and toLocal(ev.x, ev.y, lx, ly) then
+    mPushed := toLocal(ev.x, ev.y, lx, ly);
+    if (ev = '-lmb') and (focused) and (mPushed) then
     begin
+      mPushed := false;
       doAction();
     end;
     exit;
   end;
-  if (ev.eaten) or (ev.cancelled) or (not enabled) or not focused then exit;
+  if (not ev.alive) or (not enabled) or (not focused) then exit;
+  mPushed := true;
   ev.eat();
 end;
 
 
-procedure TUIButton.keyEvent (var ev: THKeyEvent);
+procedure TUIButton.keyEvent (var ev: TFUIEvent);
 begin
   inherited keyEvent(ev);
-  if (not ev.eaten) and (not ev.cancelled) and (enabled) then
+  if (ev.alive) and (enabled) then
   begin
-    if (ev = 'Enter') or (ev = 'Space') then
+    if (ev = '+Enter') or (ev = '+Space') then
     begin
+      focused := true;
+      mPushed := true;
       ev.eat();
-      doAction();
+      exit;
+    end;
+    if (focused) and ((ev = '-Enter') or (ev = '-Space')) then
+    begin
+      if (mPushed) then
+      begin
+        mPushed := false;
+        ev.eat();
+        doAction();
+      end
+      else
+      begin
+        ev.eat();
+      end;
       exit;
     end;
   end;
 end;
 
 
+// ////////////////////////////////////////////////////////////////////////// //
+procedure TUIButtonRound.AfterConstruction ();
+begin
+  inherited;
+  mHAlign := -1;
+  mVAlign := 0;
+  mCanFocus := true;
+  mDefSize := TLaySize.Create(uiContext.textWidth(mText)+8*2, uiContext.textHeight(mText)+2);
+  mCtl4Style := 'button-round';
+  mSkipLayPrepare := true;
+end;
+
+
+procedure TUIButtonRound.setText (const s: AnsiString);
+begin
+  inherited setText(s);
+  mDefSize := TLaySize.Create(uiContext.textWidth(mText)+8*2, uiContext.textHeight(mText)+2);
+end;
+
+
+procedure TUIButtonRound.layPrepare ();
+var
+  ods: TLaySize;
+begin
+  ods := mDefSize;
+  if (ods.w <> 0) or (ods.h <> 0) then
+  begin
+    mDefSize := TLaySize.Create(uiContext.textWidth(mText)+8*2, uiContext.textHeight(mText)+2);
+  end;
+  inherited layPrepare();
+  mDefSize := ods;
+end;
+
+
+procedure TUIButtonRound.drawControl (gx, gy: Integer);
+var
+  xpos, ypos: Integer;
+  cidx: Integer;
+begin
+  cidx := getColorIndex;
+
+  uiContext.color := mBackColor[cidx];
+  uiContext.fillRect(gx+1, gy, mWidth-2, mHeight);
+  uiContext.fillRect(gx, gy+1, 1, mHeight-2);
+  uiContext.fillRect(gx+mWidth-1, gy+1, 1, mHeight-2);
+
+  if (Length(mText) > 0) then
+  begin
+         if (mHAlign < 0) then xpos := 0
+    else if (mHAlign > 0) then xpos := mWidth-uiContext.textWidth(mText)
+    else xpos := (mWidth-uiContext.textWidth(mText)) div 2;
+
+         if (mVAlign < 0) then ypos := 0
+    else if (mVAlign > 0) then ypos := mHeight-uiContext.textHeight(mText)
+    else ypos := (mHeight-uiContext.textHeight(mText)) div 2;
+
+    setScissor(8, 0, mWidth-16, mHeight);
+    uiContext.color := mTextColor[cidx];
+    uiContext.drawText(gx+xpos+8, gy+ypos, mText);
+
+    if (mHotChar <> #0) and (mHotChar <> ' ') then
+    begin
+      uiContext.color := mHotColor[cidx];
+      uiContext.drawChar(gx+xpos+8+mHotOfs, gy+ypos, mHotChar);
+    end;
+  end;
+end;
+
+
 // ////////////////////////////////////////////////////////////////////////// //
 procedure TUISwitchBox.AfterConstruction ();
 begin
@@ -2930,7 +3410,8 @@ begin
   mHAlign := -1;
   mVAlign := 0;
   mCanFocus := true;
-  mDefSize := TLaySize.Create(Length(mText)*8+8*3, 8);
+  mIcon := TGxContext.TMarkIcon.Checkbox;
+  mDefSize := TLaySize.Create(uiContext.textWidth(mText)+3+uiContext.iconMarkWidth(mIcon), nmax(uiContext.iconMarkHeight(mIcon), uiContext.textHeight(mText)));
   mCtl4Style := 'switchbox';
   mChecked := false;
   mBoolVar := @mChecked;
@@ -2952,7 +3433,7 @@ end;
 procedure TUISwitchBox.setText (const s: AnsiString);
 begin
   inherited setText(s);
-  mDefSize := TLaySize.Create(Length(mText)*8+8*3, 8);
+  mDefSize := TLaySize.Create(uiContext.textWidth(mText)+3+uiContext.iconMarkWidth(mIcon), nmax(uiContext.iconMarkHeight(mIcon), uiContext.textHeight(mText)));
 end;
 
 
@@ -2987,47 +3468,50 @@ end;
 
 procedure TUISwitchBox.drawControl (gx, gy: Integer);
 var
-  xpos, ypos: Integer;
+  xpos, ypos, iwdt, dy: Integer;
   cidx: Integer;
 begin
   cidx := getColorIndex;
 
+  iwdt := uiContext.iconMarkWidth(mIcon);
        if (mHAlign < 0) then xpos := 0
-  else if (mHAlign > 0) then xpos := mWidth-(Length(mText)+4)*8
-  else xpos := (mWidth-(Length(mText)+4)*8) div 2;
+  else if (mHAlign > 0) then xpos := mWidth-(uiContext.textWidth(mText)+3+iwdt)
+  else xpos := (mWidth-(uiContext.textWidth(mText)+3+iwdt)) div 2;
 
        if (mVAlign < 0) then ypos := 0
-  else if (mVAlign > 0) then ypos := mHeight-8
-  else ypos := (mHeight-8) div 2;
+  else if (mVAlign > 0) then ypos := mHeight-uiContext.textHeight(mText)
+  else ypos := (mHeight-uiContext.textHeight(mText)) div 2;
 
+  uiContext.color := mBackColor[cidx];
+  uiContext.fillRect(gx, gy, mWidth, mHeight);
 
-  fillRect(gx, gy, mWidth, mHeight, mBackColor[cidx]);
-
-  if (checked) then
+  uiContext.color := mSwitchColor[cidx];
+  if (uiContext.iconMarkHeight(mIcon) < uiContext.textHeight(mText)) then
   begin
-    if (Length(mCheckedStr) <> 3) or (mCheckedStr[2] <> '*') then
-    begin
-      drawText8(gx+xpos, gy+ypos, mCheckedStr, mSwitchColor[cidx]);
-    end
-    else
-    begin
-      drawText8(gx+xpos, gy+ypos, mCheckedStr[1], mSwitchColor[cidx]);
-      drawText8(gx+xpos+2*8, gy+ypos, mCheckedStr[3], mSwitchColor[cidx]);
-      drawText8(gx+xpos+7, gy+ypos, '*', mSwitchColor[cidx]);
+    case uiContext.textHeight(mText) of
+      14: dy := 2;
+      16: dy := 3;
+      else dy := 1;
     end;
+    uiContext.drawIconMark(mIcon, gx, gy+ypos+uiContext.textHeight(mText)-uiContext.iconMarkHeight(mIcon)-dy, checked);
   end
   else
   begin
-    drawText8(gx+xpos, gy+ypos, mUncheckedStr, mSwitchColor[cidx]);
+    uiContext.drawIconMark(mIcon, gx, gy, checked);
   end;
 
-  drawText8(gx+xpos+8*3, gy+ypos, mText, mTextColor[cidx]);
+  uiContext.color := mTextColor[cidx];
+  uiContext.drawText(gx+xpos+3+iwdt, gy+ypos, mText);
 
-  if (mHotChar <> #0) and (mHotChar <> ' ') then drawText8(gx+xpos+8*3+mHotOfs, gy+ypos, mHotChar, mHotColor[cidx]);
+  if (mHotChar <> #0) and (mHotChar <> ' ') then
+  begin
+    uiContext.color := mHotColor[cidx];
+    uiContext.drawChar(gx+xpos+3+iwdt+mHotOfs, gy+ypos, mHotChar);
+  end;
 end;
 
 
-procedure TUISwitchBox.mouseEvent (var ev: THMouseEvent);
+procedure TUISwitchBox.mouseEvent (var ev: TFUIEvent);
 var
   lx, ly: Integer;
 begin
@@ -3041,15 +3525,15 @@ begin
     end;
     exit;
   end;
-  if (ev.eaten) or (ev.cancelled) or (not enabled) or not focused then exit;
+  if (not ev.alive) or (not enabled) or not focused then exit;
   ev.eat();
 end;
 
 
-procedure TUISwitchBox.keyEvent (var ev: THKeyEvent);
+procedure TUISwitchBox.keyEvent (var ev: TFUIEvent);
 begin
   inherited keyEvent(ev);
-  if (not ev.eaten) and (not ev.cancelled) and (enabled) then
+  if (ev.alive) and (enabled) then
   begin
     if (ev = 'Space') then
     begin
@@ -3067,8 +3551,8 @@ begin
   inherited;
   mChecked := false;
   mBoolVar := @mChecked;
-  mCheckedStr := '[x]';
-  mUncheckedStr := '[ ]';
+  mIcon := TGxContext.TMarkIcon.Checkbox;
+  setText('');
 end;
 
 
@@ -3097,9 +3581,9 @@ begin
   inherited;
   mChecked := false;
   mBoolVar := @mChecked;
-  mCheckedStr := '(*)';
-  mUncheckedStr := '( )';
   mRadioGroup := '';
+  mIcon := TGxContext.TMarkIcon.Radiobox;
+  setText('');
 end;
 
 
@@ -3153,15 +3637,27 @@ end;
 
 
 // ////////////////////////////////////////////////////////////////////////// //
+var
+  oldFocus: procedure () = nil;
+  oldBlur: procedure () = nil;
+
+procedure onWinFocus (); begin uiFocus(); if (assigned(oldFocus)) then oldFocus(); end;
+procedure onWinBlur (); begin fuiResetKMState(true); uiBlur(); if (assigned(oldBlur)) then oldBlur(); end;
+
 initialization
   registerCtlClass(TUIHBox, 'hbox');
   registerCtlClass(TUIVBox, 'vbox');
   registerCtlClass(TUISpan, 'span');
-  registerCtlClass(TUIHLine, 'hline');
-  registerCtlClass(TUIVLine, 'vline');
+  registerCtlClass(TUILine, 'line');
   registerCtlClass(TUITextLabel, 'label');
   registerCtlClass(TUIStaticText, 'static');
+  registerCtlClass(TUIButtonRound, 'round-button');
   registerCtlClass(TUIButton, 'button');
   registerCtlClass(TUICheckBox, 'checkbox');
   registerCtlClass(TUIRadioBox, 'radiobox');
+
+  oldFocus := winFocusCB;
+  oldBlur := winBlurCB;
+  winFocusCB := onWinFocus;
+  winBlurCB := onWinBlur;
 end.