Sfoglia il codice sorgente

Merge branch 'main' of gitlab.com:freepascal.org/lazarus/fresnel

Michaël Van Canneyt 11 mesi fa
parent
commit
939e888c06
89 ha cambiato i file con 8691 aggiunte e 1745 eliminazioni
  1. BIN
      bin/Binary/Static/OSX64/libsk4d.a
  2. BIN
      bin/Binary/Static/OSXARM64/libsk4d.a
  3. 1 1
      demo/Button/MainUnit.lfm
  4. 5 3
      demo/ButtonGenerator/ButtonGenerator.lpi
  5. 1 1
      demo/ButtonGenerator/ButtonGenerator.lpr
  6. 24 19
      demo/ButtonGenerator/DemoButtonGenerator.pas
  7. 0 2
      demo/ButtonGenerator/MainUnit.pas
  8. 0 8
      demo/CheckBox/CustomCheckBox.lpi
  9. 1 1
      demo/CheckBox/CustomCheckBox.lpr
  10. BIN
      demo/CheckBox/DemoCheckBox.res
  11. 5 0
      demo/CheckBox/MainUnit.lfm
  12. 1 3
      demo/CheckBox/MainUnit.pas
  13. 1 1
      demo/CheckBox/fresnel.democheckbox.inc
  14. 15 18
      demo/CheckBox/fresnel.democheckbox.pas
  15. 1 0
      demo/Combobox/.gitignore
  16. BIN
      demo/Combobox/ArrowDown.png
  17. 75 0
      demo/Combobox/ComboboxNative.lpi
  18. 16 0
      demo/Combobox/ComboboxNative.lpr
  19. 17 0
      demo/Combobox/MainUnit.lfm
  20. 57 0
      demo/Combobox/MainUnit.pas
  21. 43 0
      demo/Combobox/fresnel.democombobox.inc
  22. 289 0
      demo/Combobox/fresnel.democombobox.pas
  23. 9 0
      demo/Image/MainUnit.pas
  24. 0 4
      demo/Slider/CustomSlider.lpi
  25. 1 2
      demo/Slider/CustomSlider.lpr
  26. 2 4
      demo/Slider/MainUnit.pas
  27. 34 30
      demo/Slider/fresnel.demoslider.pas
  28. 22 0
      demo/democomps/DemoCompsReg.pas
  29. BIN
      demo/democomps/DemoCompsReg.res
  30. 58 0
      demo/democomps/fresneldemocomps.lpk
  31. 22 0
      demo/democomps/fresneldemocomps.pas
  32. 3 0
      demo/democomps/images/README.txt
  33. BIN
      demo/democomps/images/fresnel.democheckbox.tdemocheckbox.png
  34. BIN
      demo/democomps/images/fresnel.democombobox.tdemocombobox.png
  35. BIN
      demo/democomps/images/fresnel.demoslider.tdemoslider.png
  36. 4 3
      demo/lcl/ButtonGenerator/ButtonGeneratorLCLDemo.lpi
  37. 1 1
      demo/lcl/ButtonGenerator/ButtonGeneratorLCLDemo.lpr
  38. 0 2
      demo/lcl/ButtonGenerator/FresForm1.pas
  39. 1 0
      demo/lcl/CSSViewer/.gitignore
  40. BIN
      demo/lcl/CSSViewer/DemoLCLCSSView.ico
  41. 90 0
      demo/lcl/CSSViewer/DemoLCLCSSView.lpi
  42. 27 0
      demo/lcl/CSSViewer/DemoLCLCSSView.lpr
  43. BIN
      demo/lcl/CSSViewer/DemoLCLCSSView.res
  44. 8 0
      demo/lcl/CSSViewer/fresnel.cssview.lfm
  45. 134 0
      demo/lcl/CSSViewer/fresnel.cssview.pas
  46. 9 0
      demo/lcl/CSSViewer/mainunit.lfm
  47. 45 0
      demo/lcl/CSSViewer/mainunit.pas
  48. 21 0
      demo/lcl/CSSViewer/unit1.lfm
  49. 32 0
      demo/lcl/CSSViewer/unit1.pas
  50. 140 0
      design/fresnel.dsgnoptions.pas
  51. 25 0
      design/fresnel.dsgnoptsframe.lfm
  52. 68 0
      design/fresnel.dsgnoptsframe.pas
  53. 2 0
      design/fresnel.dsgnstrconsts.pas
  54. 119 11
      design/fresnel.register.pas
  55. 8 0
      design/fresneldsgn.lpk
  56. 2 2
      design/fresneldsgn.pas
  57. 139 72
      src/base/fcl-css/fpcssparser.pp
  58. 658 264
      src/base/fcl-css/fpcssresolver.pas
  59. 2694 0
      src/base/fcl-css/fpcssresparser.pas
  60. 163 124
      src/base/fcl-css/fpcssscanner.pp
  61. 198 13
      src/base/fcl-css/fpcsstree.pp
  62. 26 4
      src/base/fresnel.classes.pas
  63. 114 224
      src/base/fresnel.controls.pas
  64. 736 135
      src/base/fresnel.dom.pas
  65. 13 11
      src/base/fresnel.events.pas
  66. 42 66
      src/base/fresnel.forms.pas
  67. 662 539
      src/base/fresnel.layouter.pas
  68. 30 82
      src/base/fresnel.renderer.pas
  69. 37 20
      src/base/fresnel.textlayouter.pas
  70. 9 0
      src/base/fresnelbase.lpk
  71. 3 2
      src/base/fresnelbase.pas
  72. 20 2
      src/base/utf8utils.pp
  73. 3 0
      src/gtk3/fresnel.gtk3.pas
  74. 18 6
      src/lcl/fresnel.lcl.pas
  75. 0 3
      src/lcl/fresnel.lclcontrols.pas
  76. 6 8
      src/lcl/fresnel.lclevents.pas
  77. 1 0
      src/pas2js/fresnel.pas2js.wasmapi.pp
  78. 1 1
      src/pas2js/p2jsfresnelapi.lpk
  79. 19 9
      src/skia/fresnel.skiarenderer.pas
  80. 16 4
      src/wasm/fresnel.wasm.font.pp
  81. 1 1
      src/wasm/fresnelwasm.lpk
  82. 349 0
      tests/base/TCFlexLayout.pas
  83. 538 0
      tests/base/TCFlowLayout.pas
  84. 52 4
      tests/base/TCFresnelBaseEvents.pas
  85. 607 15
      tests/base/TCFresnelCSS.pas
  86. 9 1
      tests/base/TestFresnelBase.lpi
  87. 2 1
      tests/base/TestFresnelBase.lpr
  88. 83 13
      tests/base/tcfresnelimages.pas
  89. 3 5
      tests/base/tctextlayout.pas

BIN
bin/Binary/Static/OSX64/libsk4d.a


BIN
bin/Binary/Static/OSXARM64/libsk4d.a


+ 1 - 1
demo/Button/MainUnit.lfm

@@ -24,7 +24,7 @@ object MainForm: TMainForm
         OnMouseDown = Label1MouseDown
         OnMouseMove = Label1MouseMove
         OnMouseUp = Label1MouseUp
-        Caption = 'Labgl1'
+        Caption = 'Label1'
       end
     end
   end

+ 5 - 3
demo/ButtonGenerator/ButtonGenerator.lpi

@@ -43,16 +43,18 @@
         <ResourceBaseClassname Value="TFresnelForm"/>
       </Unit>
       <Unit>
-        <Filename Value="../Slider/DemoSlider.pas"/>
+        <Filename Value="DemoButtonGenerator.pas"/>
         <IsPartOfProject Value="True"/>
       </Unit>
       <Unit>
-        <Filename Value="../CheckBox/DemoCheckBox.pas"/>
+        <Filename Value="../Slider/fresnel.demoslider.pas"/>
         <IsPartOfProject Value="True"/>
+        <UnitName Value="Fresnel.DemoSlider"/>
       </Unit>
       <Unit>
-        <Filename Value="DemoButtonGenerator.pas"/>
+        <Filename Value="../CheckBox/fresnel.democheckbox.pas"/>
         <IsPartOfProject Value="True"/>
+        <UnitName Value="Fresnel.DemoCheckbox"/>
       </Unit>
     </Units>
   </ProjectOptions>

+ 1 - 1
demo/ButtonGenerator/ButtonGenerator.lpr

@@ -10,7 +10,7 @@ uses
   athreads,
   {$ENDIF}
   Fresnel, // this includes the Fresnel widgetset
-  Fresnel.Forms, MainUnit, DemoSlider, DemoCheckBox, DemoButtonGenerator
+  Fresnel.Forms, MainUnit, DemoButtonGenerator
   { you can add units after this };
 
 {$R *.res}

+ 24 - 19
demo/ButtonGenerator/DemoButtonGenerator.pas

@@ -5,8 +5,8 @@ unit DemoButtonGenerator;
 interface
 
 uses
-  Classes, SysUtils, Fresnel.DOM, Fresnel.Controls, Fresnel.Classes, DemoSlider,
-  DemoCheckBox;
+  Classes, SysUtils, Fresnel.DOM, Fresnel.Controls, Fresnel.Classes, fpCSSTree, Fresnel.DemoSlider,
+  Fresnel.DemoCheckBox;
 
 type
 
@@ -20,9 +20,8 @@ type
       BackgroundColor1 ='#79bbff';
       BackgroundColor2 ='#378de5';
       BorderColor ='#337bc4';
-      cStyle = TDemoSlider.cStyle
-        +TDemoCheckBox.cStyle
-        +'#ButtonDiv {'+LineEnding
+      cStyle =
+        '#ButtonDiv {'+LineEnding
         +'background:'+BackgroundColor1+';'+LineEnding
         +'border:1px solid '+BorderColor+';'+LineEnding
         +'padding:16px 31px;'+LineEnding
@@ -55,6 +54,7 @@ type
     TextShadowHorzPosSlider: TDemoSlider;
     TextShadowBlurRadiusSlider: TDemoSlider;
     constructor Create(AOwner: TComponent); override;
+    class function GetCSSTypeStyle: TCSSString; override;
     procedure UpdateButton;
   end;
 
@@ -103,7 +103,7 @@ begin
   with FontSizeSlider do begin
     MinPosition:=5;
     MaxPosition:=40;
-    Position:=12;
+    SliderPosition:=12;
   end;
 
   // padding vertical slider
@@ -111,7 +111,7 @@ begin
   with PaddingVerticalSlider do begin
     MinPosition:=0;
     MaxPosition:=50;
-    Position:=16;
+    SliderPosition:=16;
   end;
 
   // gradient
@@ -122,7 +122,7 @@ begin
   with BorderWidthSlider do begin
     MinPosition:=0;
     MaxPosition:=10;
-    Position:=1;
+    SliderPosition:=1;
   end;
 
   // border-radius corners: four checkboxes
@@ -153,7 +153,7 @@ begin
   with BorderRadiusSlider do begin
     MinPosition:=0;
     MaxPosition:=50;
-    Position:=16;
+    SliderPosition:=16;
   end;
 
   // text-shadow checkbox
@@ -164,7 +164,7 @@ begin
   with TextShadowVertPosSlider do begin
     MinPosition:=-30;
     MaxPosition:=30;
-    Position:=1;
+    SliderPosition:=1;
   end;
 
   // slider for text-shadow horizontal position
@@ -172,7 +172,7 @@ begin
   with TextShadowHorzPosSlider do begin
     MinPosition:=-30;
     MaxPosition:=30;
-    Position:=1;
+    SliderPosition:=1;
   end;
 
   // slider for text-shadow horizontal position
@@ -181,7 +181,7 @@ begin
     ValueFormat:='%.1fpx';
     MinPosition:=0;
     MaxPosition:=5;
-    Position:=0;
+    SliderPosition:=0;
   end;
 
   // the Button
@@ -200,20 +200,25 @@ begin
   UpdateButton;
 end;
 
+class function TDemoButtonGenerator.GetCSSTypeStyle: TCSSString;
+begin
+  Result:=cStyle;
+end;
+
 procedure TDemoButtonGenerator.UpdateButton;
 var
   s, NewStyle, Radius: String;
 begin
   if ButtonDiv=nil then exit;
   NewStyle:=
-     'font-size:'+FloatToCSSStr(FontSizeSlider.Position)+'px;'
-    +'padding: '+FloatToCSSStr(PaddingVerticalSlider.Position)+'px 31px;'
-    +'border-width:'+FloatToCSSStr(BorderWidthSlider.Position)+'px;';
+     'font-size:'+FloatToCSSPx(FontSizeSlider.SliderPosition)+';'
+    +'padding: '+FloatToCSSPx(PaddingVerticalSlider.SliderPosition)+' 31px;'
+    +'border-width:'+FloatToCSSPx(BorderWidthSlider.SliderPosition)+';';
 
   if GradientChkBox.Checked then
     NewStyle+='background-image:linear-gradient('+BackgroundColor1+','+BackgroundColor2+');';
 
-  Radius:=FloatToCSSStr(BorderRadiusSlider.Position)+'px';
+  Radius:=FloatToCSSPx(BorderRadiusSlider.SliderPosition);
   s:='';
   if BorderRadiusTopLeftChkBox.Checked then
     s:=Radius
@@ -236,9 +241,9 @@ begin
   s:='';
   if TextShadowChkBox.Checked then
   begin
-    s:=FloatToCSSStr(TextShadowHorzPosSlider.Position)
-      +' '+FloatToCSSStr(TextShadowVertPosSlider.Position)
-      +' '+FloatToCSSStr(TextShadowBlurRadiusSlider.Position)
+    s:=FloatToCSSStr(TextShadowHorzPosSlider.SliderPosition)
+      +' '+FloatToCSSStr(TextShadowVertPosSlider.SliderPosition)
+      +' '+FloatToCSSStr(TextShadowBlurRadiusSlider.SliderPosition)
       +' #2f6627';
   end;
   NewStyle+='text-shadow:'+s+';';

+ 0 - 2
demo/ButtonGenerator/MainUnit.pas

@@ -31,8 +31,6 @@ procedure TFresnelForm1.FresnelForm1Create(Sender: TObject);
 var
   ButtonGenerator: TDemoButtonGenerator;
 begin
-  Stylesheet.Add(TDemoButtonGenerator.cStyle);
-
   ButtonGenerator:=TDemoButtonGenerator.Create(Self);
   with ButtonGenerator do begin
     Name:='ButtonGenerator';

+ 0 - 8
demo/CheckBox/CustomCheckBox.lpi

@@ -47,14 +47,6 @@
         <ResourceBaseClass Value="Other"/>
         <ResourceBaseClassname Value="TFresnelForm"/>
       </Unit>
-      <Unit>
-        <Filename Value="DemoSlider.pas"/>
-        <IsPartOfProject Value="True"/>
-      </Unit>
-      <Unit>
-        <Filename Value="DemoCheckBox.pas"/>
-        <IsPartOfProject Value="True"/>
-      </Unit>
     </Units>
   </ProjectOptions>
   <CompilerOptions>

+ 1 - 1
demo/CheckBox/CustomCheckBox.lpr

@@ -10,7 +10,7 @@ uses
   athreads,
   {$ENDIF}
   Fresnel, // this includes the Fresnel widgetset
-  Fresnel.Forms, MainUnit, DemoCheckBox
+  Fresnel.Forms, MainUnit
   { you can add units after this };
 
 {$R *.res}

BIN
demo/CheckBox/DemoCheckBox.res


+ 5 - 0
demo/CheckBox/MainUnit.lfm

@@ -4,5 +4,10 @@ object FresnelCheckBoxForm: TFresnelCheckBoxForm
   FormTop = 206
   FormWidth = 320
   FormHeight = 240
+  Stylesheet.Strings = (
+    ':root {'
+    '  font-size: 15px;'
+    '}'
+  )
   OnCreate = FresnelCheckBoxFormCreate
 end

+ 1 - 3
demo/CheckBox/MainUnit.pas

@@ -5,7 +5,7 @@ unit MainUnit;
 interface
 
 uses
-  Classes, SysUtils, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls, DemoCheckBox;
+  Classes, SysUtils, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls, Fresnel.DemoCheckBox;
 
 type
 
@@ -30,8 +30,6 @@ procedure TFresnelCheckBoxForm.FresnelCheckBoxFormCreate(Sender: TObject);
 var
   CheckBox: TDemoCheckBox;
 begin
-  Stylesheet.Text:=TDemoCheckBox.cStyle;
-
   CheckBox:=TDemoCheckBox.Create(Self);
   with CheckBox do begin
     Name:='CheckBox';

+ 1 - 1
demo/CheckBox/checkboximage.inc → demo/CheckBox/fresnel.democheckbox.inc

@@ -1,4 +1,4 @@
-
+{%MainUnit fresnel.democheckbox.pas}
 Const
   CheckboxImage : Array[0..8320] of byte = (
      $89,$50,$4E,$47,$0D,$0A,$1A,$0A,$00,$00,$00,$0D,$49,$48,$44,$52,$00,

+ 15 - 18
demo/CheckBox/DemoCheckBox.pas → demo/CheckBox/fresnel.democheckbox.pas

@@ -1,4 +1,4 @@
-unit DemoCheckBox;
+unit Fresnel.DemoCheckbox;
 
 {$mode ObjFPC}{$H+}
 
@@ -10,7 +10,7 @@ uses
   Windows,
   {$ENDIF}
   FPReadPNG, Fresnel.DOM, Fresnel.Controls, Fresnel.Classes,
-  FCL.Events, Fresnel.Events;
+  FCL.Events, Fresnel.Events, fpCSSTree;
 
 type
 
@@ -27,10 +27,10 @@ type
       cStyle = ''
         +'.CheckBoxButton {'+LineEnding
         +'  cursor: pointer;'+LineEnding
-        +'  border-radius: 2px;'+LineEnding
+        +'  border-radius: 0.2em;'+LineEnding
         +'  padding: 1px;'+LineEnding
-        +'  width: 11px;'+LineEnding
-        +'  height: 11px;'+LineEnding
+        +'  width: 1em;'+LineEnding
+        +'  height: 1em;'+LineEnding
         +'}'+LineEnding;
       cCheckedStyle = 'background: #68f; border: 2px solid #68f;';
       cUncheckedStyle = 'background: white; border: 2px solid #999;';
@@ -59,18 +59,18 @@ type
         +'}'+LineEnding
         +TDemoCheckBoxButton.cStyle
         +'.CheckBoxButton {'+LineEnding
-        +'  margin: 0 2px 0 0;'+LineEnding
+        +'  margin: 0 0.2em 0 0;'+LineEnding
         +'}'+LineEnding
         +'.CheckBoxLabel {'+LineEnding
         +'  cursor: pointer;'+LineEnding
-        +'  font-size: 12px;'+LineEnding
-        +'  padding: 2px 3px;'+LineEnding
+        +'  padding: 0.2em 0.3em;'+LineEnding
         +'  margin-bottom: 0;'+LineEnding
         +'}'+LineEnding;
   public
     Box: TDemoCheckBoxButton;
     CaptionLabel: TLabel;
     constructor Create(AOwner: TComponent); override;
+    class function GetCSSTypeStyle: TCSSString; override;
     property Caption: TFresnelCaption read GetCaption write SetCaption;
     property Checked: boolean read GetChecked write SetChecked;
     property OnChange: TNotifyEvent read FOnChange write FOnChange;
@@ -78,11 +78,7 @@ type
 
 implementation
 
-{$IFNDEF CPUWASM32}
-{$R *.res}
-{$ELSE}
-{$I checkboximage.inc}
-{$ENDIF}
+{$I fresnel.democheckbox.inc}
 
 { TDemoCheckBoxButton }
 
@@ -114,13 +110,9 @@ begin
   UncheckedStyle:=cUncheckedStyle;
 
   Style:=UncheckedStyle;
-{$IFNDEF CPUWASM32}
-  aStream := TResourceStream.Create(HINSTANCE, 'Check', RT_RCDATA);
-{$ELSE}
   aStream:=TMemoryStream.Create;
-  aStream.WriteBuffer(CheckBoxImage,SizeOf(CheckBoxImage));
+  aStream.WriteBuffer(CheckBoxImage[0],length(CheckBoxImage));
   aStream.Position:=0;
-{$ENDIF}
   try
     Image.LoadFromStream(aStream);
   finally
@@ -180,5 +172,10 @@ begin
   end;
 end;
 
+class function TDemoCheckBox.GetCSSTypeStyle: TCSSString;
+begin
+  Result:=cStyle;
+end;
+
 end.
 

+ 1 - 0
demo/Combobox/.gitignore

@@ -0,0 +1 @@
+ComboboxNative

BIN
demo/Combobox/ArrowDown.png


+ 75 - 0
demo/Combobox/ComboboxNative.lpi

@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <Flags>
+        <MainUnitHasCreateFormStatements Value="False"/>
+        <MainUnitHasTitleStatement Value="False"/>
+        <MainUnitHasScaledStatement Value="False"/>
+      </Flags>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="ComboboxNative"/>
+      <UseAppBundle Value="False"/>
+      <ResourceType Value="res"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <RequiredPackages>
+      <Item>
+        <PackageName Value="Fresnel"/>
+        <DefaultFilename Value="../../src/fresnel.lpk" Prefer="True"/>
+      </Item>
+    </RequiredPackages>
+    <Units>
+      <Unit>
+        <Filename Value="ComboboxNative.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="MainUnit.pas"/>
+        <IsPartOfProject Value="True"/>
+        <ComponentName Value="MainForm"/>
+        <HasResources Value="True"/>
+        <ResourceBaseClass Value="Other"/>
+        <ResourceBaseClassname Value="TFresnelForm"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="ComboboxNative"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <Linking>
+      <Debugging>
+        <DebugInfoType Value="dsDwarf2"/>
+      </Debugging>
+    </Linking>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 16 - 0
demo/Combobox/ComboboxNative.lpr

@@ -0,0 +1,16 @@
+program ComboboxNative;
+
+uses
+  UTF8Utils,
+  dl,
+  Fresnel, // initializes the widgetset
+  Fresnel.App, MainUnit;
+
+begin
+  writeln();
+  FresnelApplication.HookFresnelLog:=true;
+  FresnelApplication.Initialize;
+  FresnelApplication.CreateForm(TMainForm,MainForm);
+  FresnelApplication.Run;
+end.
+

+ 17 - 0
demo/Combobox/MainUnit.lfm

@@ -0,0 +1,17 @@
+object MainForm: TMainForm
+  Caption = 'MainForm'
+  FormLeft = 450
+  FormTop = 300
+  FormWidth = 350
+  FormHeight = 255
+  OnCreate = MainFormCreate
+  object Body1: TBody
+    Style = 'border: 2px solid blue;'#10
+    object Div1: TDiv
+      object Label1: TLabel
+        Style = 'color: red;'
+        Caption = 'Label1'
+      end
+    end
+  end
+end

+ 57 - 0
demo/Combobox/MainUnit.pas

@@ -0,0 +1,57 @@
+unit MainUnit;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, Fresnel.Forms, Fresnel.Controls, Fresnel.Events,
+  FCL.Events, Fresnel.DemoCombobox;
+
+type
+
+  { TMainForm }
+
+  TMainForm = class(TFresnelForm)
+    Body1: TBody;
+    Div1: TDiv;
+    Label1: TLabel;
+    procedure MainFormCreate(Sender: TObject);
+  private
+  public
+    Combobox1: TDemoCombobox;
+  end;
+
+var
+  MainForm: TMainForm;
+
+implementation
+
+{$R *.lfm}
+
+{ TMainForm }
+
+procedure TMainForm.MainFormCreate(Sender: TObject);
+begin
+  Stylesheet.Add(''
+    +'body {'
+    +'  font-size: 15px;'
+    +'}'
+    +'#Div1 {'
+    +'  background:linear-gradient(#ededed, #bab1ba);'
+    +'  border:7px solid #18ab29;'
+    +'  padding:16px 31px;'
+    +'  font-size:15px; font-family:Arial; font-weight:bold;'
+    +'  text-shadow: 0 1 1 #333;'
+    +'  color:#fff;'
+    +'};');
+  Div1.Style:='display: none;';
+
+  Combobox1:=TDemoCombobox.Create(Self);
+  Combobox1.Name:='Combobox1';
+  Combobox1.Parent:=Body1;
+  Combobox1.Items.Add('First');
+end;
+
+end.
+

+ 43 - 0
demo/Combobox/fresnel.democombobox.inc

@@ -0,0 +1,43 @@
+{%MainUnit fresnel.democombobox.pas}
+const ArrowDownImage =
+  #137'PNG'#13#10#26#10#0#0#0#13'IHDR'#0#0#0'@'#0#0#0'@'#8#6#0#0#0#170'iq'#222#0
+  +#0#1#132'iCCPICC profile'#0#0'('#145'}'#145'=H'#195'P'#20#133'OSE)'#149#14'v'
+  +#16'Q'#200'P'#157','#136#138'8j'#21#138'P!'#212#10#173':'#152#188#244#15#154
+  +'4$).'#142#130'k'#193#193#159#197#170#131#139#179#174#14#174#130' '#248#3#226
+  +#236#224#164#232'"%'#222#151#20'Z'#196'x'#225#241'>'#206#187#231#240#222'}'
+  +#128#208#168'0'#205#234#26#7'4'#221'6'#211#201#132#152#205#173#138'='#175#8
+  +'!'#128'0"'#24#150#153'e'#204'IR'#10#190#245'uO'#189'Twq'#158#229#223#247'g'
+  +#245#169'y'#139#1#1#145'x'#150#25#166'M'#188'A<'#189'i'#27#156#247#137#163
+  +#172'$'#171#196#231#196'c&]'#144#248#145#235#138#199'o'#156#139'.'#11'<3jf'
+  +#210#243#196'Qb'#177#216#193'J'#7#179#146#169#17'O'#17#199'TM'#167'|!'#235
+  +#177#202'y'#139#179'V'#169#177#214'='#249#11#195'y}e'#153#235#180#134#144#196
+  +'"'#150' A'#132#130#26#202#168#192'F'#156'v'#157#20#11'i:O'#248#248#7']'#191
+  +'D.'#133'\e0r,'#160#10#13#178#235#7#255#131#223#179#181#10#147#19'^R8'#1't'
+  +#191'8'#206#199#8#208#179#11'4'#235#142#243'}'#236'8'#205#19' '#248#12'\'#233
+  +'m'#127#181#1#204'|'#146'^ok'#177'# '#178#13'\\'#183'5e'#15#184#220#1#6#158
+  +#12#217#148'])HK('#20#128#247'3'#250#166#28#208#127#11#132#214#188#185#181
+  +#206'q'#250#0'dhV'#169#27#224#224#16#24'-R'#246#186#207#187'{;'#231#246'oOk~'
+  +'?'#27#150'r'#132'#'#222#176#242#0#0#0#6'bKGD'#0#255#0#255#0#255#160#189#167
+  +#147#0#0#0#9'pHYs'#0#0#13#215#0#0#13#215#1'B('#155'x'#0#0#0#7'tIME'#7#232#10
+  +#2#13#4#9#195#18#13#150#0#0#1#211'IDATx'#218#237#153#185'J'#3'A'#24#128#191
+  +#128#149'`k+ZX'#138#141#198'#'#241#9#188'56b'#163#198#251'%'#188#226#149#218
+  +#210'J+[;'#31#192#198'V'#176#16'|'#2'o'#16'5'#186'k'#147#133'%$&3'#155'cf'
+  +#252'?'#152'"03'#225#251#194'2'#187#27#16#4'A'#16#4'A'#16#4'A'#16#254#31#177
+  +#130#207#9'`'#22'h'#3#238#129'S'#224#202'r'#199'v '#13't'#1'O'#192#5'p'#14'x'
+  +#133#19#179#128'_0< c'#177'|'#10'x/'#226'u'#9'4'#135''''#206#21#153#20#30'Y'
+  +#11#229'g'#128#220#31'N'#199#225#201#215'e'#2#248#192#145'C'#242'>'#240#1#180
+  +#4#11#190'*'#8'`K'#132'J'#228#131#209#29',z'#168'p'#129#15#28#26'~'#205#231
+  +#20'\:'#131#133'g'#10#139'L'#141#160'*'#127#23'>'#5';'#128'G'#139'#'#164#20
+  +'.c'#31#248#1#134#11'7'#233#205#159#147'*'#17#14#12#144#159#214#144#159'/'
+  +#181#153'm'#17#170'*'#31#16#183'$'#130#142#252'B'#165#155#199#129'g'#131'#'
+  +#212'T>J'#132#253':'#200'Oi'#200'/'#234'~'#153'i'#17#234'*'#31#208#167#17'a'
+  +#207#21'yS"Lj'#200#167#171#253#11#232'D'#200#184'"'#31#142#240'R'#199#8#19'&'
+  +#201'G'#137#176#235#138'|'#148#8';'#10#251#143'+'#202'{'#245#148#15#232#175
+  +'Q'#132'1'#13#249#165'F'#221#139#235'D'#216'vE>'#28#225'U1'#194'V'#9#249'OE'
+  +#249'eS'#158#199#7'"F'#24#181'Y>J'#132'M`'#196#5#249'('#17#190#21#229'WL'#127
+  +'+;'#8#188')FpF'#190'V'#17'<`'#21#203'HT)'#130#149#242#213#138#224#1'kXNR3'
+  +#130#7#172#227#8#170#17#156#146'W'#141#224#164'|'#192'P'#153#8#30#176#129#227
+  +'$)'#254'gl'#174#17#231'|'#172'A'#17'Z'#243'Oq=@'#19'p'#11#156#0'7'#8#130' '
+  +#8#130' '#8#130' '#8'B'#205#249#5#245#185#222#207#19'3'#29#184#0#0#0#0'IEND'
+  +#174'B`'#130;
+

+ 289 - 0
demo/Combobox/fresnel.democombobox.pas

@@ -0,0 +1,289 @@
+unit Fresnel.DemoCombobox;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, System.UITypes, FPReadPNG, Fresnel.DOM, Fresnel.Controls,
+  Fresnel.Classes, FCL.Events, Fresnel.Events, fpCSSTree;
+
+type
+
+  { TDemoCombobox }
+
+  TDemoCombobox = class(TDiv,IFPObserver)
+  private
+    FItemIndex: integer;
+    FItems: TStrings;
+    FOnChange: TNotifyEvent;
+    procedure CaptionDivMouseDown(Event: TAbstractEvent);
+    function GetCaption: string;
+    procedure SetCaption(const AValue: string);
+    procedure SetItemIndex(const AValue: integer);
+    procedure SetItems(const AValue: TStrings);
+  protected
+    procedure SetName(const NewName: TComponentName); override;
+    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
+    Procedure FPOObservedChanged(ASender: TObject; {%H-}Operation: TFPObservedOperation; {%H-}Data: Pointer); override;
+  public
+    // default styles
+    const
+      cStyle = ''
+        +'.Combobox {'+LineEnding
+        +'  position: relative;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxCaption {'+LineEnding
+        +'  padding: 6px;'+LineEnding
+        +'  border: 1px solid #5080e0;'+LineEnding
+        +'  border-radius: 5px;'+LineEnding
+        +'  background-color: #b6d6f0;'+LineEnding
+        +'  display: flex;'+LineEnding
+        +'  justify-content: center;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxCaption:hover {'+LineEnding
+        +'  background-color: #a6e6ff;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxCaptionLabel {'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxCaptionIcon {'+LineEnding
+        +'  width: 16px;'+LineEnding
+        +'  height: 16px;'+LineEnding
+        +'  padding-left: 8px;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxMenu {'+LineEnding
+        +'  position: absolute;'+LineEnding
+        +'  padding-top: 2px;'+LineEnding
+        +'  border-radius: 5px;'+LineEnding
+        +'  border: 1px solid #5080e0;'+LineEnding
+        +'  background-color: #b6d6f0;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxContent {'+LineEnding
+        +'  padding: 5px 0;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxItem {'+LineEnding
+        +'  padding: 5px 20px;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxItem:hover {'+LineEnding
+        +'  background-color: #a6e6ff;'+LineEnding
+        +'}'+LineEnding
+        +'.ComboboxSelected {'+LineEnding
+        +'  background-color: #a6d6ff;'+LineEnding
+        +'}'+LineEnding;
+  public
+    CaptionDiv: TDiv;
+    CaptionLabel: TLabel;
+    CaptionIcon: TImage;
+    Menu: TDiv;
+    Content: TDiv;
+    constructor Create(AOwner: TComponent); override;
+    destructor Destroy; override;
+    procedure Clear; virtual;
+    class function GetCSSTypeStyle: TCSSString; override;
+    procedure UpdateItems; virtual;
+    property OnChange: TNotifyEvent read FOnChange write FOnChange;
+    property Items: TStrings read FItems write SetItems;
+    property ItemIndex: integer read FItemIndex write SetItemIndex;
+    property Caption: string read GetCaption write SetCaption;
+  end;
+
+implementation
+
+{$I fresnel.democombobox.inc}
+
+{ TDemoCombobox }
+
+procedure TDemoCombobox.SetItems(const AValue: TStrings);
+begin
+  if (FItems=AValue) or FItems.Equals(AValue) then Exit;
+  FItems:=AValue;
+  UpdateItems;
+end;
+
+procedure TDemoCombobox.SetName(const NewName: TComponentName);
+var
+  WasCaption: Boolean;
+begin
+  WasCaption:=Caption=Name;
+  inherited SetName(NewName);
+  if WasCaption then
+    Caption:=NewName;
+end;
+
+procedure TDemoCombobox.Notification(AComponent: TComponent; Operation: TOperation);
+begin
+  inherited Notification(AComponent, Operation);
+  if Operation=opRemove then
+  begin
+    if AComponent=CaptionDiv then
+      CaptionDiv:=nil;
+    if AComponent=CaptionLabel then
+      CaptionLabel:=nil;
+    if AComponent=CaptionIcon then
+      CaptionIcon:=nil;
+    if AComponent=Content then
+      Content:=nil;
+    if AComponent=Menu then
+      Menu:=nil;
+  end;
+end;
+
+procedure TDemoCombobox.FPOObservedChanged(ASender: TObject; Operation: TFPObservedOperation;
+  Data: Pointer);
+begin
+  if ASender=FItems then
+  begin
+    UpdateItems;
+  end;
+end;
+
+procedure TDemoCombobox.SetItemIndex(const AValue: integer);
+begin
+  if FItemIndex=AValue then Exit;
+  if (csDestroying in ComponentState) or (Content=nil) then exit;
+  if (AValue>=Content.NodeCount) and not (csLoading in ComponentState) then
+    exit;
+  if (FItemIndex>=0) and (FItemIndex<Content.NodeCount) then
+    Content.Nodes[FItemIndex].RemoveCSSClass('ComboboxSelected');
+  FItemIndex:=AValue;
+  if (FItemIndex>=0) and (FItemIndex<Content.NodeCount) then
+    Content.Nodes[FItemIndex].AddCSSClass('ComboboxSelected');
+end;
+
+function TDemoCombobox.GetCaption: string;
+begin
+  if CaptionLabel<>nil then
+    Result:=CaptionLabel.Caption
+  else
+    Result:='';
+end;
+
+procedure TDemoCombobox.CaptionDivMouseDown(Event: TAbstractEvent);
+begin
+  if Menu.GetStyleAttr('display')='none' then
+    Menu.SetStyleAttr('display','')
+  else
+    Menu.SetStyleAttr('display','none');
+end;
+
+procedure TDemoCombobox.SetCaption(const AValue: string);
+begin
+  if CaptionLabel<>nil then
+    CaptionLabel.Caption:=AValue;
+end;
+
+constructor TDemoCombobox.Create(AOwner: TComponent);
+var
+  aStream: TMemoryStream;
+begin
+  inherited Create(AOwner);
+  FItemIndex:=-1;
+  FItems:=TStringList.Create;
+  FItems.FPOAttachObserver(Self);
+
+  CSSClasses.Add('Combobox');
+
+  CaptionDiv:=TDiv.Create(Self);
+  with CaptionDiv do begin
+    Name:='CaptionDiv';
+    CSSClasses.Add('ComboboxCaption');
+    Parent:=Self;
+    AddEventListener(evtMouseDown,@CaptionDivMouseDown);
+  end;
+
+  CaptionLabel:=TLabel.Create(Self);
+  with CaptionLabel do begin
+    Name:='CaptionLabel';
+    Caption:=Self.Name;
+    CSSClasses.Add('ComboboxCaptionLabel');
+    Parent:=CaptionDiv;
+  end;
+
+  CaptionIcon:=TImage.Create(Self);
+  with CaptionIcon do begin
+    Name:='CaptionIcon';
+    //Caption:=#$E2#$8C#$84; // Down Arrowhead ⌄ , needs font that has it
+    CSSClasses.Add('ComboboxCaptionIcon');
+    Parent:=CaptionDiv;
+  end;
+  aStream:=TMemoryStream.Create;
+  try
+    aStream.WriteBuffer(ArrowDownImage[1],length(ArrowDownImage));
+    aStream.Position:=0;
+    CaptionIcon.Image.LoadFromStream(aStream);
+  finally
+    aStream.Free;
+  end;
+
+  Menu:=TDiv.Create(Self);
+  with Menu do begin
+    Name:='Menu';
+    CSSClasses.Add('ComboboxMenu');
+    SetStyleAttr('display','none');
+    Parent:=Self;
+  end;
+
+  Content:=TDiv.Create(Self);
+  with Content do begin
+    Name:='Content';
+    CSSClasses.Add('ComboboxContent');
+    Parent:=Menu;
+  end;
+end;
+
+destructor TDemoCombobox.Destroy;
+begin
+  FItemIndex:=-1;
+  FreeAndNil(FItems);
+  inherited Destroy;
+end;
+
+procedure TDemoCombobox.Clear;
+begin
+  FItemIndex:=-1;
+  FItems.Clear;
+  while Content.NodeCount>FItems.Count do
+    Content.Nodes[Content.NodeCount-1].Free;
+end;
+
+class function TDemoCombobox.GetCSSTypeStyle: TCSSString;
+begin
+  Result:=cStyle;
+end;
+
+procedure TDemoCombobox.UpdateItems;
+var
+  i: Integer;
+  ItemCaption: String;
+  ItemDiv: TDiv;
+  aLabel: TLabel;
+begin
+  for i:=0 to FItems.Count-1 do
+  begin
+    ItemCaption:=FItems[i];
+    ItemDiv:=FItems.Objects[i] as TDiv;
+    if ItemDiv=nil then
+    begin
+      ItemDiv:=TDiv.Create(Self);
+      ItemDiv.Name:='ItemDiv'+IntToStr(i);
+      ItemDiv.CSSClasses.Add('ComboboxItem');
+      aLabel:=TLabel.Create(Self);
+      aLabel.Name:='ItemLabel'+IntToStr(i);
+      aLabel.Caption:=ItemCaption;
+      aLabel.Parent:=ItemDiv;
+      ItemDiv.Parent:=Content;
+    end else begin
+      aLabel:=ItemDiv.Nodes[0] as TLabel;
+      aLabel.Caption:=ItemCaption;
+    end;
+    if i=ItemIndex then
+      ItemDiv.AddCSSClass('ComboboxSelected')
+    else
+      ItemDiv.RemoveCSSClass('ComboboxSelected');
+  end;
+  while Content.NodeCount>FItems.Count do
+    Content.Nodes[Content.NodeCount-1].Free;
+end;
+
+end.
+

+ 9 - 0
demo/Image/MainUnit.pas

@@ -16,6 +16,7 @@ type
   private
   public
     Image1: TImage;
+    Image2: TImage;
   end;
 
 var
@@ -38,7 +39,15 @@ begin
     Parent:=Self;
   end;
 
+  Image2:=TImage.Create(Self);
+  with Image2 do begin
+    Name:='Image2';
+    Style:='border: 1px solid black;';
+    Parent:=Self;
+  end;
+
   Image1.Image.LoadFromFile('powered_by.png');
+  Image2.Image.LoadFromFile('powered_by.png');
 end;
 
 end.

+ 0 - 4
demo/Slider/CustomSlider.lpi

@@ -47,10 +47,6 @@
         <ResourceBaseClass Value="Other"/>
         <ResourceBaseClassname Value="TFresnelForm"/>
       </Unit>
-      <Unit>
-        <Filename Value="DemoSlider.pas"/>
-        <IsPartOfProject Value="True"/>
-      </Unit>
     </Units>
   </ProjectOptions>
   <CompilerOptions>

+ 1 - 2
demo/Slider/CustomSlider.lpr

@@ -10,8 +10,7 @@ uses
   athreads,
   {$ENDIF}
   Fresnel, // this includes the Fresnel widgetset
-  Fresnel.Forms, MainUnit, DemoSlider
-  { you can add units after this };
+  Fresnel.Forms, MainUnit;
 
 {$R *.res}
 

+ 2 - 4
demo/Slider/MainUnit.pas

@@ -5,7 +5,7 @@ unit MainUnit;
 interface
 
 uses
-  Classes, SysUtils, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls, DemoSlider;
+  Classes, SysUtils, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls, Fresnel.DemoSlider;
 
 type
 
@@ -30,15 +30,13 @@ procedure TFresnelSliderForm.FresnelSliderFormCreate(Sender: TObject);
 var
   Slider: TDemoSlider;
 begin
-  Stylesheet.Text:=TDemoSlider.cStyle;
-
   Slider:=TDemoSlider.Create(Self);
   with Slider do begin
     Name:='Slider';
     Style:='width: 150px';
     MinPosition:=0;
     MaxPosition:=100;
-    Position:=30;
+    SliderPosition:=30;
     Parent:=Self;
   end;
 end;

+ 34 - 30
demo/Slider/DemoSlider.pas → demo/Slider/fresnel.demoslider.pas

@@ -1,4 +1,4 @@
-unit DemoSlider;
+unit Fresnel.DemoSlider;
 
 {$mode ObjFPC}{$H+}
 
@@ -6,7 +6,7 @@ interface
 
 uses
   Classes, SysUtils, math, System.UITypes, Fresnel.DOM, Fresnel.Controls,
-  Fresnel.Classes, FCL.Events, Fresnel.Events;
+  Fresnel.Classes, FCL.Events, Fresnel.Events, fpCSSTree;
 
 type
 
@@ -17,7 +17,7 @@ type
     FMaxPosition: TFresnelLength;
     FMinPosition: TFresnelLength;
     FOnChange: TNotifyEvent;
-    FPosition: TFresnelLength;
+    FSliderPosition: TFresnelLength;
     FValueFormat: string;
     function GetCaption: TFresnelCaption;
     procedure OnSliderDivMouse(Event: TAbstractEvent);
@@ -25,33 +25,31 @@ type
     procedure SetCaption(const AValue: TFresnelCaption);
     procedure SetMaxPosition(const AValue: TFresnelLength);
     procedure SetMinPosition(const AValue: TFresnelLength);
-    procedure SetPosition(AValue: TFresnelLength);
+    procedure SetSliderPosition(AValue: TFresnelLength);
     procedure SetValueFormat(const AValue: string);
     procedure UpdateValueLabel;
-    procedure UpdatePosition;
+    procedure UpdateSliderPosition;
   public
     const
       cStyle = ''
        +'.SliderCaptionDiv {'+LineEnding
-       +'  margin-bottom: 4px;'+LineEnding
-       +'  font-size: 12px;'+LineEnding
+       +'  margin-bottom: 0.3em;'+LineEnding
        +'  width: 100%;'+LineEnding
        +'}'+LineEnding
        +'.SliderLabel {'+LineEnding
        +'}'+LineEnding
        +'.SliderValue {'+LineEnding
-       +'  margin-left: 5px;'+LineEnding
-       +'  font-size: 13px;'+LineEnding
+       +'  margin-left: 0.4em;'+LineEnding
        +'  font-weight: 700;'+LineEnding
        +'  color: #f66020;'+LineEnding
        +'}'+LineEnding
        +'.SliderDiv {'+LineEnding
-       +'  margin: 7px 0 5px;'+LineEnding
+       +'  margin: 0.5em 0 0.4em;'+LineEnding
        +'  position: relative;'+LineEnding
        +'  border: 1px solid #5080e0;'+LineEnding
        +'  background-color: #b6d6f0;'+LineEnding
-       +'  height: 9px;'+LineEnding
-       +'  border-radius: 6px;'+LineEnding
+       +'  height: 0.8em;'+LineEnding
+       +'  border-radius: 5px;'+LineEnding
        +'  width: 100%;'+LineEnding
        +'}'+LineEnding
        +'.SliderRange {'+LineEnding
@@ -67,14 +65,14 @@ type
        +'.SliderPoint {'+LineEnding
        +'  position: absolute;'+LineEnding
        +'  z-index: 2;'+LineEnding
-       +'  width: 15px;'+LineEnding
-       +'  height: 15px;'+LineEnding
+       +'  width: 1.3em;'+LineEnding
+       +'  height: 1.3em;'+LineEnding
        +'  border: 1px solid #385590;'+LineEnding
        +'  background-color: #fff;'+LineEnding
        +'  border-radius: 50%;'+LineEnding
        +'  cursor: pointer;'+LineEnding
-       +'  top: -.3em;'+LineEnding
-       +'  margin-left: -.6em;'+LineEnding
+       +'  top: -0.3em;'+LineEnding
+       +'  margin-left: -0.5em;'+LineEnding
        +'}'+LineEnding;
     var
     CaptionDiv: TDiv;
@@ -84,9 +82,10 @@ type
     SliderRange: TDiv; // inside SliderDiv
     SliderPoint: TDiv; // inside SliderDiv
     constructor Create(AOwner: TComponent); override;
+    class function GetCSSTypeStyle: TCSSString; override;
     property MinPosition: TFresnelLength read FMinPosition write SetMinPosition;
     property MaxPosition: TFresnelLength read FMaxPosition write SetMaxPosition;
-    property Position: TFresnelLength read FPosition write SetPosition;
+    property SliderPosition: TFresnelLength read FSliderPosition write SetSliderPosition;
     property Caption: TFresnelCaption read GetCaption write SetCaption;
     property ValueFormat: string read FValueFormat write SetValueFormat;
     property OnChange: TNotifyEvent read FOnChange write FOnChange;
@@ -113,7 +112,7 @@ begin
     begin
       w:=SliderDiv.RenderedBorderBox.Width;
       if w<1 then exit;
-      Position:=Evt.X/w * (MaxPosition-MinPosition) + MinPosition;
+      SliderPosition:=Evt.X/w * (MaxPosition-MinPosition) + MinPosition;
     end;
   end;
 end;
@@ -132,7 +131,7 @@ begin
       w:=SliderDiv.RenderedBorderBox.Width;
       if w<1 then exit;
       x:=Evt.X + SliderPoint.RenderedContentBox.Left;
-      Position:=x/w * (MaxPosition-MinPosition) + MinPosition;
+      SliderPosition:=x/w * (MaxPosition-MinPosition) + MinPosition;
     end;
   end;
 end;
@@ -147,8 +146,8 @@ begin
   if FMaxPosition=AValue then Exit;
   FMaxPosition:=AValue;
   FMinPosition:=Min(MinPosition,MaxPosition);
-  FPosition:=Min(MaxPosition,Max(MinPosition,Position));
-  UpdatePosition;
+  FSliderPosition:=Min(MaxPosition,Max(MinPosition,SliderPosition));
+  UpdateSliderPosition;
 end;
 
 procedure TDemoSlider.SetMinPosition(const AValue: TFresnelLength);
@@ -156,16 +155,16 @@ begin
   if FMinPosition=AValue then Exit;
   FMinPosition:=AValue;
   FMaxPosition:=Max(MinPosition,MaxPosition);
-  FPosition:=Min(MaxPosition,Max(MinPosition,Position));
-  UpdatePosition;
+  FSliderPosition:=Min(MaxPosition,Max(MinPosition,SliderPosition));
+  UpdateSliderPosition;
 end;
 
-procedure TDemoSlider.SetPosition(AValue: TFresnelLength);
+procedure TDemoSlider.SetSliderPosition(AValue: TFresnelLength);
 begin
   AValue:=Min(MaxPosition,Max(MinPosition,AValue));
-  if AValue=FPosition then exit;
-  FPosition:=AValue;
-  UpdatePosition;
+  if AValue=FSliderPosition then exit;
+  FSliderPosition:=AValue;
+  UpdateSliderPosition;
   if Assigned(OnChange) then OnChange(Self);
 end;
 
@@ -180,16 +179,16 @@ procedure TDemoSlider.UpdateValueLabel;
 var
   s: String;
 begin
-  s:=Format(ValueFormat,[Position]);
+  s:=Format(ValueFormat,[SliderPosition]);
   SliderValue.Caption:=s;
 end;
 
-procedure TDemoSlider.UpdatePosition;
+procedure TDemoSlider.UpdateSliderPosition;
 var
   p: TFresnelLength;
   s: String;
 begin
-  p:=(Position-MinPosition)/(MaxPosition-MinPosition);
+  p:=(SliderPosition-MinPosition)/(MaxPosition-MinPosition);
   s:=FloatToCSSStr(p*100)+'%';
   SliderRange.Style:='width: '+s;
   SliderPoint.Style:='left: '+s;
@@ -251,5 +250,10 @@ begin
   end;
 end;
 
+class function TDemoSlider.GetCSSTypeStyle: TCSSString;
+begin
+  Result:=cStyle;
+end;
+
 end.
 

+ 22 - 0
demo/democomps/DemoCompsReg.pas

@@ -0,0 +1,22 @@
+unit DemoCompsReg;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, Fresnel.DemoCheckBox, Fresnel.DemoSlider, Fresnel.DemoCombobox;
+
+procedure Register;
+
+implementation
+
+{$R DemoCompsReg.res}
+
+procedure Register;
+begin
+  RegisterComponents('Fresnel',[TDemoCheckBox,TDemoSlider,TDemoCombobox]);
+end;
+
+end.
+

BIN
demo/democomps/DemoCompsReg.res


+ 58 - 0
demo/democomps/fresneldemocomps.lpk

@@ -0,0 +1,58 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <Package Version="5">
+    <Name Value="FresnelDemoComps"/>
+    <Type Value="RunAndDesignTime"/>
+    <Author Value="Mattias Gaertner"/>
+    <CompilerOptions>
+      <Version Value="11"/>
+      <SearchPaths>
+        <IncludeFiles Value="../Combobox;../CheckBox"/>
+        <OtherUnitFiles Value="../Combobox;../CheckBox;../Slider"/>
+        <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+      </SearchPaths>
+    </CompilerOptions>
+    <Description Value="Some demo components for Fresnel."/>
+    <License Value="Modified LGPL2"/>
+    <Version Major="1"/>
+    <Files>
+      <Item>
+        <Filename Value="../Combobox/fresnel.democombobox.inc"/>
+        <Type Value="Include"/>
+      </Item>
+      <Item>
+        <Filename Value="../Combobox/fresnel.democombobox.pas"/>
+        <UnitName Value="Fresnel.DemoCombobox"/>
+      </Item>
+      <Item>
+        <Filename Value="../CheckBox/fresnel.democheckbox.inc"/>
+        <Type Value="Include"/>
+      </Item>
+      <Item>
+        <Filename Value="../CheckBox/fresnel.democheckbox.pas"/>
+        <UnitName Value="Fresnel.DemoCheckbox"/>
+      </Item>
+      <Item>
+        <Filename Value="../Slider/fresnel.demoslider.pas"/>
+        <UnitName Value="Fresnel.DemoSlider"/>
+      </Item>
+      <Item>
+        <Filename Value="DemoCompsReg.pas"/>
+        <HasRegisterProc Value="True"/>
+        <UnitName Value="DemoCompsReg"/>
+      </Item>
+    </Files>
+    <RequiredPkgs>
+      <Item>
+        <PackageName Value="FresnelDsgn"/>
+      </Item>
+    </RequiredPkgs>
+    <UsageOptions>
+      <UnitPath Value="$(PkgOutDir)"/>
+    </UsageOptions>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+  </Package>
+</CONFIG>

+ 22 - 0
demo/democomps/fresneldemocomps.pas

@@ -0,0 +1,22 @@
+{ This file was automatically created by Lazarus. Do not edit!
+  This source is only used to compile and install the package.
+ }
+
+unit FresnelDemoComps;
+
+{$warn 5023 off : no warning about unused units}
+interface
+
+uses
+  Fresnel.DemoCombobox, Fresnel.DemoCheckbox, Fresnel.DemoSlider, DemoCompsReg, LazarusPackageIntf;
+
+implementation
+
+procedure Register;
+begin
+  RegisterUnit('DemoCompsReg', @DemoCompsReg.Register);
+end;
+
+initialization
+  RegisterPackage('FresnelDemoComps', @Register);
+end.

+ 3 - 0
demo/democomps/images/README.txt

@@ -0,0 +1,3 @@
+Updating image resources:
+
+lazres ../DemoCompsReg.res demo*.png

BIN
demo/democomps/images/fresnel.democheckbox.tdemocheckbox.png


BIN
demo/democomps/images/fresnel.democombobox.tdemocombobox.png


BIN
demo/democomps/images/fresnel.demoslider.tdemoslider.png


+ 4 - 3
demo/lcl/ButtonGenerator/ButtonGeneratorLCLDemo.lpi

@@ -57,11 +57,12 @@
         <IsPartOfProject Value="True"/>
       </Unit>
       <Unit>
-        <Filename Value="../../CheckBox/DemoCheckBox.pas"/>
+        <Filename Value="../../Slider/fresnel.demoslider.pas"/>
         <IsPartOfProject Value="True"/>
+        <UnitName Value="Fresnel.DemoSlider"/>
       </Unit>
       <Unit>
-        <Filename Value="../../Slider/DemoSlider.pas"/>
+        <Filename Value="../../CheckBox/fresnel.democheckbox.inc"/>
         <IsPartOfProject Value="True"/>
       </Unit>
     </Units>
@@ -72,7 +73,7 @@
       <Filename Value="ButtonGeneratorLCLDemo"/>
     </Target>
     <SearchPaths>
-      <IncludeFiles Value="$(ProjOutDir)"/>
+      <IncludeFiles Value="$(ProjOutDir);../../CheckBox"/>
       <OtherUnitFiles Value="../../ButtonGenerator;../../CheckBox;../../Slider"/>
       <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
     </SearchPaths>

+ 1 - 1
demo/lcl/ButtonGenerator/ButtonGeneratorLCLDemo.lpr

@@ -10,7 +10,7 @@ uses
   athreads,
   {$ENDIF}
   Interfaces, // this includes the LCL widgetset
-  Forms, UnitLCLForm1, FresForm1, DemoButtonGenerator, DemoCheckBox, DemoSlider,
+  Forms, UnitLCLForm1, FresForm1, DemoButtonGenerator, Fresnel.DemoSlider,
   Fresnel
   { you can add units after this };
 

+ 0 - 2
demo/lcl/ButtonGenerator/FresForm1.pas

@@ -30,8 +30,6 @@ implementation
 
 procedure TFresnelForm1.FresnelForm1Create(Sender: TObject);
 begin
-  Stylesheet.Add(TDemoButtonGenerator.cStyle);
-
   ButtonGenerator:=TDemoButtonGenerator.Create(Self);
   with ButtonGenerator do begin
     Name:='ButtonGenerator';

+ 1 - 0
demo/lcl/CSSViewer/.gitignore

@@ -0,0 +1 @@
+DemoLCLCSSView

BIN
demo/lcl/CSSViewer/DemoLCLCSSView.ico


+ 90 - 0
demo/lcl/CSSViewer/DemoLCLCSSView.lpi

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<CONFIG>
+  <ProjectOptions>
+    <Version Value="12"/>
+    <General>
+      <SessionStorage Value="InProjectDir"/>
+      <Title Value="DemoLCLCSSView"/>
+      <Scaled Value="True"/>
+      <ResourceType Value="res"/>
+      <UseXPManifest Value="True"/>
+      <XPManifest>
+        <DpiAware Value="True"/>
+      </XPManifest>
+      <Icon Value="0"/>
+    </General>
+    <BuildModes>
+      <Item Name="Default" Default="True"/>
+    </BuildModes>
+    <PublishOptions>
+      <Version Value="2"/>
+      <UseFileFilters Value="True"/>
+    </PublishOptions>
+    <RunParams>
+      <FormatVersion Value="2"/>
+    </RunParams>
+    <RequiredPackages>
+      <Item>
+        <PackageName Value="FresnelDsgn"/>
+      </Item>
+      <Item>
+        <PackageName Value="FresnelLCL"/>
+      </Item>
+      <Item>
+        <PackageName Value="LCL"/>
+      </Item>
+    </RequiredPackages>
+    <Units>
+      <Unit>
+        <Filename Value="DemoLCLCSSView.lpr"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="mainunit.pas"/>
+        <IsPartOfProject Value="True"/>
+        <ComponentName Value="Form1"/>
+        <HasResources Value="True"/>
+        <ResourceBaseClass Value="Form"/>
+        <UnitName Value="MainUnit"/>
+      </Unit>
+      <Unit>
+        <Filename Value="unit1.pas"/>
+        <IsPartOfProject Value="True"/>
+        <ComponentName Value="FresnelForm1"/>
+        <ResourceBaseClass Value="Other"/>
+        <ResourceBaseClassname Value="TFresnelForm"/>
+        <UnitName Value="Unit1"/>
+      </Unit>
+    </Units>
+  </ProjectOptions>
+  <CompilerOptions>
+    <Version Value="11"/>
+    <Target>
+      <Filename Value="DemoLCLCSSView"/>
+    </Target>
+    <SearchPaths>
+      <IncludeFiles Value="$(ProjOutDir)"/>
+      <UnitOutputDirectory Value="lib/$(TargetCPU)-$(TargetOS)"/>
+    </SearchPaths>
+    <Linking>
+      <Options>
+        <Win32>
+          <GraphicApplication Value="True"/>
+        </Win32>
+      </Options>
+    </Linking>
+  </CompilerOptions>
+  <Debugging>
+    <Exceptions>
+      <Item>
+        <Name Value="EAbort"/>
+      </Item>
+      <Item>
+        <Name Value="ECodetoolError"/>
+      </Item>
+      <Item>
+        <Name Value="EFOpenError"/>
+      </Item>
+    </Exceptions>
+  </Debugging>
+</CONFIG>

+ 27 - 0
demo/lcl/CSSViewer/DemoLCLCSSView.lpr

@@ -0,0 +1,27 @@
+program DemoLCLCSSView;
+
+{$mode objfpc}{$H+}
+
+uses
+  {$IFDEF UNIX}
+  cthreads,
+  {$ENDIF}
+  {$IFDEF HASAMIGA}
+  athreads,
+  {$ENDIF}
+  Interfaces, // this includes the LCL widgetset
+  Forms, MainUnit, Unit1, Fresnel
+  { you can add units after this };
+
+{$R *.res}
+
+begin
+  RequireDerivedFormResource:=True;
+  Application.Scaled:=True;
+  Application.{%H-}MainFormOnTaskbar:=True;
+  Application.Initialize;
+  Application.CreateForm(TForm1, Form1);
+  Application.CreateForm(TFresnelForm1, FresnelForm1);
+  Application.Run;
+end.
+

BIN
demo/lcl/CSSViewer/DemoLCLCSSView.res


+ 8 - 0
demo/lcl/CSSViewer/fresnel.cssview.lfm

@@ -0,0 +1,8 @@
+object FresnelCSSView: TFresnelCSSView
+  Style = 'position:absolute; box-sizing:border-box; left:248px; top:10px; width:320px; height:240px'
+  Caption = 'CSS'
+  FormLeft = 418
+  FormTop = 216
+  FormWidth = 516
+  FormHeight = 521
+end

+ 134 - 0
demo/lcl/CSSViewer/fresnel.cssview.pas

@@ -0,0 +1,134 @@
+unit Fresnel.CSSView;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, Fresnel.Classes, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls;
+
+type
+
+  { TFresnelCSSView }
+
+  TFresnelCSSView = class(TFresnelForm)
+  private
+    FTargetViewPort: TFresnelViewport;
+    procedure SetTargetViewPort(const AValue: TFresnelViewport);
+  protected
+    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
+    procedure UpdateTree; virtual;
+  public
+    type
+
+      { TElementNode }
+
+      TElementNode = class(TComponent)
+      private
+        FElement: TFresnelElement;
+        procedure SetElement(const AValue: TFresnelElement);
+      protected
+        procedure Notification(AComponent: TComponent; Operation: TOperation); override;
+      public
+        property Element: TFresnelElement read FElement write SetElement;
+      end;
+
+  public
+    MainDiv: TDiv;
+    ElementsTree: TDiv;
+    ElementStyles: TDiv;
+    TargetRoot: TElementNode;
+    constructor Create(AOwner: TComponent); override;
+    property TargetViewPort: TFresnelViewport read FTargetViewPort write SetTargetViewPort;
+  end;
+
+var
+  FresnelCSSView: TFresnelCSSView;
+
+implementation
+
+{$R *.lfm}
+
+{ TFresnelCSSView }
+
+procedure TFresnelCSSView.SetTargetViewPort(const AValue: TFresnelViewport);
+begin
+  if FTargetViewPort=AValue then Exit;
+  if FTargetViewPort<>nil then
+    RemoveFreeNotification(FTargetViewPort);
+  FTargetViewPort:=AValue;
+  if FTargetViewPort<>nil then
+    FreeNotification(FTargetViewPort);
+  UpdateTree;
+end;
+
+procedure TFresnelCSSView.Notification(AComponent: TComponent; Operation: TOperation);
+begin
+  inherited Notification(AComponent, Operation);
+  if Operation=opRemove then
+  begin
+    if FTargetViewPort=AComponent then
+      FTargetViewPort:=nil;
+  end;
+end;
+
+procedure TFresnelCSSView.UpdateTree;
+begin
+  if TargetViewPort=nil then
+  begin
+    FreeAndNil(TargetRoot);
+    exit;
+  end;
+
+end;
+
+constructor TFresnelCSSView.Create(AOwner: TComponent);
+begin
+  inherited Create(AOwner);
+
+  MainDiv:=TDiv.Create(Self);
+  with MainDiv do begin
+    Name:='MainDiv';
+    Style:='display: flex;';
+    Parent:=Self;
+  end;
+
+  ElementsTree:=TDiv.Create(Self);
+  with ElementsTree do begin
+    Name:='ElementsTree';
+    Style:='justify-self: 50%; border: 1px solid black; padding: 6px; flex-grow: 1;';
+    Parent:=MainDiv;
+  end;
+
+  ElementStyles:=TDiv.Create(Self);
+  with ElementStyles do begin
+    Name:='ElementStyles';
+    Style:='justify-self: 50%; border: 1px solid black; padding: 6px; flex-grow: 1;';
+    Parent:=MainDiv;
+  end;
+end;
+
+{ TFresnelCSSView.TElementNode }
+
+procedure TFresnelCSSView.TElementNode.SetElement(const AValue: TFresnelElement);
+begin
+  if FElement=AValue then Exit;
+  if FElement<>nil then
+    RemoveFreeNotification(FElement);
+  FElement:=AValue;
+  if FElement<>nil then
+    FreeNotification(FElement);
+end;
+
+procedure TFresnelCSSView.TElementNode.Notification(AComponent: TComponent; Operation: TOperation);
+begin
+  inherited Notification(AComponent, Operation);
+  if Operation=opRemove then
+  begin
+    if FElement=AComponent then
+      FElement:=nil;
+  end;
+end;
+
+end.
+

+ 9 - 0
demo/lcl/CSSViewer/mainunit.lfm

@@ -0,0 +1,9 @@
+object Form1: TForm1
+  Left = 240
+  Height = 240
+  Top = 250
+  Width = 320
+  Caption = 'Form1'
+  LCLVersion = '4.99.0.0'
+  OnCreate = FormCreate
+end

+ 45 - 0
demo/lcl/CSSViewer/mainunit.pas

@@ -0,0 +1,45 @@
+unit MainUnit;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, Forms, Controls, Graphics, Dialogs, Unit1, Fresnel.CSSView;
+
+type
+
+  { TForm1 }
+
+  TForm1 = class(TForm)
+    procedure FormCreate(Sender: TObject);
+  private
+    procedure OnQueued({%H-}Data: PtrInt);
+
+  public
+
+  end;
+
+var
+  Form1: TForm1;
+
+implementation
+
+{$R *.lfm}
+
+{ TForm1 }
+
+procedure TForm1.FormCreate(Sender: TObject);
+begin
+  Application.QueueAsyncCall(@OnQueued,0);
+end;
+
+procedure TForm1.OnQueued(Data: PtrInt);
+begin
+  FresnelCSSView:=TFresnelCSSView.Create(Self);
+  FresnelCSSView.Show;
+  FresnelCSSView.TargetViewPort:=FresnelForm1;
+end;
+
+end.
+

+ 21 - 0
demo/lcl/CSSViewer/unit1.lfm

@@ -0,0 +1,21 @@
+object FresnelForm1: TFresnelForm1
+  Style = 'position:absolute; box-sizing:border-box; left:240px; top:10px; width:320px; height:240px'
+  FormLeft = 358
+  FormTop = 223
+  FormWidth = 370
+  FormHeight = 301
+  Stylesheet.Strings = (
+    'div {'
+    '  border: 1px solid blue;'
+    '}'
+  )
+  object Body1: TBody
+    object Div1: TDiv
+      Style = 'position:absolute; box-sizing:border-box; left:16px; top:2px; width:320px; height:80px'
+      object Label1: TLabel
+        Style = 'position:absolute; box-sizing:border-box; left:26px; top:18px'
+        Caption = 'Label1'
+      end
+    end
+  end
+end

+ 32 - 0
demo/lcl/CSSViewer/unit1.pas

@@ -0,0 +1,32 @@
+unit Unit1;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, Fresnel.Classes, Fresnel.Forms, Fresnel.DOM, Fresnel.Controls;
+
+type
+
+  { TFresnelForm1 }
+
+  TFresnelForm1 = class(TFresnelForm)
+    Body1: TBody;
+    Div1: TDiv;
+    Label1: TLabel;
+  private
+
+  public
+
+  end;
+
+var
+  FresnelForm1: TFresnelForm1;
+
+implementation
+
+{$R *.lfm}
+
+end.
+

+ 140 - 0
design/fresnel.dsgnoptions.pas

@@ -0,0 +1,140 @@
+unit Fresnel.DsgnOptions;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, LazFileCache, LazConfigStorage, BaseIDEIntf;
+
+const
+  FresnelDsgnOptsFile = 'fresneldsgnoptions.xml';
+
+type
+
+  { TFresnelDsgnOptions }
+
+  TFresnelDsgnOptions = class(TComponent)
+  private
+    FChangeStamp: int64;
+    FPositionAbsolute: boolean;
+    FSavedStamp: int64;
+    function GetModified: boolean;
+    procedure SetModified(const AValue: boolean);
+    procedure SetPositionAbsolute(const AValue: boolean);
+  public
+    constructor Create(AOwner: TComponent); override;
+    destructor Destroy; override;
+    procedure IncreaseChangeStamp; inline;
+    procedure Load;
+    procedure Save;
+    procedure LoadFromConfig(Cfg: TConfigStorage);
+    procedure SaveToConfig(Cfg: TConfigStorage);
+  public
+    property ChangeStamp: int64 read FChangeStamp;
+    property Modified: boolean read GetModified write SetModified;
+    property PositionAbsolute: boolean read FPositionAbsolute write SetPositionAbsolute; // true = when putting a new element onto a form use position absolute
+  end;
+
+var
+  FresnelOptions: TFresnelDsgnOptions;
+
+implementation
+
+{ TFresnelDsgnOptions }
+
+procedure TFresnelDsgnOptions.SetPositionAbsolute(const AValue: boolean);
+begin
+  if FPositionAbsolute=AValue then Exit;
+  FPositionAbsolute:=AValue;
+end;
+
+constructor TFresnelDsgnOptions.Create(AOwner: TComponent);
+begin
+  inherited;
+  FChangeStamp:=LUInvalidChangeStamp64;
+  FPositionAbsolute:=true;
+end;
+
+destructor TFresnelDsgnOptions.Destroy;
+begin
+  inherited Destroy;
+end;
+
+procedure TFresnelDsgnOptions.IncreaseChangeStamp;
+begin
+  LUIncreaseChangeStamp64(FChangeStamp);
+end;
+
+procedure TFresnelDsgnOptions.Load;
+var
+  Cfg: TConfigStorage;
+begin
+  Cfg:=GetIDEConfigStorage(FresnelDsgnOptsFile,true);
+  try
+    LoadFromConfig(Cfg);
+  finally
+    Cfg.Free;
+  end;
+end;
+
+procedure TFresnelDsgnOptions.Save;
+var
+  Cfg: TConfigStorage;
+begin
+  Cfg:=GetIDEConfigStorage(FresnelDsgnOptsFile,false);
+  try
+    SaveToConfig(Cfg);
+  finally
+    Cfg.Free;
+  end;
+end;
+
+Const
+  KeyPositionAbsolute = 'positionabsolute/value';
+
+procedure TFresnelDsgnOptions.LoadFromConfig(Cfg: TConfigStorage);
+begin
+  PositionAbsolute:=Cfg.GetValue(KeyPositionAbsolute,true);
+
+  Modified:=false;
+end;
+
+procedure TFresnelDsgnOptions.SaveToConfig(Cfg: TConfigStorage);
+begin
+  Cfg.SetDeleteValue(KeyPositionAbsolute,PositionAbsolute,true);
+
+  Modified:=false;
+end;
+
+function TFresnelDsgnOptions.GetModified: boolean;
+begin
+  Result:=FSavedStamp<>FChangeStamp;
+end;
+
+procedure TFresnelDsgnOptions.SetModified(const AValue: boolean);
+begin
+  if AValue then
+    IncreaseChangeStamp
+  else
+    FSavedStamp:=FChangeStamp;
+end;
+
+procedure DoneFresnelOptions;
+begin
+  if FresnelOptions<>nil then
+  begin
+    try
+      if FresnelOptions.Modified then
+        FresnelOptions.Save;
+    except
+    end;
+    FreeAndNil(FresnelOptions);
+  end;
+end;
+
+finalization
+  DoneFresnelOptions;
+
+end.
+

+ 25 - 0
design/fresnel.dsgnoptsframe.lfm

@@ -0,0 +1,25 @@
+object FresnelOptionsFrame: TFresnelOptionsFrame
+  Left = 0
+  Height = 240
+  Top = 0
+  Width = 320
+  ClientHeight = 240
+  ClientWidth = 320
+  TabOrder = 0
+  DesignLeft = 263
+  DesignTop = 291
+  object PositionAbsoluteCheckBox: TCheckBox
+    AnchorSideLeft.Control = Owner
+    AnchorSideTop.Control = Owner
+    Left = 6
+    Height = 23
+    Top = 6
+    Width = 131
+    BorderSpacing.Left = 6
+    BorderSpacing.Top = 6
+    Caption = 'Position absolute'
+    ParentShowHint = False
+    ShowHint = True
+    TabOrder = 0
+  end
+end

+ 68 - 0
design/fresnel.dsgnoptsframe.pas

@@ -0,0 +1,68 @@
+{ IDE options frame for Fresnel options
+
+  Author: Mattias Gaertner
+}
+unit Fresnel.DsgnOptsFrame;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils,
+  // LCL
+  Forms, StdCtrls, Dialogs,
+  // IdeIntf
+  IDEOptionsIntf, IDEOptEditorIntf,
+  // Fresnel
+  Fresnel.DsgnStrConsts, Fresnel.DsgnOptions;
+
+type
+
+  { TFresnelOptionsFrame }
+
+  TFresnelOptionsFrame = class(TAbstractIDEOptionsEditor)
+    PositionAbsoluteCheckBox: TCheckBox;
+  private
+  public
+    function GetTitle: String; override;
+    procedure Setup({%H-}ADialog: TAbstractOptionsEditorDialog); override;
+    procedure ReadSettings({%H-}AOptions: TAbstractIDEOptions); override;
+    procedure WriteSettings({%H-}AOptions: TAbstractIDEOptions); override;
+    class function SupportedOptionsClass: TAbstractIDEOptionsClass; override;
+  end;
+
+implementation
+
+{$R *.lfm}
+
+{ TFresnelOptionsFrame }
+
+function TFresnelOptionsFrame.GetTitle: String;
+begin
+  Result:='Fresnel';
+end;
+
+procedure TFresnelOptionsFrame.Setup(ADialog: TAbstractOptionsEditorDialog);
+begin
+  PositionAbsoluteCheckBox.Caption:='Position absolute'; // CSS keywords dont need to be translated
+  PositionAbsoluteCheckBox.Hint:=frsWhenPuttingANewElementOntoAFormPositionItAbsoluteU;
+end;
+
+procedure TFresnelOptionsFrame.ReadSettings(AOptions: TAbstractIDEOptions);
+begin
+  PositionAbsoluteCheckBox.Checked:=FresnelOptions.PositionAbsolute;
+end;
+
+procedure TFresnelOptionsFrame.WriteSettings(AOptions: TAbstractIDEOptions);
+begin
+  FresnelOptions.PositionAbsolute:=PositionAbsoluteCheckBox.Checked;
+end;
+
+class function TFresnelOptionsFrame.SupportedOptionsClass: TAbstractIDEOptionsClass;
+begin
+  Result:=IDEEditorGroups.GetByIndex(GroupEnvironment)^.GroupClass;
+end;
+
+end.
+

+ 2 - 0
design/fresnel.dsgnstrconsts.pas

@@ -8,6 +8,8 @@ resourcestring
   frsFresnelApplication = 'Fresnel Application';
   frsFresnelApplicationDesc = 'A graphical Free Pascal application using'
     +' the cross-platform Fresnel library for its GUI.';
+  frsWhenPuttingANewElementOntoAFormPositionItAbsoluteU = 'When putting a new element onto a form, '
+    +'position it absolute using the mouse coordinates';
 
 implementation
 

+ 119 - 11
design/fresnel.register.pas

@@ -16,11 +16,11 @@ interface
 
 uses
   LCLProc, LCLType, Classes, SysUtils, FormEditingIntf, PropEdits, LazIDEIntf,
-  ComponentEditors, LCLIntf, Graphics, Controls, Forms, ProjectIntf,
-  PackageIntf, LazLoggerBase, CodeToolManager, CodeCache, CodeTree,
-  SourceChanger, StdCodeTools, Fresnel.DOM, Fresnel.Controls, Fresnel.Forms,
+  ComponentEditors, IDEOptEditorIntf, LCLIntf, Graphics, Controls, Forms, ProjectIntf,
+  PackageIntf, IDEOptionsIntf, LazLoggerBase, CodeToolManager, CodeCache,
+  StdCodeTools, Fresnel.DOM, Fresnel.Controls, Fresnel.Forms,
   Fresnel.Renderer, Fresnel.Classes, Fresnel.LCLApp, Fresnel.LCL,
-  Fresnel.DsgnStrConsts, Fresnel.StylePropEdit;
+  Fresnel.DsgnStrConsts, Fresnel.StylePropEdit, Fresnel.DsgnOptsFrame, Fresnel.DsgnOptions;
 
 const
   ProjDescNameFresnelApplication = 'Fresnel Application';
@@ -57,6 +57,7 @@ type
     procedure GetBounds(AComponent: TComponent; out CurBounds: TRect); override;
     procedure GetClientArea(AComponent: TComponent; out
                       CurClientArea: TRect; out ScrollOffset: TPoint); override;
+    procedure InitComponent(AComponent, NewParent: TComponent; NewBounds: TRect); override;
     procedure Paint; override;
     procedure SetBounds(AComponent: TComponent; NewBounds: TRect); override;
   public
@@ -121,6 +122,9 @@ var
   FileDescFresnelForm: TFileDescFresnelForm;
   ProjDescFresnelApplication: TProjDescFresnelApplication;
 
+var
+  FresnelOptionsFrameID: integer = 1000;
+
 procedure Register;
 
 implementation
@@ -129,21 +133,32 @@ implementation
 
 procedure Register;
 begin
+  FresnelOptions:=TFresnelDsgnOptions.Create(nil);
+
+  // register mediator for designer forms
   FormEditingHook.RegisterDesignerMediator(TFresnelFormMediator);
   FormEditingHook.SetDesignerBaseClassCanAppCreateForm(TFresnelCustomForm,true);
 
+  // register elements
   RegisterComponents('Fresnel',[TDiv,TSpan,TLabel,TButton,TImage,TBody]);
   RegisterComponentRequirements([TDiv,TSpan,TLabel,TButton,TImage,TBody],TFresnelComponentRequirements);
 
+  // register fresnel form as new file type
   FileDescFresnelForm:=TFileDescFresnelForm.Create;
   RegisterProjectFileDescriptor(FileDescFresnelForm,FileDescGroupName);
 
+  // register fresnel application as new project type
   ProjDescFresnelApplication:=TProjDescFresnelApplication.Create;
   RegisterProjectDescriptor(ProjDescFresnelApplication);
 
+  // register property editors
   RegisterPropertyEditor(TypeInfo(String), TFresnelElement, 'Style', TFresnelStylePropertyEditor);
   RegisterPropertyEditor(TypeInfo(String), TFresnelCustomForm, 'Style', THiddenPropertyEditor);
   RegisterPropertyEditor(TypeInfo(TStrings), TFresnelCustomForm, 'Stylesheet', TFresnelStyleSheetPropertyEditor);
+
+  // register IDE options frame
+  FresnelOptionsFrameID:=RegisterIDEOptionsEditor(GroupEnvironment,TFresnelOptionsFrame,
+                                                  FresnelOptionsFrameID)^.Index;
 end;
 
 { TFresnelFormMediator }
@@ -215,8 +230,9 @@ begin
     CurBounds:=FDsgnForm.FormBounds.GetRect;
   end else if AComponent is TFresnelElement then
   begin
+    // return borderbox
     El:=TFresnelElement(AComponent);
-    aBox:=El.GetBoundingClientRect;
+    aBox:=El.RenderedBorderBox;
     FresnelRectToRect(aBox,CurBounds);
   end else
     inherited GetBounds(AComponent,CurBounds);
@@ -225,6 +241,11 @@ end;
 
 procedure TFresnelFormMediator.SetBounds(AComponent: TComponent;
   NewBounds: TRect);
+var
+  El: TFresnelElement;
+  OldStyle: String;
+  NewBorderBox: TFresnelRect;
+  NewLeft, NewTop, NewWidth, NewHeight: TFresnelLength;
 begin
   //debugln(['TFresnelFormMediator.SetBounds ',DbgSName(AComponent),' ',dbgs(NewBounds)]);
   if AComponent=FDsgnForm then
@@ -232,8 +253,50 @@ begin
     FDsgnForm.WSResize(TFresnelRect.Create(NewBounds),NewBounds.Width,NewBounds.Height);
   end else if AComponent is TFresnelElement then
   begin
-    // bounds are controlled by CSS
-    // todo: absolute, fixed
+    // an element (bounds are controlled by CSS)
+    El:=TFresnelElement(AComponent);
+    if El.ComputedPosition in [CSSRegistry.kwAbsolute,CSSRegistry.kwFixed] then
+    begin
+      // NewBounds is borderbox
+      with El.LayoutNode do begin
+        NewBorderBox.SetRect(NewBounds);
+        NewLeft:=NewBorderBox.Left-MarginLeft;
+        NewTop:=NewBorderBox.Top-MarginTop;
+        NewWidth:=NewBorderBox.Width;
+        NewHeight:=NewBorderBox.Height;
+
+        // todo: if parent position is static, use the nearest parent with non static
+        // todo: right and bottom aligned
+
+        OldStyle:=El.Style;
+        case El.ComputedBoxSizing of
+        CSSRegistry.kwBorderBox:
+          begin
+
+          end;
+        CSSRegistry.kwPaddingBox:
+          begin
+            NewWidth:=NewWidth-BorderLeft-BorderRight;
+            NewHeight:=NewHeight-BorderTop-BorderBottom;
+          end;
+        CSSRegistry.kwContentBox:
+          begin
+            NewWidth:=NewWidth-BorderLeft-BorderRight-PaddingLeft-PaddingRight;
+            NewHeight:=NewHeight-BorderTop-BorderBottom-PaddingTop-PaddingBottom;
+          end;
+        end;
+      end;
+
+      if El.GetStyleAttr('left')<>'' then
+        El.SetStyleAttr('left',FloatToCSSPx(NewLeft));
+      if El.GetStyleAttr('top')<>'' then
+        El.SetStyleAttr('top',FloatToCSSPx(NewTop));
+      if El.GetStyleAttr('width')<>'' then
+        El.SetStyleAttr('width',FloatToCSSPx(NewWidth));
+      if El.GetStyleAttr('height')<>'' then
+        El.SetStyleAttr('height',FloatToCSSPx(NewHeight));
+      debugln(['TFresnelFormMediator.SetBounds AComponent=',DbgSName(AComponent),' OldStyle=[',OldStyle,'] OldBorderBox=',FloatToCSSStr(El.RenderedBorderBox.Left),',',FloatToCSSStr(El.RenderedBorderBox.Top),' w=',FloatToCSSStr(El.RenderedBorderBox.Width),',h=',FloatToCSSStr(El.RenderedBorderBox.Height),' box-sizing=',CSSRegistry.Keywords[El.ComputedBoxSizing],' NewLeft,Top=',FloatToCSSStr(NewLeft),',',FloatToCSSStr(NewTop),' NewWH=',FloatToCSSStr(NewWidth),'x',FloatToCSSStr(NewHeight)]);
+    end;
   end else begin
     inherited SetBounds(AComponent, NewBounds);
   end;
@@ -241,14 +304,59 @@ end;
 
 procedure TFresnelFormMediator.GetClientArea(AComponent: TComponent; out
   CurClientArea: TRect; out ScrollOffset: TPoint);
+var
+  El: TFresnelElement;
+  Box, BorderBox: TFresnelRect;
 begin
   if AComponent=FDsgnForm then
   begin
     CurClientArea:=Rect(0,0,round(FDsgnForm.Width),round(FDsgnForm.Height));
     ScrollOffset:=Point(0,0);
-  end else begin
-    inherited GetClientArea(AComponent, CurClientArea, ScrollOffset);
-  end;
+  end else if AComponent is TFresnelElement then begin
+    // return contentbox inside the borderbox
+    El:=TFresnelElement(AComponent);
+    BorderBox:=El.RenderedBorderBox;
+    Box:=El.RenderedContentBox;
+    Box.Offset(-BorderBox.Left,-BorderBox.Top);
+    FresnelRectToRect(Box,CurClientArea);
+  end else
+    inherited;
+end;
+
+procedure TFresnelFormMediator.InitComponent(AComponent, NewParent: TComponent; NewBounds: TRect);
+var
+  El: TFresnelElement;
+  BorderBox: TFresnelRect;
+begin
+  if AComponent is TFresnelElement then
+  begin
+    // set parentcomponent, needed for streaming
+    TFresnelFormMediator(AComponent).SetParentComponent(NewParent);
+    El:=TFresnelElement(AComponent);
+    debugln(['TFresnelFormMediator.InitComponent AComponent=',DbgSName(AComponent),' NewParent=',DbgSName(NewParent),' Bounds=',dbgs(NewBounds)]);
+    if FresnelOptions.PositionAbsolute then
+    begin
+      // todo: if parent position is static, use the nearest parent with non static
+      BorderBox.SetRect(NewBounds);
+      // todo: compute margins via resolver
+      El.SetStyleAttr('position','absolute');
+      El.SetStyleAttr('box-sizing','border-box');
+      El.SetStyleAttr('left',FloatToCSSPx(BorderBox.Left));
+      El.SetStyleAttr('top',FloatToCSSPx(BorderBox.Top));
+      if not (El is TReplacedElement) then
+      begin
+        if (BorderBox.Width<=0) and (El.NodeCount=0) then
+          BorderBox.Width:=50;
+        if (BorderBox.Height<=0) and (El.NodeCount=0) then
+          BorderBox.Height:=50;
+        if BorderBox.Width>0 then
+          El.SetStyleAttr('width',FloatToCSSPx(BorderBox.Width));
+        if BorderBox.Height>0 then
+          El.SetStyleAttr('height',FloatToCSSPx(BorderBox.Height));
+      end;
+    end;
+  end else
+    inherited;
 end;
 
 function TFresnelFormMediator.GetComponentOriginOnForm(AComponent: TComponent
@@ -265,7 +373,7 @@ begin
     El:=TFresnelElement(AComponent);
     if not El.Rendered then
       exit(Point(0,0));
-    BorderBox:=El.GetBoundingClientRect;
+    BorderBox:=El.GetBorderBoxOnViewport;
     Result.X:=round(BorderBox.Left);
     Result.Y:=round(BorderBox.Top);
   end else

+ 8 - 0
design/fresneldsgn.lpk

@@ -27,6 +27,14 @@
         <Filename Value="fresnel.dsgnstrconsts.pas"/>
         <UnitName Value="Fresnel.DsgnStrConsts"/>
       </Item>
+      <Item>
+        <Filename Value="fresnel.dsgnoptsframe.pas"/>
+        <UnitName Value="fresnel.dsgnoptsframe"/>
+      </Item>
+      <Item>
+        <Filename Value="fresnel.dsgnoptions.pas"/>
+        <UnitName Value="fresnel.dsgnoptions"/>
+      </Item>
     </Files>
     <RequiredPkgs>
       <Item>

+ 2 - 2
design/fresneldsgn.pas

@@ -8,8 +8,8 @@ unit FresnelDsgn;
 interface
 
 uses
-  Fresnel.Register, Fresnel.StylePropEdit, Fresnel.DsgnStrConsts, 
-  LazarusPackageIntf;
+  Fresnel.Register, Fresnel.StylePropEdit, Fresnel.DsgnStrConsts, Fresnel.DsgnOptsFrame, 
+  Fresnel.DsgnOptions, LazarusPackageIntf;
 
 implementation
 

+ 139 - 72
src/base/fcl-css/fpcssparser.pp

@@ -19,6 +19,7 @@ unit fpCSSParser;
 {$mode ObjFPC}{$H+}
 {$IF FPC_FULLVERSION>30300}
 {$WARN 6060 off} // Case statement does not handle all possible cases
+{$WARN 6058 off} // Call to subroutine "$1" marked as inline is not inlined
 {$ENDIF}
 
 interface
@@ -75,12 +76,12 @@ Type
     function ParseAttributeSelector: TCSSElement; virtual;
     function ParseWQName: TCSSElement;
     function ParseDeclaration(aIsAt : Boolean = false): TCSSDeclarationElement; virtual;
-    function ParseCall(aName: TCSSString): TCSSElement; virtual;
+    function ParseCall(aName: TCSSString; IsSelector: boolean): TCSSCallElement; virtual;
     procedure ParseSelectorCommaList(aCall: TCSSCallElement); virtual;
     procedure ParseRelationalSelectorCommaList(aCall: TCSSCallElement); virtual;
     procedure ParseNthChildParams(aCall: TCSSCallElement); virtual;
     function ParseUnary: TCSSElement; virtual;
-    function ParseUnit: TCSSUnits; virtual;
+    function ParseUnit: TCSSUnit; virtual;
     function ParseIdentifier : TCSSIdentifierElement; virtual;
     function ParseHashIdentifier : TCSSHashIdentifierElement; virtual;
     function ParseClassName : TCSSClassNameElement; virtual;
@@ -90,6 +91,7 @@ Type
     function ParseInteger: TCSSElement; virtual;
     function ParseFloat: TCSSElement; virtual;
     function ParseString: TCSSElement; virtual;
+    function ParseColor: TCSSElement; virtual;
     Function ParseUnicodeRange : TCSSElement; virtual;
     function ParseArray(aPrefix: TCSSElement): TCSSElement; virtual;
     function ParseURL: TCSSElement; virtual;
@@ -116,7 +118,7 @@ Type
     CSSUnaryElementClass: TCSSUnaryElementClass;
     CSSUnicodeRangeElementClass: TCSSUnicodeRangeElementClass;
     CSSURLElementClass: TCSSURLElementClass;
-    Constructor Create(AInput: TStream; ExtraScannerOptions : TCSSScannerOptions = []); overload;
+    Constructor Create(AInput: TStream; ExtraScannerOptions : TCSSScannerOptions = []); overload; // AInput is not freed
     Constructor Create(AScanner : TCSSScanner); virtual; overload;
     Destructor Destroy; override;
     Function Parse : TCSSElement;
@@ -131,6 +133,7 @@ Type
 
 Function TokenToBinaryOperation(aToken : TCSSToken) : TCSSBinaryOperation;
 Function TokenToUnaryOperation(aToken : TCSSToken) : TCSSUnaryOperation;
+Function IsValidCSSAttributeName(const aName: TCSSString): boolean;
 
 implementation
 
@@ -189,6 +192,26 @@ begin
   end;
 end;
 
+function IsValidCSSAttributeName(const aName: TCSSString): boolean;
+var
+  p, StartP: PCSSChar;
+begin
+  if aName='' then exit(false);
+  StartP:=PCSSChar(aName);
+  p:=StartP;
+  if p^='-' then
+  begin
+    inc(p);
+    if p^='-' then
+      inc(p);
+    if not (p^ in ['A'..'Z','a'..'z']) then
+      exit;
+    inc(p);
+  end;
+  while p^ in ['A'..'Z','a'..'z','_','-'] do inc(p);
+  Result:=p=StartP+length(aName);
+end;
+
 { TCSSParser }
 
 function TCSSParser.GetAtEOF: Boolean;
@@ -726,28 +749,33 @@ begin
   Result:=FPeekToken;
 end;
 
-function TCSSParser.ParseUnit : TCSSUnits;
+function TCSSParser.ParseUnit : TCSSUnit;
 
+var
+  p: PCSSChar;
+  U: TCSSUnit;
 begin
   Result:=cuNone;
-  if (CurrentToken in [ctkIDENTIFIER,ctkPERCENTAGE]) then
+  case CurrentToken of
+  ctkPERCENTAGE:
     begin
-    Case currentTokenString of
-    '%'   : Result:=cuPERCENT;
-    'px'  : Result:=cuPX;
-    'rem' : Result:=cuREM;
-    'em'  : Result:=cuEM;
-    'fr'  : Result:=cuFR;
-    'vw'  : Result:=cuVW;
-    'vh'  : Result:=cuVH;
-    'pt'  : Result:=cuPT;
-    'deg' : Result:=cuDEG;
-    else
-      // Ignore. For instance margin: 0 auto
+    Result:=cuPercent;
+    Consume(CurrentToken);
     end;
-    if Result<>cuNone then
-      Consume(CurrentToken);
+  ctkIDENTIFIER:
+    begin
+    p:=PCSSChar(CurrentTokenString);
+    for U:=Succ(cuNone) to High(TCSSUnit) do
+      if CompareMem(p,PCSSChar(CSSUnitNames[U]),SizeOf(TCSSChar)*length(CSSUnitNames[U])) then
+        begin
+        Result:=U;
+        Consume(CurrentToken);
+        break;
+        end;
     end;
+  ctkWHITESPACE:
+    Consume(CurrentToken);
+  end;
 end;
 
 function TCSSParser.CreateElement(aClass : TCSSElementClass): TCSSElement;
@@ -797,20 +825,31 @@ end;
 function TCSSParser.ParseInteger: TCSSElement;
 
 Var
-  aValue : Integer;
+  aCode, aValue : Integer;
   aInt : TCSSIntegerElement;
+  OldReturnWhiteSpace: Boolean;
 
 begin
-  aValue:=StrToInt(CurrentTokenString);
+  Val(CurrentTokenString,aValue,aCode);
+  if aCode<>0 then
+    begin
+    DoError(SErrInvalidFloat,[CurrentTokenString]);
+    GetNextToken;
+    exit(nil);
+    end;
   aInt:=TCSSIntegerElement(CreateElement(CSSIntegerElementClass));
+  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
   try
     aInt.Value:=aValue;
+    Scanner.ReturnWhiteSpace:=true;
     Consume(ctkINTEGER);
     aInt.Units:=ParseUnit;
     Result:=aInt;
     aInt:=nil;
   finally
     aInt.Free;
+    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
+    SkipWhiteSpace;
   end;
 end;
 
@@ -819,19 +858,29 @@ Var
   aCode : Integer;
   aValue : Double;
   aFloat : TCSSFloatElement;
+  OldReturnWhiteSpace: Boolean;
 
 begin
   Val(CurrentTokenString,aValue,aCode);
   if aCode<>0 then
+    begin
     DoError(SErrInvalidFloat,[CurrentTokenString]);
+    GetNextToken;
+    exit(nil);
+    end;
   aFloat:=TCSSFloatElement(CreateElement(CSSFloatElementClass));
+  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
   try
-    Consume(ctkFloat);
     aFloat.Value:=aValue;
+    Scanner.ReturnWhiteSpace:=true;
+    Consume(ctkFloat);
     aFloat.Units:=ParseUnit;
+    if CurrentToken=ctkWHITESPACE then
+      GetNextToken;
     Result:=aFloat;
     aFloat:=nil;
   finally
+    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
     aFloat.Free;
   end;
 end;
@@ -905,6 +954,8 @@ Var
 
 begin
   aDecl:=nil;
+  while CurrentToken=ctkUNKNOWN do
+    GetNextToken;
   if not (CurrentToken in [ctkRBRACE,ctkSEMICOLON]) then
     begin
     aDecl:=ParseDeclaration(aIsAt);
@@ -996,22 +1047,22 @@ function TCSSParser.ParseUnary: TCSSElement;
 var
   Un : TCSSUnaryElement;
   Op : TCSSUnaryOperation;
+  El: TCSSElement;
 
 begin
   Result:=nil;
   if not (CurrentToken in [ctkDOUBLECOLON, ctkMinus, ctkPlus, ctkDiv, ctkGT, ctkTILDE]) then
     Raise ECSSParser.CreateFmt(SUnaryInvalidToken,[CurrentTokenString]);
+  op:=TokenToUnaryOperation(CurrentToken);
+  Consume(CurrentToken);
+  if CurrentToken=ctkWHITESPACE then
+    Raise ECSSParser.CreateFmt(SUnaryInvalidToken,['white space']);
+  El:=ParseComponentValue;
+
   Un:=TCSSUnaryElement(CreateElement(CSSUnaryElementClass));
-  try
-    op:=TokenToUnaryOperation(CurrentToken);
-    Un.Operation:=op;
-    Consume(CurrentToken);
-    Un.Right:=ParseComponentValue;
-    Result:=Un;
-    Un:=nil;
-  finally
-    Un.Free;
-  end;
+  Un.Operation:=op;
+  Un.Right:=El;
+  Result:=Un;
 end;
 
 function TCSSParser.ParseComponentValueList(AllowRules : Boolean = True): TCSSElement;
@@ -1099,7 +1150,16 @@ var
 
 begin
   aToken:=CurrentToken;
+  if aToken=ctkUNKNOWN then
+    begin
+    DoError('invalid');
+    repeat
+      GetNextToken;
+    until CurrentToken<>ctkUNKNOWN;
+    aToken:=CurrentToken;
+    end;
   Case aToken of
+    ctkEOF: exit(nil);
     ctkLPARENTHESIS: Result:=ParseParenthesis;
     ctkURL: Result:=ParseURL;
     ctkPSEUDO: Result:=ParsePseudo;
@@ -1111,18 +1171,17 @@ begin
     ctkGT,
     ctkTilde: Result:=ParseUnary;
     ctkUnicodeRange: Result:=ParseUnicodeRange;
-    ctkSTRING,
-    ctkHASH : Result:=ParseString;
+    ctkSTRING: Result:=ParseString;
+    ctkHASH: Result:=ParseColor;
     ctkINTEGER: Result:=ParseInteger;
     ctkFloat : Result:=ParseFloat;
     ctkPSEUDOFUNCTION,
-    ctkFUNCTION : Result:=ParseCall('');
+    ctkFUNCTION : Result:=ParseCall('',false);
     ctkSTAR: Result:=ParseInvalidToken;
-    ctkIDENTIFIER: Result:=ParseIdentifier;
+    ctkIDENTIFIER,ctkPERCENTAGE: Result:=ParseIdentifier;
     ctkCLASSNAME : Result:=ParseClassName;
   else
     Result:=nil;
-//    Consume(aToken);// continue
   end;
   if aToken in FinalTokens then
     exit;
@@ -1142,7 +1201,7 @@ function TCSSParser.ParseSelector: TCSSElement;
       ctkCLASSNAME : Result:=ParseClassName;
       ctkLBRACKET: Result:=ParseAttributeSelector;
       ctkPSEUDO: Result:=ParsePseudo;
-      ctkPSEUDOFUNCTION: Result:=ParseCall('');
+      ctkPSEUDOFUNCTION: Result:=ParseCall('',true);
     else
       DoWarn(SErrUnexpectedToken ,[
                GetEnumName(TypeInfo(TCSSToken),Ord(CurrentToken)),
@@ -1244,6 +1303,7 @@ begin
       Bin.Free;
       end;
   end;
+  SkipWhiteSpace;
 end;
 
 function TCSSParser.ParseAttributeSelector: TCSSElement;
@@ -1334,14 +1394,15 @@ function TCSSParser.ParseDeclaration(aIsAt: Boolean = false): TCSSDeclarationEle
 Var
   aDecl : TCSSDeclarationElement;
   aKey,aValue : TCSSElement;
-  aPrevDisablePseudo : Boolean;
   aList : TCSSListElement;
+  OldOptions: TCSSScannerOptions;
 
 begin
   aList:=nil;
-  aDecl:= TCSSDeclarationElement(CreateElement(CSSDeclarationElementClass));
+  OldOptions:=Scanner.Options;
+  aDecl:=TCSSDeclarationElement(CreateElement(CSSDeclarationElementClass));
   try
-    aPrevDisablePseudo:= Scanner.DisablePseudo;
+    // read attribute names
     Scanner.DisablePseudo:=True;
     aKey:=ParseComponentValue;
     aDecl.AddKey(aKey);
@@ -1350,7 +1411,7 @@ begin
       While (CurrentToken=ctkCOMMA) do
         begin
         while (CurrentToken=ctkCOMMA) do
-          Consume(ctkCOMMA);
+          GetNextToken;
         aKey:=ParseComponentValue;
         aDecl.AddKey(aKey);
         end;
@@ -1366,12 +1427,13 @@ begin
       if aDecl.Colon then
         Consume(ctkColon)
       end;
-    Scanner.DisablePseudo:=aPrevDisablePseudo;
     aValue:=ParseComponentValue;
     aList:=TCSSListElement(CreateElement(CSSListElementClass));
     aList.AddChild(aValue);
     if aDecl.Colon then
       begin
+      // read attribute value
+      // + and - must be enclosed in whitespace, +3 and -4 are values
       While not (CurrentToken in [ctkEOF,ctkSemicolon,ctkRBRACE,ctkImportant]) do
         begin
         While CurrentToken=ctkCOMMA do
@@ -1395,23 +1457,21 @@ begin
     Result:=aDecl;
     aDecl:=nil;
   finally
-    Scanner.DisablePseudo:=False;
+    Scanner.Options:=OldOptions;
     aDecl.Free;
     aList.Free;
   end;
 end;
 
-function TCSSParser.ParseCall(aName : TCSSString): TCSSElement;
-
+function TCSSParser.ParseCall(aName: TCSSString; IsSelector: boolean
+  ): TCSSCallElement;
 var
   aCall : TCSSCallElement;
   l : Integer;
-  OldReturnWhiteSpace: Boolean;
   aValue: TCSSElement;
 begin
-  OldReturnWhiteSpace:=Scanner.ReturnWhiteSpace;
-  Scanner.ReturnWhiteSpace:=false;
-  aCall:=TCSSCallElement(CreateELement(CSSCallElementClass));
+  if IsSelector then ;
+  aCall:=TCSSCallElement(CreateElement(CSSCallElementClass));
   try
     if (aName='') then
       aName:=CurrentTokenString;
@@ -1419,9 +1479,10 @@ begin
     if (L>0) and (aName[L]='(') then
       aName:=Copy(aName,1,L-1);
     aCall.Name:=aName;
-    if CurrentToken=ctkPSEUDOFUNCTION then
+    if IsSelector and (CurrentToken=ctkPSEUDOFUNCTION) then
       begin
       Consume(ctkPSEUDOFUNCTION);
+      SkipWhiteSpace;
       case aName of
       ':not',':is',':where':
         ParseSelectorCommaList(aCall);
@@ -1431,8 +1492,9 @@ begin
         ParseNthChildParams(aCall);
       end;
       end
-    else
+    else begin
       Consume(ctkFUNCTION);
+    end;
     // Call argument list can be empty: mask()
     While not (CurrentToken in [ctkRPARENTHESIS,ctkEOF]) do
       begin
@@ -1444,7 +1506,7 @@ begin
       end;
       aCall.AddArg(aValue);
       if (CurrentToken=ctkCOMMA) then
-        Consume(ctkCOMMA);
+        GetNextToken;
       end;
     if CurrentToken=ctkEOF then
       DoError(SErrUnexpectedEndOfFile,[aName]);
@@ -1452,7 +1514,6 @@ begin
     Result:=aCall;
     aCall:=nil;
   finally
-    Scanner.ReturnWhiteSpace:=OldReturnWhiteSpace;
     aCall.Free;
   end;
 end;
@@ -1464,12 +1525,12 @@ begin
   while not (CurrentToken in [ctkEOF,ctkRBRACKET,ctkRBRACE,ctkRPARENTHESIS]) do
     begin
     El:=ParseSelector;
-    if EL=nil then exit;
+    if El=nil then exit;
     aCall.AddArg(El);
-    SkipWhiteSpace;
     if CurrentToken<>ctkCOMMA then
       exit;
     GetNextToken;
+    SkipWhiteSpace;
   end;
 end;
 
@@ -1562,36 +1623,42 @@ begin
 
   if CurrentToken in [ctkMINUS,ctkPLUS] then
     aCall.AddArg(ParseUnary);
-  if CurrentToken=ctkINTEGER then
-    aCall.AddArg(ParseInteger);
   if (CurrentToken=ctkIDENTIFIER) and SameText(CurrentTokenString,'of') then
     begin
     aCall.AddArg(ParseIdentifier);
+    SkipWhiteSpace;
     aCall.AddArg(ParseSelector);
+    SkipWhiteSpace;
     end;
 end;
 
 function TCSSParser.ParseString: TCSSElement;
+var
+  aStr: TCSSStringElement;
+  aValue: TCSSString;
+begin
+  aValue:=CurrentTokenString;
+  aStr:=TCSSStringElement(CreateElement(CSSStringElementClass));
+  try
+    aStr.Value:=aValue;
+    Consume(ctkSTRING);
+    Result:=aStr;
+    aStr:=nil;
+  finally
+    aStr.Free;
+  end;
+end;
 
-Var
-  aValue : TCSSString;
-  aEl : TCSSElement;
-  aStr : TCSSStringElement;
-
+function TCSSParser.ParseColor: TCSSElement;
+var
+  aStr: TCSSStringElement;
+  aValue: TCSSString;
 begin
   aValue:=CurrentTokenString;
   aStr:=TCSSStringElement(CreateElement(CSSStringElementClass));
   try
-    if CurrentToken=ctkSTRING then
-      Consume(ctkSTRING)
-    else
-      Consume(ctkHASH); // e.g. #rrggbb
     aStr.Value:=aValue;
-    While (CurrentToken in [ctkIDENTIFIER,ctkSTRING,ctkINTEGER,ctkFLOAT,ctkHASH]) do
-      begin
-      aEl:=ParseComponentValue;
-      aStr.Children.Add(aEl);
-      end;
+    Consume(ctkHASH); // e.g. #rrggbb
     Result:=aStr;
     aStr:=nil;
   finally

File diff suppressed because it is too large
+ 658 - 264
src/base/fcl-css/fpcssresolver.pas


+ 2694 - 0
src/base/fcl-css/fpcssresparser.pas

@@ -0,0 +1,2694 @@
+{
+    This file is part of the Free Pascal Run time library.
+    Copyright (c) 2023 by Michael Van Canneyt ([email protected])
+
+    This file contains CSS utility class
+
+    See the File COPYING.FPC, included in this distribution,
+    for details about the copyright.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
+
+ **********************************************************************
+}
+{$IFNDEF FPC_DOTTEDUNITS}
+unit fpCSSResParser;
+{$ENDIF FPC_DOTTEDUNITS}
+
+{$mode ObjFPC}{$H+}
+{$Interfaces CORBA}
+{$ModeSwitch AdvancedRecords}
+
+{$IF FPC_FULLVERSION>30300}
+  {$WARN 6060 off} // Case statement does not handle all possible cases
+{$ENDIF}
+{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
+interface
+
+{$IFDEF FPC_DOTTEDUNITS}
+uses
+  System.Classes, System.SysUtils, System.Math, System.Contnrs, System.StrUtils,
+  Fcl.AVLTree, FpCss.Tree, FpCss.Scanner, FpCss.Parser;
+{$ELSE FPC_DOTTEDUNITS}
+uses
+  Classes, SysUtils, Math, Contnrs, AVL_Tree, fpCSSTree, fpCSSScanner,
+  fpCSSParser;
+{$ENDIF FPC_DOTTEDUNITS}
+
+const
+  CSSIDNone = 0;
+  // built-in attribute IDs
+  CSSAttributeID_ID = 1; // id of attribute key 'id'
+  CSSAttributeID_Class = 2; // id of attribute key 'class'
+  CSSAttributeID_All = 3; // id of attribute key 'all'
+  CSSAttributeID_LastResolver = CSSAttributeID_All;
+
+  // built-in type IDs
+  CSSTypeID_Universal = 1; // id of type '*'
+  CSSTypeID_LastResolver = CSSTypeID_Universal;
+
+  // built-in pseudo class IDs
+  CSSPseudoID_Root = 1; // :root
+  CSSPseudoID_Empty = CSSPseudoID_Root+1; // :empty
+  CSSPseudoID_FirstChild = CSSPseudoID_Empty+1; // :first-child
+  CSSPseudoID_LastChild = CSSPseudoID_FirstChild+1; // :last-child
+  CSSPseudoID_OnlyChild = CSSPseudoID_LastChild+1; // :only-child
+  CSSPseudoID_FirstOfType = CSSPseudoID_OnlyChild+1; // :first-of-type
+  CSSPseudoID_LastOfType = CSSPseudoID_FirstOfType+1; // :last-of-type
+  CSSPseudoID_OnlyOfType = CSSPseudoID_LastOfType+1; // :only-of-type
+  CSSPseudoID_LastResolver = CSSPseudoID_OnlyOfType;
+
+  CSSPseudoClassNames: array[0..CSSPseudoID_LastResolver] of TCSSString = (
+    '?',
+    ':root',
+    ':empty',
+    ':first-child',
+    ':last-child',
+    ':only-child',
+    ':first-of-type',
+    ':last-of-type',
+    ':only-of-type'
+    );
+
+  // built-in pseudo function IDs
+  CSSCallID_Not = 1; // :not()
+  CSSCallID_Is = CSSCallID_Not+1; // :is()
+  CSSCallID_Where = CSSCallID_Is+1; // :where()
+  CSSCallID_Has = CSSCallID_Where+1; // :has()
+  CSSCallID_NthChild = CSSCallID_Has+1; // :nth-child(n)
+  CSSCallID_NthLastChild = CSSCallID_NthChild+1; // :nth-last-child(n)
+  CSSCallID_NthOfType = CSSCallID_NthLastChild+1; // :nth-of-type(n)
+  CSSCallID_NthLastOfType = CSSCallID_NthOfType+1; // :nth-last-of-type(n)
+  CSSCallID_LastResolver = CSSCallID_NthLastOfType;
+
+  CSSSelectorCallNames: array[0..CSSCallID_LastResolver] of TCSSString = (
+    '?',
+    ':not()',
+    ':is()',
+    ':where()',
+    ':has()',
+    ':nth-child(n)',
+    ':nth-last-child(n)',
+    ':nth-of-type(n)',
+    ':nth-last-of-type(n)'
+    );
+
+  // keywords
+  CSSKeywordNone = 1;
+  CSSKeywordInitial = CSSKeywordNone+1;
+  CSSKeywordInherit = CSSKeywordInitial+1;
+  CSSKeywordUnset = CSSKeywordInherit+1;
+  CSSKeywordRevert = CSSKeywordUnset+1;
+  CSSKeywordRevertLayer = CSSKeywordRevert+1;
+  CSSKeywordAuto = CSSKeywordRevertLayer+1;
+  CSSKeyword_LastResolver = CSSKeywordAuto;
+
+  // attribute functions
+  CSSAttrFuncVar = 1;
+
+  CSSMinSafeIntDouble = -$1fffffffffffff; // -9007199254740991 54 bits (52 plus signed bit plus implicit highest bit)
+  CSSMaxSafeIntDouble =  $1fffffffffffff; //  9007199254740991
+
+type
+  TCSSMsgID = int64; // used for message numbers, e.g. error codes
+  TCSSNumericalID = integer; // used for IDs of each type and attribute
+  TCSSNumericalIDArray = array of TCSSNumericalID;
+
+  TCSSNumericalIDKind = (
+    nikAttribute,
+    nikPseudoClass, // e.g. "hover" of ":hover"
+    nikPseudoFunction, // e.g. "is" of ":is()"
+    nikType,
+    nikKeyword,
+    nikAttrFunction // e.g. "calc" of "calc()"
+    );
+  TCSSNumericalIDs = set of TCSSNumericalIDKind;
+
+const
+  nikAllDescriptors = [nikAttribute,nikPseudoClass,nikType]; // all items having a descriptor
+
+  CSSNumericalIDKindNames: array[TCSSNumericalIDKind] of TCSSString = (
+    'Type',
+    'PseudoClass',
+    'PseudoFunction',
+    'Attribute',
+    'Keyword',
+    'AttributeFunction'
+    );
+
+type
+  TCSSAlphaColor = DWord;
+
+  TCSSNamedColor = record
+    Name: TCSSString;
+    Color: TCSSAlphaColor;
+  end;
+const
+  CSSNamedColors: array[0..148] of TCSSNamedColor = (
+    (Name: 'aliceblue'; Color: TCSSAlphaColor($fff0f8ff)),
+    (Name: 'antiquewhite'; Color: TCSSAlphaColor($fffaebd7)),
+    (Name: 'aqua'; Color: TCSSAlphaColor($ff00ffff)),
+    (Name: 'aquamarine'; Color: TCSSAlphaColor($ff7fffd4)),
+    (Name: 'azure'; Color: TCSSAlphaColor($fff0ffff)),
+    (Name: 'beige'; Color: TCSSAlphaColor($fff5f5dc)),
+    (Name: 'bisque'; Color: TCSSAlphaColor($ffffe4c4)),
+    (Name: 'black'; Color: TCSSAlphaColor($ff000000)),
+    (Name: 'blanchedalmond'; Color: TCSSAlphaColor($ffffebcd)),
+    (Name: 'blue'; Color: TCSSAlphaColor($ff0000ff)),
+    (Name: 'blueviolet'; Color: TCSSAlphaColor($ff8a2be2)),
+    (Name: 'brown'; Color: TCSSAlphaColor($ffa52a2a)),
+    (Name: 'burlywood'; Color: TCSSAlphaColor($ffdeb887)),
+    (Name: 'cadetblue'; Color: TCSSAlphaColor($ff5f9ea0)),
+    (Name: 'chartreuse'; Color: TCSSAlphaColor($ff7fff00)),
+    (Name: 'chocolate'; Color: TCSSAlphaColor($ffd2691e)),
+    (Name: 'coral'; Color: TCSSAlphaColor($ffff7f50)),
+    (Name: 'cornflowerblue'; Color: TCSSAlphaColor($ff6495ed)),
+    (Name: 'cornsilk'; Color: TCSSAlphaColor($fffff8dc)),
+    (Name: 'crimson'; Color: TCSSAlphaColor($ffdc143c)),
+    (Name: 'cyan'; Color: TCSSAlphaColor($ff00ffff)),
+    (Name: 'darkblue'; Color: TCSSAlphaColor($ff00008b)),
+    (Name: 'darkcyan'; Color: TCSSAlphaColor($ff008b8b)),
+    (Name: 'darkgoldenrod'; Color: TCSSAlphaColor($ffb8860b)),
+    (Name: 'darkgray'; Color: TCSSAlphaColor($ffa9a9a9)),
+    (Name: 'darkgreen'; Color: TCSSAlphaColor($ff006400)),
+    (Name: 'darkgrey'; Color: TCSSAlphaColor($ffa9a9a9)),
+    (Name: 'darkkhaki'; Color: TCSSAlphaColor($ffbdb76b)),
+    (Name: 'darkmagenta'; Color: TCSSAlphaColor($ff8b008b)),
+    (Name: 'darkolivegreen'; Color: TCSSAlphaColor($ff556b2f)),
+    (Name: 'darkorange'; Color: TCSSAlphaColor($ffff8c00)),
+    (Name: 'darkorchid'; Color: TCSSAlphaColor($ff9932cc)),
+    (Name: 'darkred'; Color: TCSSAlphaColor($ff8b0000)),
+    (Name: 'darksalmon'; Color: TCSSAlphaColor($ffe9967a)),
+    (Name: 'darkseagreen'; Color: TCSSAlphaColor($ff8fbc8f)),
+    (Name: 'darkslateblue'; Color: TCSSAlphaColor($ff483d8b)),
+    (Name: 'darkslategray'; Color: TCSSAlphaColor($ff2f4f4f)),
+    (Name: 'darkslategrey'; Color: TCSSAlphaColor($ff2f4f4f)),
+    (Name: 'darkturquoise'; Color: TCSSAlphaColor($ff00ced1)),
+    (Name: 'darkviolet'; Color: TCSSAlphaColor($ff9400d3)),
+    (Name: 'deeppink'; Color: TCSSAlphaColor($ffff1493)),
+    (Name: 'deepskyblue'; Color: TCSSAlphaColor($ff00bfff)),
+    (Name: 'dimgray'; Color: TCSSAlphaColor($ff696969)),
+    (Name: 'dimgrey'; Color: TCSSAlphaColor($ff696969)),
+    (Name: 'dodgerblue'; Color: TCSSAlphaColor($ff1e90ff)),
+    (Name: 'firebrick'; Color: TCSSAlphaColor($ffb22222)),
+    (Name: 'floralwhite'; Color: TCSSAlphaColor($fffffaf0)),
+    (Name: 'forestgreen'; Color: TCSSAlphaColor($ff228b22)),
+    (Name: 'fuchsia'; Color: TCSSAlphaColor($ffff00ff)),
+    (Name: 'gainsboro'; Color: TCSSAlphaColor($ffdcdcdc)),
+    (Name: 'ghostwhite'; Color: TCSSAlphaColor($fff8f8ff)),
+    (Name: 'gold'; Color: TCSSAlphaColor($ffffd700)),
+    (Name: 'goldenrod'; Color: TCSSAlphaColor($ffdaa520)),
+    (Name: 'gray'; Color: TCSSAlphaColor($ff808080)),
+    (Name: 'green'; Color: TCSSAlphaColor($ff008000)),
+    (Name: 'greenyellow'; Color: TCSSAlphaColor($ffadff2f)),
+    (Name: 'grey'; Color: TCSSAlphaColor($ff808080)),
+    (Name: 'honeydew'; Color: TCSSAlphaColor($fff0fff0)),
+    (Name: 'hotpink'; Color: TCSSAlphaColor($ffff69b4)),
+    (Name: 'indianred'; Color: TCSSAlphaColor($ffcd5c5c)),
+    (Name: 'indigo'; Color: TCSSAlphaColor($ff4b0082)),
+    (Name: 'ivory'; Color: TCSSAlphaColor($fffffff0)),
+    (Name: 'khaki'; Color: TCSSAlphaColor($fff0e68c)),
+    (Name: 'lavender'; Color: TCSSAlphaColor($ffe6e6fa)),
+    (Name: 'lavenderblush'; Color: TCSSAlphaColor($fffff0f5)),
+    (Name: 'lawngreen'; Color: TCSSAlphaColor($ff7cfc00)),
+    (Name: 'lemonchiffon'; Color: TCSSAlphaColor($fffffacd)),
+    (Name: 'lightblue'; Color: TCSSAlphaColor($ffadd8e6)),
+    (Name: 'lightcoral'; Color: TCSSAlphaColor($fff08080)),
+    (Name: 'lightcyan'; Color: TCSSAlphaColor($ffe0ffff)),
+    (Name: 'lightgoldenrodyellow'; Color: TCSSAlphaColor($fffafad2)),
+    (Name: 'lightgray'; Color: TCSSAlphaColor($ffd3d3d3)),
+    (Name: 'lightgreen'; Color: TCSSAlphaColor($ff90ee90)),
+    (Name: 'lightgrey'; Color: TCSSAlphaColor($ffd3d3d3)),
+    (Name: 'lightpink'; Color: TCSSAlphaColor($ffffb6c1)),
+    (Name: 'lightsalmon'; Color: TCSSAlphaColor($ffffa07a)),
+    (Name: 'lightseagreen'; Color: TCSSAlphaColor($ff20b2aa)),
+    (Name: 'lightskyblue'; Color: TCSSAlphaColor($ff87cefa)),
+    (Name: 'lightslategray'; Color: TCSSAlphaColor($ff778899)),
+    (Name: 'lightslategrey'; Color: TCSSAlphaColor($ff778899)),
+    (Name: 'lightsteelblue'; Color: TCSSAlphaColor($ffb0c4de)),
+    (Name: 'lightyellow'; Color: TCSSAlphaColor($ffffffe0)),
+    (Name: 'lime'; Color: TCSSAlphaColor($ff00ff00)),
+    (Name: 'limegreen'; Color: TCSSAlphaColor($ff32cd32)),
+    (Name: 'linen'; Color: TCSSAlphaColor($fffaf0e6)),
+    (Name: 'magenta'; Color: TCSSAlphaColor($ffff00ff)),
+    (Name: 'maroon'; Color: TCSSAlphaColor($ff800000)),
+    (Name: 'mediumaquamarine'; Color: TCSSAlphaColor($ff66cdaa)),
+    (Name: 'mediumblue'; Color: TCSSAlphaColor($ff0000cd)),
+    (Name: 'mediumorchid'; Color: TCSSAlphaColor($ffba55d3)),
+    (Name: 'mediumpurple'; Color: TCSSAlphaColor($ff9370db)),
+    (Name: 'mediumseagreen'; Color: TCSSAlphaColor($ff3cb371)),
+    (Name: 'mediumslateblue'; Color: TCSSAlphaColor($ff7b68ee)),
+    (Name: 'mediumspringgreen'; Color: TCSSAlphaColor($ff00fa9a)),
+    (Name: 'mediumturquoise'; Color: TCSSAlphaColor($ff48d1cc)),
+    (Name: 'mediumvioletred'; Color: TCSSAlphaColor($ffc71585)),
+    (Name: 'midnightblue'; Color: TCSSAlphaColor($ff191970)),
+    (Name: 'mintcream'; Color: TCSSAlphaColor($fff5fffa)),
+    (Name: 'mistyrose'; Color: TCSSAlphaColor($ffffe4e1)),
+    (Name: 'moccasin'; Color: TCSSAlphaColor($ffffe4b5)),
+    (Name: 'navajowhite'; Color: TCSSAlphaColor($ffffdead)),
+    (Name: 'navy'; Color: TCSSAlphaColor($ff000080)),
+    (Name: 'oldlace'; Color: TCSSAlphaColor($fffdf5e6)),
+    (Name: 'olive'; Color: TCSSAlphaColor($ff808000)),
+    (Name: 'olivedrab'; Color: TCSSAlphaColor($ff6b8e23)),
+    (Name: 'orange'; Color: TCSSAlphaColor($ffffa500)),
+    (Name: 'orangered'; Color: TCSSAlphaColor($ffff4500)),
+    (Name: 'orchid'; Color: TCSSAlphaColor($ffda70d6)),
+    (Name: 'palegoldenrod'; Color: TCSSAlphaColor($ffeee8aa)),
+    (Name: 'palegreen'; Color: TCSSAlphaColor($ff98fb98)),
+    (Name: 'paleturquoise'; Color: TCSSAlphaColor($ffafeeee)),
+    (Name: 'palevioletred'; Color: TCSSAlphaColor($ffdb7093)),
+    (Name: 'papayawhip'; Color: TCSSAlphaColor($ffffefd5)),
+    (Name: 'peachpuff'; Color: TCSSAlphaColor($ffffdab9)),
+    (Name: 'peru'; Color: TCSSAlphaColor($ffcd853f)),
+    (Name: 'pink'; Color: TCSSAlphaColor($ffffc0cb)),
+    (Name: 'plum'; Color: TCSSAlphaColor($ffdda0dd)),
+    (Name: 'powderblue'; Color: TCSSAlphaColor($ffb0e0e6)),
+    (Name: 'purple'; Color: TCSSAlphaColor($ff800080)),
+    (Name: 'rebeccapurple'; Color: TCSSAlphaColor($ff663399)),
+    (Name: 'red'; Color: TCSSAlphaColor($ffff0000)),
+    (Name: 'rosybrown'; Color: TCSSAlphaColor($ffbc8f8f)),
+    (Name: 'royalblue'; Color: TCSSAlphaColor($ff4169e1)),
+    (Name: 'saddlebrown'; Color: TCSSAlphaColor($ff8b4513)),
+    (Name: 'salmon'; Color: TCSSAlphaColor($fffa8072)),
+    (Name: 'sandybrown'; Color: TCSSAlphaColor($fff4a460)),
+    (Name: 'seagreen'; Color: TCSSAlphaColor($ff2e8b57)),
+    (Name: 'seashell'; Color: TCSSAlphaColor($fffff5ee)),
+    (Name: 'sienna'; Color: TCSSAlphaColor($ffa0522d)),
+    (Name: 'silver'; Color: TCSSAlphaColor($ffc0c0c0)),
+    (Name: 'skyblue'; Color: TCSSAlphaColor($ff87ceeb)),
+    (Name: 'slateblue'; Color: TCSSAlphaColor($ff6a5acd)),
+    (Name: 'slategray'; Color: TCSSAlphaColor($ff708090)),
+    (Name: 'slategrey'; Color: TCSSAlphaColor($ff708090)),
+    (Name: 'snow'; Color: TCSSAlphaColor($fffffafa)),
+    (Name: 'springgreen'; Color: TCSSAlphaColor($ff00ff7f)),
+    (Name: 'steelblue'; Color: TCSSAlphaColor($ff4682b4)),
+    (Name: 'tan'; Color: TCSSAlphaColor($ffd2b48c)),
+    (Name: 'teal'; Color: TCSSAlphaColor($ff008080)),
+    (Name: 'thistle'; Color: TCSSAlphaColor($ffd8bfd8)),
+    (Name: 'tomato'; Color: TCSSAlphaColor($ffff6347)),
+    (Name: 'transparent'; Color: TCSSAlphaColor($ff0)),
+    (Name: 'turquoise'; Color: TCSSAlphaColor($ff40e0d0)),
+    (Name: 'violet'; Color: TCSSAlphaColor($ffee82ee)),
+    (Name: 'wheat'; Color: TCSSAlphaColor($fff5deb3)),
+    (Name: 'white'; Color: TCSSAlphaColor($ffffffff)),
+    (Name: 'whitesmoke'; Color: TCSSAlphaColor($fff5f5f5)),
+    (Name: 'yellow'; Color: TCSSAlphaColor($ffffff00)),
+    (Name: 'yellowgreen'; Color: TCSSAlphaColor($ff9acd32))
+  );
+
+
+type
+
+  { TCSSRegistryNamedItem }
+
+  TCSSRegistryNamedItem = class
+  public
+    Name: TCSSString; // case sensitive
+    Index: TCSSNumericalID;
+  end;
+
+  { TCSSPseudoClassDesc }
+
+  TCSSPseudoClassDesc = class(TCSSRegistryNamedItem)
+  public
+  end;
+  TCSSPseudoClassDescClass = class of TCSSPseudoClassDesc;
+  TCSSPseudoClassDescArray = array of TCSSPseudoClassDesc;
+
+  { TCSSTypeDesc }
+
+  TCSSTypeDesc = class(TCSSRegistryNamedItem)
+  public
+  end;
+  TCSSTypeDescClass = class of TCSSTypeDesc;
+  TCSSTypeDescArray = array of TCSSTypeDesc;
+
+  { TCSSAttributeKeyData }
+
+  TCSSAttributeKeyData = class(TCSSElementOwnedData)
+  public
+    Invalid: boolean;
+    Complete: boolean;
+    Value: TCSSString;
+  end;
+  TCSSAttributeKeyDataClass = class of TCSSAttributeKeyData;
+
+  TCSSBaseResolver = class;
+  TCSSResolverParser = class;
+
+  { TCSSAttributeDesc - general properties of a CSS attribute }
+
+  TCSSAttributeDesc = class(TCSSRegistryNamedItem)
+  public
+    type
+      TCheckEvent = function(Resolver: TCSSBaseResolver): boolean of object;
+      TSplitShorthandEvent = procedure(Resolver: TCSSBaseResolver;
+           var AttrIDs: TCSSNumericalIDArray; var Values: TCSSStringArray) of object;
+  public
+    Inherits: boolean; // true = the default value is the parent's value
+    All: boolean; // true = can be changed by the 'all' attribute
+    InitialValue: TCSSString;
+    CompProps: array of TCSSAttributeDesc; // if this attribute is a shorthand,
+      // these are the component properties (longhands + sub-shorthands like border-width)
+      // used by the cascade algorithm to delete all overwritten properties
+    OnCheck: TCheckEvent; // called by the parser after reading a declaration and there is no var()
+      // return false if invalid, so the resolver skips this declaration
+    OnSplitShorthand: TSplitShorthandEvent; // called by resolver after resolving var(), if any value is empty, the initialvalue is used
+  end;
+  TCSSAttributeDescClass = class of TCSSAttributeDesc;
+  TCSSAttributeDescArray = array of TCSSAttributeDesc;
+
+  { TCSSRegistry }
+
+  TCSSRegistry = class
+  private
+    FAttrFunctionCount: TCSSNumericalID;
+    FAttributeCount: TCSSNumericalID;
+    FHashLists: array[TCSSNumericalIDKind] of TFPHashList; // name to TCSSRegistryNamedItem
+    FKeywordCount: TCSSNumericalID;
+    FPseudoClassCount: TCSSNumericalID;
+    FPseudoFunctionCount: TCSSNumericalID;
+    FStamp, FModifiedStamp: TCSSNumericalID;
+    FTypeCount: TCSSNumericalID;
+    function GetModified: boolean;
+    procedure SetModified(const AValue: boolean);
+  public
+    constructor Create;
+    procedure Init; virtual; // add basic items
+    destructor Destroy; override;
+    function FindNamedItem(Kind: TCSSNumericalIDKind; const aName: TCSSString): TCSSRegistryNamedItem; overload;
+    function IndexOfNamedItem(Kind: TCSSNumericalIDKind; const aName: TCSSString): TCSSNumericalID; overload;
+    procedure ConsistencyCheck; virtual;
+    procedure ChangeStamp;
+    property Stamp: TCSSNumericalID read FStamp; // always >0
+    property Modified: boolean read GetModified write SetModified;
+  public
+    // attributes
+    Attributes: TCSSAttributeDescArray; // Note: Attributes[0] is nil to spot bugs easily
+    Attribute_ClassOf: TCSSAttributeDescClass;
+    NotAllAttributes: TCSSAttributeDescArray;
+    function AddAttribute(Attr: TCSSAttributeDesc): TCSSAttributeDesc; overload;
+    function AddAttribute(const aName: TCSSString; const aInitialValue: TCSSString = '';
+      aInherits: boolean = false; aAll: boolean = true; aClass: TCSSAttributeDescClass = nil): TCSSAttributeDesc; overload;
+    function FindAttribute(const aName: TCSSString): TCSSAttributeDesc; overload;
+    function IndexOfAttributeName(const aName: TCSSString): TCSSNumericalID; overload;
+    procedure AddSplitLonghand(var AttrIDs: TCSSNumericalIDArray; var Values: TCSSStringArray; AttrID: TCSSNumericalID; const Value: TCSSString); overload;
+    procedure AddSplitLonghandSides(var AttrIDs: TCSSNumericalIDArray; var Values: TCSSStringArray;
+      TopID, RightID, BottomID, LeftID: TCSSNumericalID; const Found: TCSSStringArray); overload;
+    procedure AddSplitLonghandCorners(var AttrIDs: TCSSNumericalIDArray; var Values: TCSSStringArray;
+      TopLeftID, TopRightID, BottomLeftID, BottomRightID: TCSSNumericalID; const Found: TCSSStringArray); overload;
+    property AttributeCount: TCSSNumericalID read FAttributeCount;
+  public
+    // pseudo classes
+    PseudoClasses: TCSSPseudoClassDescArray; // Note: PseudoClasses[0] is nil to spot bugs easily
+    PseudoClass_ClassOf: TCSSPseudoClassDescClass;
+    function AddPseudoClass(aPseudo: TCSSPseudoClassDesc): TCSSPseudoClassDesc; overload;
+    function AddPseudoClass(const aName: TCSSString; aClass: TCSSPseudoClassDescClass = nil): TCSSPseudoClassDesc; overload;
+    function FindPseudoClass(const aName: TCSSString): TCSSPseudoClassDesc; overload;
+    function IndexOfPseudoClassName(const aName: TCSSString): TCSSNumericalID; overload;
+    property PseudoClassCount: TCSSNumericalID read FPseudoClassCount;
+  public
+    // pseudo functions lowercase (they are parsed case insensitive)
+    PseudoFunctions: TCSSStringArray;
+    function AddPseudoFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    function IndexOfPseudoFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    property PseudoFunctionCount: TCSSNumericalID read FPseudoFunctionCount;
+  public
+    // types
+    Types: TCSSTypeDescArray; // Note: Types[0] is nil to spot bugs easily
+    Type_ClassOf: TCSSTypeDescClass;
+    function AddType(aType: TCSSTypeDesc): TCSSTypeDesc; overload;
+    function AddType(const aName: TCSSString; aClass: TCSSTypeDescClass = nil): TCSSTypeDesc; overload;
+    function FindType(const aName: TCSSString): TCSSTypeDesc; overload;
+    function IndexOfTypeName(const aName: TCSSString): TCSSNumericalID; overload;
+    property TypeCount: TCSSNumericalID read FTypeCount;
+  public
+    // keywords
+    Keywords: TCSSStringArray;
+    kwFirstColor, kwLastColor, kwTransparent: TCSSNumericalID;
+    function AddKeyword(const aName: TCSSString): TCSSNumericalID; overload;
+    procedure AddKeywords(const Names: TCSSStringArray; out First, Last: TCSSNumericalID); overload;
+    function IndexOfKeyword(const aName: TCSSString): TCSSNumericalID; overload;
+    procedure AddColorKeywords; virtual;
+    function GetNamedColor(const aName: TCSSString): TCSSAlphaColor; virtual; overload;
+    function GetKeywordColor(KeywordID: TCSSNumericalID): TCSSAlphaColor; virtual; overload;
+    property KeywordCount: TCSSNumericalID read FKeywordCount;
+  public
+    // attribute functions
+    AttrFunctions: TCSSStringArray;
+    const afVar = CSSAttrFuncVar;
+    function AddAttrFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    function IndexOfAttrFunction(const aName: TCSSString): TCSSNumericalID; overload;
+    property AttrFunctionCount: TCSSNumericalID read FAttrFunctionCount;
+  end;
+
+  TCSSValueParserLogEvent = procedure(MsgType: TEventType; const ID: TCSSMsgID;
+                               const Msg: TCSSString; PosEl: TCSSElement) of object;
+
+  { TCSSResolvedIdentifierElement }
+
+  TCSSResolvedIdentifierElement = class(TCSSIdentifierElement)
+  public
+    NumericalID: TCSSNumericalID;
+    Kind: TCSSNumericalIDKind;
+  end;
+
+  { TCSSResolvedPseudoClassElement }
+
+  TCSSResolvedPseudoClassElement = class(TCSSPseudoClassElement)
+  public
+    NumericalID: TCSSNumericalID;
+    Kind: TCSSNumericalIDKind;
+  end;
+
+  { TCSSNthChildParams }
+
+  TCSSNthChildParams = class
+  public
+    Modulo: integer;
+    Start: integer;
+    HasOf: boolean; // for nth-of-type() HasOf=true and OfSelector=nil
+    OfSelector: TCSSElement;
+  end;
+  TCSSNthChildParamsClass = class of TCSSNthChildParams;
+
+  { TCSSResolvedCallElement }
+
+  TCSSResolvedCallElement = class(TCSSCallElement)
+  public
+    NameNumericalID: TCSSNumericalID;
+    Kind: TCSSNumericalIDKind;
+    Params: TObject; // e.g. TCSSNthChildParams
+    destructor Destroy; override;
+  end;
+
+  { TCSSValueData }
+
+  TCSSValueData = class(TCSSElementOwnedData)
+  public
+    NormValue: TCSSString; // normalized value, stripped of comments and e.g. '0.1' instead of '000.100' or '.1'
+  end;
+
+  TCSSResValueKind = (
+    rvkNone,
+    rvkInvalid,
+    rvkSymbol,
+    rvkFloat,
+    rvkCalcFloat,
+    rvkKeyword,
+    rvkKeywordUnknown,
+    rvkFunction,
+    rvkFunctionUnknown,
+    rvkString,
+    rvkHexColor
+    );
+
+  { TCSSResCompValue }
+
+  TCSSResCompValue = record
+    Kind: TCSSResValueKind;
+    StartP, EndP: PCSSChar;
+    function AsString: TCSSString;
+    function FloatAsString: TCSSString;
+    case longint of
+    1: (Float: Double; FloatUnit: TCSSUnit);
+    2: (KeywordID: TCSSNumericalID);
+    3: (FunctionID: TCSSNumericalID; BracketOpen: PCSSChar);
+    4: (Symbol: TCSSToken);
+  end;
+
+  { TCSSCheckAttrParams_Dimension }
+
+  TCSSCheckAttrParams_Dimension = record
+  public
+    AllowedUnits: TCSSUnits;
+    AllowNegative, AllowFrac: boolean;
+    AllowedKeywordIDs: TCSSNumericalIDArray;
+    function Fits(const ResValue: TCSSResCompValue): boolean; overload;
+  end;
+
+  { TCSSBaseResolver }
+
+  TCSSBaseResolver = class(TComponent)
+  private
+    FCSSRegistry: TCSSRegistry;
+  protected
+    procedure SetCSSRegistry(const AValue: TCSSRegistry); virtual;
+  public
+    CurAttrData: TCSSAttributeKeyData;
+    CurDesc: TCSSAttributeDesc;
+    CurValue: TCSSString;
+    CurComp: TCSSResCompValue;
+    function InitParseAttr(Desc: TCSSAttributeDesc; AttrData: TCSSAttributeKeyData; const Value: TCSSString): boolean; virtual; // true if parsing can start
+    procedure InitParseAttr(const Value: TCSSString); virtual;
+    // check whole attribute, skipping invalid values, emit warnings:
+    function CheckAttribute_Keyword(const AllowedKeywordIDs: TCSSNumericalIDArray): boolean; virtual;
+    function CheckAttribute_CommaList_Keyword(const AllowedKeywordIDs: TCSSNumericalIDArray): boolean; virtual;
+    function CheckAttribute_Dimension(const Params: TCSSCheckAttrParams_Dimension): boolean; virtual;
+    function CheckAttribute_Color(const AllowedKeywordIDs: TCSSNumericalIDArray): boolean; virtual;
+    // parse whole attribute, skipping invalid values:
+    function ReadNext: boolean;
+    function ReadAttribute_Keyword(out Invalid: boolean; const AllowedKeywordIDs: TCSSNumericalIDArray): boolean; virtual;
+    function ReadAttribute_Dimension(out Invalid: boolean; const Params: TCSSCheckAttrParams_Dimension): boolean; virtual;
+    function ReadAttribute_Color(out Invalid: boolean; const AllowedKeywordIDs: TCSSNumericalIDArray): boolean; virtual;
+    function IsBaseKeyword(KeywordID: TCSSNumericalID): boolean;
+    function IsKeywordIn(aKeywordID: TCSSNumericalID; const KeywordIDs: TCSSNumericalIDArray): boolean; overload;
+    function IsKeywordIn(const KeywordIDs: TCSSNumericalIDArray): boolean; overload;
+    function IsLengthOrPercentage(AllowNegative: boolean): boolean; overload;
+    function IsLengthOrPercentage(const ResValue: TCSSResCompValue; AllowNegative: boolean): boolean; overload;
+    function IsSymbol(Token: TCSSToken): boolean; overload;
+    function GetCompString: TCSSString; overload;
+    function GetCompString(const aValue: string; const ResValue: TCSSResCompValue): TCSSString; overload;
+    // low level functions to parse attribute components
+    function ReadComp(var aComp: TCSSResCompValue): boolean; // true if parsing attribute can continue
+    procedure ReadWordID(var aComp: TCSSResCompValue);
+    class function ReadValue(var aComp: TCSSResCompValue): boolean; // true if parsing attribute can continue, not using CSSRegistry
+    class function ReadNumber(var aComp: TCSSResCompValue): boolean;
+    class function ReadIdentifier(var aComp: TCSSResCompValue): boolean;
+    class procedure SkipToEndOfAttribute(var p: PCSSChar);
+    class function SkipString(var p: PCSSChar): boolean;
+    class function SkipBrackets(var p: PCSSChar; Lvl: integer = 1): boolean;
+    // registry
+    function GetAttributeID(const aName: TCSSString; AutoCreate: boolean = false): TCSSNumericalID; virtual;
+    function GetAttributeDesc(AttrID: TCSSNumericalID): TCSSAttributeDesc; virtual;
+    function GetTypeID(const aName: TCSSString): TCSSNumericalID; virtual;
+    function GetPseudoClassID(const aName: TCSSString): TCSSNumericalID; virtual;
+    function GetPseudoFunctionID(const aName: TCSSString): TCSSNumericalID; virtual;
+
+    property CSSRegistry: TCSSRegistry read FCSSRegistry write SetCSSRegistry;
+  end;
+
+  { TCSSResolverParser
+    - resolves identifiers to IDs
+    - warns about constructs unsupported by resolver }
+
+  TCSSResolverParser = class(TCSSParser)
+  private
+    FOnLog: TCSSValueParserLogEvent;
+    FResolver: TCSSBaseResolver;
+  protected
+    function ResolveAttribute(El: TCSSResolvedIdentifierElement): TCSSNumericalID; virtual;
+    function ResolveType(El: TCSSResolvedIdentifierElement): TCSSNumericalID; virtual;
+    function ResolvePseudoClass(El: TCSSResolvedPseudoClassElement): TCSSNumericalID; virtual;
+    function ResolvePseudoFunction(El: TCSSResolvedCallElement): TCSSNumericalID; virtual;
+    function ParseCall(aName: TCSSString; IsSelector: boolean): TCSSCallElement; override;
+    function ParseDeclaration(aIsAt: Boolean): TCSSDeclarationElement; override;
+    function ParseSelector: TCSSElement; override;
+    procedure CheckSelector(El: TCSSElement); virtual;
+    procedure CheckSelectorArray(anArray: TCSSArrayElement); virtual;
+    procedure CheckSelectorArrayBinary(aBinary: TCSSBinaryElement); virtual;
+    procedure CheckSelectorBinary(aBinary: TCSSBinaryElement); virtual;
+    procedure CheckSelectorList(aList: TCSSListElement); virtual;
+    procedure CheckNthChildParams(aCall: TCSSResolvedCallElement); virtual;
+    function ComputeValue(El: TCSSElement): TCSSString; virtual;
+  public
+    CSSNthChildParamsClass: TCSSNthChildParamsClass;
+    CSSAttributeKeyDataClass: TCSSAttributeKeyDataClass;
+    constructor Create(AScanner: TCSSScanner); override; overload;
+    destructor Destroy; override;
+    procedure Log(MsgType: TEventType; const ID: TCSSMsgID; const Msg: TCSSString; PosEl: TCSSElement); virtual;
+    class function IsWhiteSpace(const s: TCSSString): boolean; virtual; overload;
+    property Resolver: TCSSBaseResolver read FResolver write FResolver;
+    property OnLog: TCSSValueParserLogEvent read FOnLog write FOnLog;
+  end;
+
+implementation
+
+Const
+  Alpha = ['A'..'Z','a'..'z'];
+  Num   = ['0'..'9'];
+  AlNum = Alpha+Num;
+  AlIden = Alpha+['-'];
+  AlNumIden = AlNum+['-'];
+  Whitespace = [#9,#10,#13,' '];
+  //WhitespaceZ = Whitespace+[#0];
+  Hex = ['0'..'9','a'..'z','A'..'Z'];
+  ValEnd = [#0,#10,#13,#9,' ',';',',','}',')',']']; // used for skipping a component value
+
+{ TCSSRegistry }
+
+procedure TCSSRegistry.SetModified(const AValue: boolean);
+begin
+  if AValue then
+    ChangeStamp
+  else
+    FModifiedStamp:=FStamp;
+end;
+
+function TCSSRegistry.GetModified: boolean;
+begin
+  Result:=FStamp=FModifiedStamp;
+end;
+
+constructor TCSSRegistry.Create;
+var
+  Kind: TCSSNumericalIDKind;
+  i: Integer;
+begin
+  for Kind in TCSSNumericalIDKind do
+    FHashLists[Kind]:=TFPHashList.Create;
+  if Attribute_ClassOf=nil then
+    Attribute_ClassOf:=TCSSAttributeDesc;
+  if PseudoClass_ClassOf=nil then
+    PseudoClass_ClassOf:=TCSSPseudoClassDesc;
+  if Type_ClassOf=nil then
+    Type_ClassOf:=TCSSTypeDesc;
+
+  // init attributes
+  SetLength(Attributes,32);
+  for i:=0 to length(Attributes)-1 do Attributes[i]:=nil;
+  FAttributeCount:=1; // index 0 is CSSIDNone
+
+  // init pseudo classes
+  SetLength(PseudoClasses,32);
+  for i:=0 to length(PseudoClasses)-1 do PseudoClasses[i]:=nil;
+  FPseudoClassCount:=1; // index 0 is CSSIDNone
+
+  // init pseudo functions
+  SetLength(PseudoFunctions,16);
+  FPseudoFunctionCount:=1; // index 0 is CSSIDNone
+
+  // init types
+  SetLength(Types,32);
+  for i:=0 to length(Types)-1 do Types[i]:=nil;
+  FTypeCount:=1; // index 0 is CSSIDNone
+
+  // init keywords
+  SetLength(Keywords,32);
+  FKeywordCount:=1; // index 0 is CSSIDNone
+
+  // init keywords
+  SetLength(AttrFunctions,32);
+  FAttrFunctionCount:=1; // index 0 is CSSIDNone
+end;
+
+procedure TCSSRegistry.Init;
+begin
+  // init attributes
+  if AddAttribute('id').Index<>CSSAttributeID_ID then
+    raise Exception.Create('20240617191749');
+  if AddAttribute('class').Index<>CSSAttributeID_Class then
+    raise Exception.Create('20240617191801');
+  if AddAttribute('all').Index<>CSSAttributeID_All then
+    raise Exception.Create('20240617191816');
+
+  // init pseudo classes
+  if AddPseudoClass('root').Index<>CSSPseudoID_Root then
+    raise Exception.Create('20240623165848');
+  if AddPseudoClass('empty').Index<>CSSPseudoID_Empty then
+    raise Exception.Create('20240623170450');
+  if AddPseudoClass('first-child').Index<>CSSPseudoID_FirstChild then
+    raise Exception.Create('20240623170508');
+  if AddPseudoClass('last-child').Index<>CSSPseudoID_LastChild then
+    raise Exception.Create('20240623170521');
+  if AddPseudoClass('only-child').Index<>CSSPseudoID_OnlyChild then
+    raise Exception.Create('20240623170534');
+  if AddPseudoClass('first-of-type').Index<>CSSPseudoID_FirstOfType then
+    raise Exception.Create('20240623170547');
+  if AddPseudoClass('last-of-type').Index<>CSSPseudoID_LastOfType then
+    raise Exception.Create('20240623170558');
+  if AddPseudoClass('only-of-type').Index<>CSSPseudoID_OnlyOfType then
+    raise Exception.Create('20240623170609');
+
+  // init pseudo functions
+  if AddPseudoFunction('not')<>CSSCallID_Not then
+    raise Exception.Create('20240625183757');
+  if AddPseudoFunction('is')<>CSSCallID_Is then
+    raise Exception.Create('20240625142038');
+  if AddPseudoFunction('where')<>CSSCallID_Where then
+    raise Exception.Create('20240625142049');
+  if AddPseudoFunction('has')<>CSSCallID_Has then
+    raise Exception.Create('20240625142104');
+  if AddPseudoFunction('nth-child')<>CSSCallID_NthChild then
+    raise Exception.Create('20240625142124');
+  if AddPseudoFunction('nth-last-child')<>CSSCallID_NthLastChild then
+    raise Exception.Create('20240625142136');
+  if AddPseudoFunction('nth-of-type')<>CSSCallID_NthOfType then
+    raise Exception.Create('20240625142156');
+  if AddPseudoFunction('nth-last-of-type')<>CSSCallID_NthLastOfType then
+    raise Exception.Create('20240625142212');
+
+  // init types
+  if AddType('*').Index<>CSSTypeID_Universal then
+    raise Exception.Create('20240617190914');
+
+  // init keywords
+  if AddKeyword('none')<>CSSKeywordNone then
+    raise Exception.Create('20240623184021');
+  if AddKeyword('initial')<>CSSKeywordInitial then
+    raise Exception.Create('20240623184030');
+  if AddKeyword('inherit')<>CSSKeywordInherit then
+    raise Exception.Create('20240623184042');
+  if AddKeyword('unset')<>CSSKeywordUnset then
+    raise Exception.Create('20240623184053');
+  if AddKeyword('revert')<>CSSKeywordRevert then
+    raise Exception.Create('20240623184104');
+  if AddKeyword('revert-layer')<>CSSKeywordRevertLayer then
+    raise Exception.Create('20240623184114');
+  if AddKeyword('auto')<>CSSKeywordAuto then
+    raise Exception.Create('20240625182731');
+
+  // init attribute functions
+  if AddAttrFunction('var')<>CSSAttrFuncVar then
+    raise Exception.Create('20240716124054');
+end;
+
+destructor TCSSRegistry.Destroy;
+var
+  i: Integer;
+  Kind: TCSSNumericalIDKind;
+begin
+  for Kind in TCSSNumericalIDKind do
+    FreeAndNil(FHashLists[Kind]);
+
+  // attributes
+  NotAllAttributes:=nil;
+  for i:=1 to AttributeCount-1 do
+    FreeAndNil(Attributes[i]);
+  Attributes:=nil;
+  FAttributeCount:=0;
+
+  // pseudo classes
+  for i:=1 to PseudoClassCount-1 do
+    FreeAndNil(PseudoClasses[i]);
+  PseudoClasses:=nil;
+  FPseudoClassCount:=0;
+
+  // types
+  for i:=1 to TypeCount-1 do
+    FreeAndNil(Types[i]);
+  Types:=nil;
+  FTypeCount:=0;
+
+  // keywords
+  FKeywordCount:=0;
+
+  inherited Destroy;
+end;
+
+function TCSSRegistry.FindNamedItem(Kind: TCSSNumericalIDKind;
+  const aName: TCSSString): TCSSRegistryNamedItem;
+begin
+  if Kind in nikAllDescriptors then
+    Result:=TCSSRegistryNamedItem(FHashLists[Kind].Find(aName))
+  else
+    raise Exception.Create('20240625141820');
+end;
+
+function TCSSRegistry.IndexOfNamedItem(Kind: TCSSNumericalIDKind;
+  const aName: TCSSString): TCSSNumericalID;
+var
+  Item: TCSSRegistryNamedItem;
+  p: Pointer;
+begin
+  if Kind in nikAllDescriptors then
+  begin
+    Item:=TCSSRegistryNamedItem(FHashLists[Kind].Find(aName));
+    if Item<>nil then
+      Result:=Item.Index
+    else
+      Result:=-1;
+  end else begin
+    p:=FHashLists[Kind].Find(aName);
+    if p=nil then
+      exit(CSSIDNone)
+    else
+      Result:={%H-}TCSSNumericalID(p);
+  end;
+end;
+
+procedure TCSSRegistry.ConsistencyCheck;
+var
+  ID, ID2: TCSSNumericalID;
+  AttrDesc, SubAttrDesc: TCSSAttributeDesc;
+  PseudoClassDesc: TCSSPseudoClassDesc;
+  TypeDesc: TCSSTypeDesc;
+  i: Integer;
+  aName: TCSSString;
+begin
+  if AttributeCount>length(Attributes) then
+    raise Exception.Create('20240629102438');
+  for ID:=1 to AttributeCount-1 do
+  begin
+    AttrDesc:=Attributes[ID];
+    if AttrDesc=nil then
+      raise Exception.Create('20240629102530 attr ID='+IntToStr(ID)+' Desc=nil');
+    aName:=AttrDesc.Name;
+    if aName='' then
+      raise Exception.Create('20240629100056 attr ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629100143 attr ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231211 attr ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    if AttrDesc.Index<>ID then
+      raise Exception.Create('20240629095849 attr ID='+IntToStr(ID)+' Desc.Index='+IntToStr(AttrDesc.Index)+' "'+aName+'"');
+    ID2:=IndexOfAttributeName(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629101227 attr ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+
+    if length(AttrDesc.CompProps)>0 then
+    begin
+      // check shorthand
+      for i:=0 to length(AttrDesc.CompProps)-1 do
+      begin
+        SubAttrDesc:=AttrDesc.CompProps[i];
+        if SubAttrDesc=nil then
+          raise Exception.Create('20240629102701 attr ID='+IntToStr(ID)+' Shorthand="'+aName+'" CompDesc=nil '+IntToStr(i));
+        if (SubAttrDesc.Index<=0) then
+          raise Exception.Create('20240629100345 attr ID='+IntToStr(ID)+' Shorthand="'+aName+'" invalid CompAttr '+IntToStr(SubAttrDesc.Index));
+        if (SubAttrDesc.Index>=ID) then
+          raise Exception.Create('20240629100345 attr ID='+IntToStr(ID)+' Shorthand="'+aName+'" CompAttr after Shorthand: SubID='+IntToStr(SubAttrDesc.Index)+' SubName='+SubAttrDesc.Name);
+      end;
+    end;
+  end;
+
+  if PseudoClassCount>length(PseudoClasses) then
+    raise Exception.Create('20240629102438');
+  for ID:=1 to PseudoClassCount-1 do
+  begin
+    PseudoClassDesc:=PseudoClasses[ID];
+    if PseudoClassDesc=nil then
+      raise Exception.Create('20240629102605 pseudo class ID='+IntToStr(ID)+' Desc=nil');
+    aName:=PseudoClassDesc.Name;
+    if aName='' then
+      raise Exception.Create('20240629100652 pseudo class ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629100657 pseudo class ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231235 pseudo class ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    if PseudoClassDesc.Index<>ID then
+      raise Exception.Create('20240629100659 pseudo class ID='+IntToStr(ID)+' Desc.Index='+IntToStr(PseudoClassDesc.Index)+' "'+aName+'"');
+    ID2:=IndexOfPseudoClassName(PseudoClassDesc.Name);
+    if ID2<>ID then
+      raise Exception.Create('20240629101227 pseudo class ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+
+  if PseudoFunctionCount>length(PseudoFunctions) then
+    raise Exception.Create('20240629103430');
+  for ID:=1 to PseudoFunctionCount-1 do
+  begin
+    aName:=PseudoFunctions[ID];
+    if aName='' then
+      raise Exception.Create('20240629103431 pseudo function ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629103433 pseudo function ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231235 pseudo function ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    ID2:=IndexOfPseudoFunction(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629103434 pseudo function ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+
+  if TypeCount>length(Types) then
+    raise Exception.Create('20240629102438');
+  for ID:=1 to TypeCount-1 do
+  begin
+    TypeDesc:=Types[ID];
+    if TypeDesc=nil then
+      raise Exception.Create('20240629102620 type ID='+IntToStr(ID)+' Desc=nil');
+    aName:=TypeDesc.Name;
+    if aName='' then
+      raise Exception.Create('20240629100812 type ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629100825 type ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231645 type ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    if TypeDesc.Index<>ID then
+      raise Exception.Create('20240629101013 type ID='+IntToStr(ID)+' Desc.Index='+IntToStr(TypeDesc.Index)+' "'+aName+'"');
+    ID2:=IndexOfTypeName(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629101529 type ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+
+  if KeywordCount>length(Keywords) then
+    raise Exception.Create('20240629103200');
+  for ID:=1 to KeywordCount-1 do
+  begin
+    aName:=Keywords[ID];
+    if aName='' then
+      raise Exception.Create('20240629103223 keyword ID='+IntToStr(ID)+' missing name');
+    if length(aName)>255 then
+      raise Exception.Create('20240629103242 keyword ID='+IntToStr(ID)+' name too long "'+aName+'"');
+    if aName[1]=':' then
+      raise Exception.Create('20240701231656 keyword ID='+IntToStr(ID)+' invalid name "'+aName+'"');
+    ID2:=IndexOfKeyword(aName);
+    if ID2<>ID then
+      raise Exception.Create('20240629103303 keyword ID='+IntToStr(ID)+' "'+aName+'" IndexOf failed: '+IntToStr(ID2));
+  end;
+end;
+
+procedure TCSSRegistry.ChangeStamp;
+begin
+  if FStamp<high(FStamp) then
+    inc(FStamp)
+  else
+    FStamp:=1;
+end;
+
+function TCSSRegistry.AddAttribute(Attr: TCSSAttributeDesc
+  ): TCSSAttributeDesc;
+begin
+  Result:=Attr;
+  if Attr.Name='' then
+    raise ECSSParser.Create('missing name');
+  if FindAttribute(Attr.Name)<>nil then
+    raise ECSSParser.Create('duplicate attribute "'+Attr.Name+'"');
+
+  if AttributeCount=length(Attributes) then
+  begin
+    if AttributeCount<32 then
+      SetLength(Attributes,32)
+    else
+      SetLength(Attributes,2*AttributeCount);
+    FillByte(Attributes[AttributeCount],SizeOf(Pointer)*(length(Attributes)-AttributeCount),0);
+  end;
+  Attributes[AttributeCount]:=Attr;
+  Attr.Index:=AttributeCount;
+  FHashLists[nikAttribute].Add(Attr.Name,Attr);
+  inc(FAttributeCount);
+  ChangeStamp;
+end;
+
+function TCSSRegistry.AddAttribute(const aName: TCSSString;
+  const aInitialValue: TCSSString; aInherits: boolean; aAll: boolean;
+  aClass: TCSSAttributeDescClass): TCSSAttributeDesc;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if FindAttribute(aName)<>nil then
+    raise ECSSParser.Create('duplicate attribute "'+aName+'"');
+  if aClass=nil then
+    aClass:=Attribute_ClassOf;
+
+  Result:=aClass.Create;
+  Result.Name:=aName;
+  Result.InitialValue:=aInitialValue;
+  Result.Inherits:=aInherits;
+  Result.All:=aAll;
+  AddAttribute(Result);
+
+  if not aAll then
+    Insert(Result,NotAllAttributes,length(NotAllAttributes));
+end;
+
+function TCSSRegistry.FindAttribute(const aName: TCSSString
+  ): TCSSAttributeDesc;
+begin
+  Result:=TCSSAttributeDesc(FHashLists[nikAttribute].Find(aName));
+end;
+
+function TCSSRegistry.IndexOfAttributeName(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  Attr: TCSSAttributeDesc;
+begin
+  Attr:=TCSSAttributeDesc(FHashLists[nikAttribute].Find(aName));
+  if Attr<>nil then
+    Result:=Attr.Index
+  else
+    Result:=-1;
+end;
+
+procedure TCSSRegistry.AddSplitLonghand(var AttrIDs: TCSSNumericalIDArray;
+  var Values: TCSSStringArray; AttrID: TCSSNumericalID; const Value: TCSSString);
+begin
+  System.Insert(AttrID,AttrIDs,length(AttrIDs));
+  System.Insert(Value,Values,length(Values));
+end;
+
+procedure TCSSRegistry.AddSplitLonghandSides(var AttrIDs: TCSSNumericalIDArray;
+  var Values: TCSSStringArray; TopID, RightID, BottomID, LeftID: TCSSNumericalID;
+  const Found: TCSSStringArray);
+begin
+  if length(Found)=0 then
+    exit; // invalid
+
+  Setlength(AttrIDs,4);
+  Setlength(Values,4);
+
+  AttrIDs[0]:=TopID;
+  AttrIDs[1]:=RightID;
+  AttrIDs[2]:=BottomID;
+  AttrIDs[3]:=LeftID;
+
+  // sets sides depending on how many values were passed:
+  // 1: all four the same
+  // 2: top and bottom | left and right
+  // 3: top | left and right | bottom
+  // 4: top | right | bottom | left
+  case length(Found) of
+  1:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[0];
+      Values[2]:=Found[0];
+      Values[3]:=Found[0];
+    end;
+  2:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[1];
+      Values[2]:=Found[0];
+      Values[3]:=Found[1];
+    end;
+  3:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[1];
+      Values[2]:=Found[2];
+      Values[3]:=Found[1];
+    end;
+  4:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[1];
+      Values[2]:=Found[2];
+      Values[3]:=Found[3];
+    end;
+  end;
+end;
+
+procedure TCSSRegistry.AddSplitLonghandCorners(var AttrIDs: TCSSNumericalIDArray;
+  var Values: TCSSStringArray; TopLeftID, TopRightID, BottomLeftID, BottomRightID: TCSSNumericalID;
+  const Found: TCSSStringArray);
+begin
+  if length(Found)=0 then
+    exit; // invalid
+
+  Setlength(AttrIDs,4);
+  Setlength(Values,4);
+
+  AttrIDs[0]:=TopLeftID;
+  AttrIDs[1]:=TopRightID;
+  AttrIDs[2]:=BottomRightID;
+  AttrIDs[3]:=BottomLeftID;
+
+  // sets corners depending on how many values were passed:
+  // 1: all four the same
+  // 2: top-left-and-bottom-right | top-right-and-bottom-left
+  // 3: top-left | top-right-and-bottom-left | bottom-right
+  // 4: top-left | top-right | bottom-right | bottom-left
+
+  case length(Found) of
+  1:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[0];
+      Values[2]:=Found[0];
+      Values[3]:=Found[0];
+    end;
+  2:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[1];
+      Values[2]:=Found[0];
+      Values[3]:=Found[1];
+    end;
+  3:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[1];
+      Values[2]:=Found[2];
+      Values[3]:=Found[1];
+    end;
+  4:
+    begin
+      Values[0]:=Found[0];
+      Values[1]:=Found[1];
+      Values[2]:=Found[2];
+      Values[3]:=Found[3];
+    end;
+  end;
+end;
+
+function TCSSRegistry.AddPseudoClass(aPseudo: TCSSPseudoClassDesc
+  ): TCSSPseudoClassDesc;
+begin
+  Result:=aPseudo;
+  if aPseudo.Name='' then
+    raise ECSSParser.Create('missing name');
+  if FindPseudoClass(aPseudo.Name)<>nil then
+    raise ECSSParser.Create('duplicate pseudo class "'+aPseudo.Name+'"');
+
+  if PseudoClassCount=length(PseudoClasses) then
+  begin
+    if PseudoClassCount<32 then
+      SetLength(PseudoClasses,32)
+    else
+      SetLength(PseudoClasses,2*PseudoClassCount);
+    FillByte(PseudoClasses[PseudoClassCount],SizeOf(Pointer)*(length(PseudoClasses)-PseudoClassCount),0);
+  end;
+  PseudoClasses[PseudoClassCount]:=aPseudo;
+  aPseudo.Index:=PseudoClassCount;
+  FHashLists[nikPseudoClass].Add(aPseudo.Name,aPseudo);
+  inc(FPseudoClassCount);
+  ChangeStamp;
+end;
+
+function TCSSRegistry.AddPseudoClass(const aName: TCSSString;
+  aClass: TCSSPseudoClassDescClass): TCSSPseudoClassDesc;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if FindPseudoClass(aName)<>nil then
+    raise ECSSParser.Create('duplicate pseudo class "'+aName+'"');
+  if aClass=nil then
+    aClass:=PseudoClass_ClassOf;
+
+  Result:=aClass.Create;
+  Result.Name:=aName;
+  AddPseudoClass(Result);
+end;
+
+function TCSSRegistry.FindPseudoClass(const aName: TCSSString
+  ): TCSSPseudoClassDesc;
+begin
+  Result:=TCSSPseudoClassDesc(FHashLists[nikPseudoClass].Find(aName));
+end;
+
+function TCSSRegistry.IndexOfPseudoClassName(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  aPseudo: TCSSPseudoClassDesc;
+begin
+  aPseudo:=TCSSPseudoClassDesc(FHashLists[nikPseudoClass].Find(aName));
+  if aPseudo<>nil then
+    Result:=aPseudo.Index
+  else
+    Result:=-1;
+end;
+
+function TCSSRegistry.AddPseudoFunction(const aName: TCSSString
+  ): TCSSNumericalID;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if length(aName)>255 then
+    raise ECSSParser.Create('pseudo function name too long');
+  if aName<>LowerCase(aName) then
+    raise ECSSParser.Create('pseudo function name not lowercase');
+  Result:=IndexOfKeyword(aName);
+  if Result>0 then
+    raise ECSSParser.Create('duplicate pseudo function "'+aName+'"');
+
+  if PseudoFunctionCount=length(PseudoFunctions) then
+  begin
+    if PseudoFunctionCount<32 then
+      SetLength(PseudoFunctions,32)
+    else
+      SetLength(PseudoFunctions,2*PseudoFunctionCount);
+  end;
+  Result:=PseudoFunctionCount;
+  PseudoFunctions[Result]:=aName;
+  FHashLists[nikPseudoFunction].Add(aName,{%H-}Pointer(Result));
+  inc(FPseudoFunctionCount);
+  ChangeStamp;
+end;
+
+function TCSSRegistry.IndexOfPseudoFunction(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  p: Pointer;
+begin
+  p:=FHashLists[nikPseudoFunction].Find(aName);
+  if p=nil then
+    exit(CSSIDNone)
+  else
+    Result:={%H-}TCSSNumericalID(p);
+end;
+
+function TCSSRegistry.AddType(aType: TCSSTypeDesc): TCSSTypeDesc;
+begin
+  Result:=aType;
+  if aType.Name='' then
+    raise ECSSParser.Create('missing name');
+  if FindType(aType.Name)<>nil then
+    raise ECSSParser.Create('duplicate type "'+aType.Name+'"');
+
+  if TypeCount=length(Types) then
+  begin
+    if TypeCount<32 then
+      SetLength(Types,32)
+    else
+      SetLength(Types,2*TypeCount);
+    FillByte(Types[TypeCount],SizeOf(Pointer)*(length(Types)-TypeCount),0);
+  end;
+  Types[TypeCount]:=aType;
+  aType.Index:=TypeCount;
+  FHashLists[nikType].Add(aType.Name,aType);
+  inc(FTypeCount);
+  ChangeStamp;
+end;
+
+function TCSSRegistry.AddType(const aName: TCSSString; aClass: TCSSTypeDescClass
+  ): TCSSTypeDesc;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if FindType(aName)<>nil then
+    raise ECSSParser.Create('duplicate type "'+aName+'"');
+  if aClass=nil then
+    aClass:=Type_ClassOf;
+
+  Result:=aClass.Create;
+  Result.Name:=aName;
+  AddType(Result);
+end;
+
+function TCSSRegistry.FindType(const aName: TCSSString): TCSSTypeDesc;
+begin
+  Result:=TCSSTypeDesc(FHashLists[nikType].Find(aName));
+end;
+
+function TCSSRegistry.IndexOfTypeName(const aName: TCSSString): TCSSNumericalID;
+var
+  aType: TCSSTypeDesc;
+begin
+  aType:=TCSSTypeDesc(FHashLists[nikType].Find(aName));
+  if aType<>nil then
+    Result:=aType.Index
+  else
+    Result:=-1;
+end;
+
+function TCSSRegistry.AddKeyword(const aName: TCSSString): TCSSNumericalID;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if length(aName)>255 then
+    raise ECSSParser.Create('keyword too long');
+  Result:=IndexOfKeyword(aName);
+  if Result>0 then
+    raise ECSSParser.Create('duplicate keyword "'+aName+'"');
+
+  if KeywordCount=length(Keywords) then
+  begin
+    if KeywordCount<32 then
+      SetLength(Keywords,32)
+    else
+      SetLength(Keywords,2*KeywordCount);
+  end;
+  Result:=KeywordCount;
+  Keywords[Result]:=aName;
+  FHashLists[nikKeyword].Add(aName,{%H-}Pointer(Result));
+  inc(FKeywordCount);
+  ChangeStamp;
+end;
+
+procedure TCSSRegistry.AddKeywords(const Names: TCSSStringArray; out First, Last: TCSSNumericalID);
+var
+  i, NewCnt: integer;
+begin
+  if Names=nil then begin
+    First:=CSSIDNone;
+    Last:=CSSIDNone;
+    exit;
+  end;
+  for i:=0 to length(Names)-1 do
+    if IndexOfKeyword(Names[i])>CSSIDNone then
+      raise Exception.Create('20240712215853');
+
+  NewCnt:=KeywordCount+length(Names);
+  if NewCnt>length(Keywords) then
+  begin
+    NewCnt:=(NewCnt div 32 +1) *32;
+    SetLength(Keywords,NewCnt);
+  end;
+
+  First:=KeywordCount;
+  for i:=0 to length(Names)-1 do
+  begin
+    Last:=KeywordCount;
+    Keywords[Last]:=Names[i];
+    FHashLists[nikKeyword].Add(Names[i],{%H-}Pointer(Last));
+    inc(FKeywordCount);
+  end;
+  ChangeStamp;
+end;
+
+function TCSSRegistry.IndexOfKeyword(const aName: TCSSString): TCSSNumericalID;
+var
+  p: Pointer;
+begin
+  p:=FHashLists[nikKeyword].Find(aName);
+  if p=nil then
+    exit(CSSIDNone)
+  else
+    Result:={%H-}TCSSNumericalID(p);
+end;
+
+procedure TCSSRegistry.AddColorKeywords;
+var
+  Names: TCSSStringArray;
+  i: Integer;
+begin
+  SetLength(Names{%H-},length(CSSNamedColors));
+  for i:=0 to High(CSSNamedColors) do
+    Names[i]:=CSSNamedColors[i].Name;
+  AddKeywords(Names,kwFirstColor,kwLastColor);
+  kwTransparent:=IndexOfKeyword('transparent');
+end;
+
+function TCSSRegistry.GetNamedColor(const aName: TCSSString): TCSSAlphaColor;
+begin
+  Result:=GetKeywordColor(IndexOfKeyword(aName));
+end;
+
+function TCSSRegistry.GetKeywordColor(KeywordID: TCSSNumericalID): TCSSAlphaColor;
+begin
+  if (KeywordID<kwFirstColor) or (KeywordID>kwLastColor) then
+    Result:=$ff000000
+  else
+    Result:=CSSNamedColors[KeywordID-kwFirstColor].Color;
+end;
+
+function TCSSRegistry.AddAttrFunction(const aName: TCSSString): TCSSNumericalID;
+begin
+  if aName='' then
+    raise ECSSParser.Create('missing name');
+  if length(aName)>255 then
+    raise ECSSParser.Create('function name too long');
+  Result:=IndexOfAttrFunction(aName);
+  if Result>0 then
+    raise ECSSParser.Create('duplicate attribute function "'+aName+'"');
+
+  if AttrFunctionCount=length(AttrFunctions) then
+  begin
+    if AttrFunctionCount<32 then
+      SetLength(AttrFunctions,32)
+    else
+      SetLength(AttrFunctions,2*AttrFunctionCount);
+  end;
+  Result:=AttrFunctionCount;
+  AttrFunctions[Result]:=aName;
+  FHashLists[nikAttrFunction].Add(aName,{%H-}Pointer(Result));
+  inc(FAttrFunctionCount);
+  ChangeStamp;
+end;
+
+function TCSSRegistry.IndexOfAttrFunction(const aName: TCSSString
+  ): TCSSNumericalID;
+var
+  p: Pointer;
+begin
+  p:=FHashLists[nikAttrFunction].Find(aName);
+  if p=nil then
+    exit(CSSIDNone)
+  else
+    Result:={%H-}TCSSNumericalID(p);
+end;
+
+{ TCSSResolvedCallElement }
+
+destructor TCSSResolvedCallElement.Destroy;
+begin
+  FreeAndNil(Params);
+  inherited Destroy;
+end;
+
+{ TCSSResCompValue }
+
+function TCSSResCompValue.AsString: TCSSString;
+var
+  l: integer;
+begin
+  if (StartP=nil) or (EndP=nil) or (EndP<=StartP) then
+    exit('');
+  l:=EndP-StartP;
+  SetLength(Result,l);
+  Move(StartP^,Result[1],l);
+end;
+
+function TCSSResCompValue.FloatAsString: TCSSString;
+begin
+  Result:=FloatToCSSStr(Float)+CSSUnitNames[FloatUnit];
+end;
+
+{ TCSSCheckAttrParams_Dimension }
+
+function TCSSCheckAttrParams_Dimension.Fits(const ResValue: TCSSResCompValue): boolean;
+var
+  i: Integer;
+begin
+  Result:=false;
+  case ResValue.Kind of
+  rvkFloat:
+    if ResValue.FloatUnit in AllowedUnits then
+    begin
+      if not (ResValue.FloatUnit in AllowedUnits) then exit;
+      if (not AllowNegative) and (ResValue.Float<0) then exit;
+      if (not AllowFrac) and (Frac(ResValue.Float)>0) then exit;
+      exit(true);
+    end else if (ResValue.FloatUnit=cuNone) and (ResValue.Float=0) then
+      exit(true);
+  rvkKeyword:
+    for i:=0 to length(AllowedKeywordIDs)-1 do
+      if ResValue.KeywordID=AllowedKeywordIDs[i] then
+        exit(true);
+  end;
+end;
+
+{ TCSSBaseResolver }
+
+procedure TCSSBaseResolver.SetCSSRegistry(const AValue: TCSSRegistry);
+begin
+  if FCSSRegistry=AValue then Exit;
+  FCSSRegistry:=AValue;
+end;
+
+function TCSSBaseResolver.InitParseAttr(Desc: TCSSAttributeDesc; AttrData: TCSSAttributeKeyData;
+  const Value: TCSSString): boolean;
+var
+  p: PCSSChar;
+begin
+  Result:=false;
+  CurAttrData:=AttrData;
+  CurDesc:=Desc;
+  CurValue:=Value;
+  CurComp:=Default(TCSSResCompValue);
+  CurComp.EndP:=PCSSChar(CurValue);
+  if not ReadNext then
+  begin
+    if CurAttrData<>nil then
+      CurAttrData.Invalid:=true;
+    exit;
+  end;
+  if (CurAttrData<>nil) and (CurComp.Kind=rvkKeyword)
+      and IsBaseKeyword(CurComp.KeywordID) then
+  begin
+    p:=CurComp.EndP;
+    while (p^ in Whitespace) do inc(p);
+    if p^>#0 then
+    begin
+      // "inherit" must be alone
+      CurAttrData.Invalid:=true;
+      exit;
+    end;
+    CurAttrData.Complete:=true;
+  end;
+  Result:=true;
+end;
+
+procedure TCSSBaseResolver.InitParseAttr(const Value: TCSSString);
+begin
+  CurValue:=Value;
+  CurComp:=Default(TCSSResCompValue);
+  CurComp.EndP:=PCSSChar(CurValue);
+  ReadNext;
+end;
+
+function TCSSBaseResolver.CheckAttribute_Keyword(const AllowedKeywordIDs: TCSSNumericalIDArray
+  ): boolean;
+begin
+  Result:=ReadAttribute_Keyword(CurAttrData.Invalid,AllowedKeywordIDs);
+end;
+
+function TCSSBaseResolver.CheckAttribute_CommaList_Keyword(
+  const AllowedKeywordIDs: TCSSNumericalIDArray): boolean;
+var
+  i: Integer;
+  Fits: Boolean;
+begin
+  CurAttrData.Invalid:=true;
+  repeat
+    Fits:=false;
+    case CurComp.Kind of
+    rvkKeyword:
+      for i:=0 to length(AllowedKeywordIDs)-1 do
+        if CurComp.KeywordID=AllowedKeywordIDs[i] then
+        begin
+          Fits:=true;
+          break;
+        end;
+    rvkFunction:
+      begin
+        // todo: check for allowed functions
+        Fits:=true;
+      end;
+    end;
+    if not Fits then exit;
+
+    if not ReadNext then
+    begin
+      // ok
+      CurAttrData.Invalid:=false;
+      exit(true);
+    end;
+    if (CurComp.Kind<>rvkSymbol) or (CurComp.Symbol=ctkCOMMA) then
+      exit;
+  until not ReadNext;
+  Result:=false;
+end;
+
+function TCSSBaseResolver.CheckAttribute_Dimension(const Params: TCSSCheckAttrParams_Dimension
+  ): boolean;
+begin
+  Result:=ReadAttribute_Dimension(CurAttrData.Invalid,Params);
+end;
+
+function TCSSBaseResolver.CheckAttribute_Color(const AllowedKeywordIDs: TCSSNumericalIDArray
+  ): boolean;
+begin
+  Result:=ReadAttribute_Color(CurAttrData.Invalid,AllowedKeywordIDs);
+end;
+
+function TCSSBaseResolver.ReadNext: boolean;
+begin
+  Result:=ReadComp(CurComp);
+end;
+
+function TCSSBaseResolver.ReadAttribute_Keyword(out Invalid: boolean;
+  const AllowedKeywordIDs: TCSSNumericalIDArray): boolean;
+var
+  i: Integer;
+begin
+  Invalid:=false;
+  repeat
+    case CurComp.Kind of
+    rvkKeyword:
+      for i:=0 to length(AllowedKeywordIDs)-1 do
+        if CurComp.KeywordID=AllowedKeywordIDs[i] then
+          exit(true);
+    end;
+    // todo: warn if invalid
+  until not ReadNext;
+  Invalid:=true;
+  Result:=false;
+end;
+
+function TCSSBaseResolver.ReadAttribute_Dimension(out Invalid: boolean;
+  const Params: TCSSCheckAttrParams_Dimension): boolean;
+var
+  i: Integer;
+begin
+  Invalid:=true;
+  repeat
+    case CurComp.Kind of
+    rvkFloat:
+      if Params.Fits(CurComp) then
+        exit(true);
+    rvkKeyword:
+      for i:=0 to length(Params.AllowedKeywordIDs)-1 do
+        if CurComp.KeywordID=Params.AllowedKeywordIDs[i] then
+          exit(true);
+    end;
+    // todo: warn if invalid
+  until not ReadNext;
+  Invalid:=true;
+  Result:=false;
+end;
+
+function TCSSBaseResolver.ReadAttribute_Color(out Invalid: boolean;
+  const AllowedKeywordIDs: TCSSNumericalIDArray): boolean;
+var
+  i: Integer;
+begin
+  Invalid:=false;
+  repeat
+    case CurComp.Kind of
+    rvkKeyword:
+      begin
+        if (CurComp.KeywordID>=CSSRegistry.kwFirstColor)
+            and (CurComp.KeywordID<=CSSRegistry.kwLastColor)
+        then
+          exit(true);
+        for i:=0 to length(AllowedKeywordIDs)-1 do
+          if CurComp.KeywordID=AllowedKeywordIDs[i] then
+            exit(true);
+      end;
+    rvkFunction:
+      begin
+        // todo: check for allowed functions
+      end;
+    rvkHexColor:
+      exit(true);
+    end;
+    // todo: warn if invalid
+  until not ReadNext;
+  Invalid:=true;
+  Result:=false;
+end;
+
+function TCSSBaseResolver.ReadComp(var aComp: TCSSResCompValue): boolean;
+begin
+  Result:=ReadValue(aComp);
+  ReadWordID(aComp);
+end;
+
+procedure TCSSBaseResolver.ReadWordID(var aComp: TCSSResCompValue);
+var
+  Identifier: TCSSString;
+begin
+  case aComp.Kind of
+  rvkFunctionUnknown:
+    begin
+      SetString(Identifier,aComp.StartP,aComp.EndP-aComp.StartP);
+      aComp.FunctionID:=CSSRegistry.IndexOfAttrFunction(Identifier);
+      if aComp.FunctionID>CSSIDNone then
+        aComp.Kind:=rvkFunction;
+    end;
+  rvkKeywordUnknown:
+    begin
+      SetString(Identifier,aComp.StartP,aComp.EndP-aComp.StartP);
+      aComp.KeywordID:=CSSRegistry.IndexOfKeyword(Identifier);
+      if aComp.KeywordID>CSSIDNone then
+        aComp.Kind:=rvkKeyword;
+    end;
+  end;
+end;
+
+class function TCSSBaseResolver.ReadValue(var aComp: TCSSResCompValue): boolean;
+var
+  c: TCSSChar;
+  p: PCSSChar;
+  l: SizeInt;
+
+  procedure SetSymbol(s: TCSSToken);
+  begin
+    aComp.Kind:=rvkSymbol;
+    aComp.Symbol:=s;
+    aComp.EndP:=p+1;
+  end;
+
+begin
+  Result:=true;
+  aComp.Kind:=rvkNone;
+
+  p:=aComp.EndP;
+
+  // skip whitespace
+  while (p^ in Whitespace) do inc(p);
+  aComp.StartP:=p;
+  aComp.EndP:=p;
+
+  c:=p^;
+  case c of
+  #0: exit(false);
+  '0'..'9':
+    if ReadNumber(aComp) then exit;
+  ',':
+    begin
+      SetSymbol(ctkCOMMA);
+      exit;
+    end;
+  ')':
+    begin
+      SetSymbol(ctkRPARENTHESIS);
+      exit;
+    end;
+  '+':
+    case p[1] of
+    '0'..'9','.':
+      if ReadNumber(aComp) then exit;
+    #0,#9,#10,#13,' ':
+      begin
+        SetSymbol(ctkPLUS);
+        exit;
+      end;
+    end;
+  '-':
+    case p[1] of
+    '0'..'9','.':
+      if ReadNumber(aComp) then exit;
+    'a'..'z','A'..'Z','-':
+      if ReadIdentifier(aComp) then exit;
+    #0,#9,#10,#13,' ':
+      begin
+        SetSymbol(ctkMINUS);
+        exit;
+      end;
+    end;
+  '.':
+    case p[1] of
+    '0'..'9':
+      if ReadNumber(aComp) then exit;
+    else
+      SetSymbol(ctkDOT);
+      exit;
+    end;
+  '*':
+    begin
+      if p[1]='=' then
+      begin
+        inc(p);
+        SetSymbol(ctkSTAREQUAL);
+      end else
+        SetSymbol(ctkSTAR);
+      exit;
+    end;
+  '/':
+    begin
+      SetSymbol(ctkDIV);
+      exit;
+    end;
+  ':':
+    begin
+      SetSymbol(ctkCOLON);
+      exit;
+    end;
+  ';':
+    begin
+      SetSymbol(ctkSEMICOLON);
+      exit;
+    end;
+  'a'..'z','A'..'Z':
+    if ReadIdentifier(aComp) then exit;
+  '#':
+    begin
+      inc(p);
+      while p^ in Hex do inc(p);
+      l:=p-aComp.StartP;
+      case l of
+      4,5,7,9:
+        begin
+          // #rgb, #rgba, #rrggbb, #rrggbbaa
+          aComp.Kind:=rvkHexColor;
+          aComp.EndP:=p;
+          exit;
+        end;
+      end;
+    end;
+  end;
+
+  // skip unknown aComp
+  aComp.Kind:=rvkInvalid;
+  repeat
+    if p^ in ValEnd then break;
+    case p^ of
+    '(','[': SkipBrackets(p);
+    '''','"': SkipString(p);
+    else inc(p);
+    end;
+  until false;
+  aComp.EndP:=p;
+end;
+
+class function TCSSBaseResolver.ReadNumber(var aComp: TCSSResCompValue): boolean;
+var
+  Negative, HasNumber: Boolean;
+  Divisor: double;
+  Exponent: Integer;
+  d: Float;
+  U: TCSSUnit;
+  StartP, p: PCSSChar;
+begin
+  Result:=false;
+  aComp.Kind:=rvkInvalid;
+  p:=aComp.StartP;
+
+  // number: 1, 0.2, .3, 4.01, 0.0, +0.0, -0.0, .50, 2e3, -6.7E-2
+  if p^='-' then
+  begin
+    Negative:=true;
+    inc(p);
+  end else begin
+    Negative:=false;
+    if p^='+' then
+      inc(p);
+  end;
+  HasNumber:=false;
+  aComp.Float:=0;
+  if p^ in Num then
+  begin
+    // read significand
+    HasNumber:=true;
+    repeat
+      aComp.Float:=aComp.Float*10+ord(p^)-ord('0');
+      if aComp.Float>CSSMaxSafeIntDouble then
+        exit; // loosing precision
+      inc(p);
+    until not (p^ in Num);
+  end;
+  if p^='.' then
+  begin
+    // read fraction
+    inc(p);
+    if not (p^ in Num) then exit;
+    Divisor:=1;
+    repeat
+      Divisor:=Divisor*10;
+      aComp.Float:=aComp.Float*10+ord(p^)-ord('0');
+      if (Divisor>CSSMaxSafeIntDouble)
+          or (aComp.Float>CSSMaxSafeIntDouble) then
+        exit; // loosing precision
+      inc(p);
+    until not (p^ in Num);
+    aComp.Float:=aComp.Float/Divisor;
+  end else if not HasNumber then
+    exit;
+  if Negative then
+    aComp.Float:=-aComp.Float;
+
+  if (p^ in ['e','E']) and not (p[1] in ['a'..'z']) then
+  begin
+    inc(p);
+    if p^='-' then
+    begin
+      Negative:=true;
+      inc(p);
+    end else begin
+      Negative:=false;
+      if p^='+' then
+        inc(p);
+    end;
+    if not (p^ in Num) then exit;
+    Exponent:=0;
+    repeat
+      inc(p);
+      Exponent:=Exponent*10+ord(p^)-ord('0');
+      if Exponent>2047 then
+        exit; // out of bounds
+    until not (p^ in Num);
+    if Exponent>0 then
+    begin
+      if Negative then
+        Exponent:=-Exponent;
+      try
+        d:=Power(10,Exponent);
+        aComp.Float:=aComp.Float*d;
+      except
+        exit;
+      end;
+    end;
+  end;
+  aComp.Kind:=rvkFloat;
+
+  // parse unit
+  U:=cuNone;
+  case p^ of
+  '%':
+    begin
+      inc(p);
+      U:=cuPercent;
+    end;
+  'a'..'z','A'..'Z':
+    begin
+      StartP:=p;
+      while p^ in Alpha do inc(p);
+      U:=high(TCSSUnit);
+      while (U>cuNone) and not CompareMem(StartP,PChar(CSSUnitNames[U]),length(CSSUnitNames[U])) do
+        U:=pred(U);
+      if U=cuNone then
+        exit; // unknown unit
+    end;
+  end;
+  aComp.FloatUnit:=U;
+  aComp.EndP:=p;
+
+  Result:=true;
+  //writeln('TCSSBaseResolver.ReadNumber "',p,'" Value=',FloatToCSSStr(aComp.Float),' U=',U,' Kind=',aComp.Kind,' Result=',Result);
+end;
+
+class function TCSSBaseResolver.ReadIdentifier(var aComp: TCSSResCompValue): boolean;
+var
+  IsFunc: Boolean;
+  p: PCSSChar;
+begin
+  Result:=false;
+  aComp.Kind:=rvkInvalid;
+  p:=aComp.EndP;
+  if not (p^ in AlIden) then exit;
+  repeat
+    inc(p);
+  until not (p^ in AlNumIden);
+  IsFunc:=p^='(';
+  if IsFunc then
+  begin
+    // function call
+    aComp.Kind:=rvkFunctionUnknown;
+    aComp.BracketOpen:=p;
+    if not SkipBrackets(p) then
+    begin
+      aComp.EndP:=p;
+      exit;
+    end;
+  end else
+    aComp.Kind:=rvkKeywordUnknown;
+  aComp.EndP:=p;
+
+  Result:=true;
+end;
+
+function TCSSBaseResolver.IsBaseKeyword(KeywordID: TCSSNumericalID): boolean;
+begin
+  Result:=(KeywordID>=CSSKeywordInitial) and (KeywordID<=CSSKeywordRevertLayer);
+end;
+
+function TCSSBaseResolver.IsKeywordIn(aKeywordID: TCSSNumericalID;
+  const KeywordIDs: TCSSNumericalIDArray): boolean;
+var
+  i: Integer;
+begin
+  for i:=0 to length(KeywordIDs)-1 do
+    if KeywordIDs[i]=aKeywordID then
+      exit(true);
+  Result:=false;
+end;
+
+function TCSSBaseResolver.IsKeywordIn(const KeywordIDs: TCSSNumericalIDArray): boolean;
+var
+  aKeywordID: TCSSNumericalID;
+  i: Integer;
+begin
+  Result:=false;
+  if CurComp.Kind<>rvkKeyword then exit;
+  aKeywordID:=CurComp.KeywordID;
+  for i:=0 to length(KeywordIDs)-1 do
+    if KeywordIDs[i]=aKeywordID then
+      exit(true);
+end;
+
+function TCSSBaseResolver.IsLengthOrPercentage(AllowNegative: boolean): boolean;
+begin
+  Result:=false;
+  case CurComp.Kind of
+  rvkFloat:
+    if CurComp.FloatUnit in cuAllLengthsAndPercent then
+    begin
+      if (not AllowNegative) and (CurComp.Float<0) then exit;
+      exit(true);
+    end
+    else if (CurComp.FloatUnit=cuNone) and (CurComp.Float=0) then
+      exit(true); // 0 without unit is allowed
+  end;
+end;
+
+function TCSSBaseResolver.IsLengthOrPercentage(const ResValue: TCSSResCompValue;
+  AllowNegative: boolean): boolean;
+begin
+  Result:=false;
+  case ResValue.Kind of
+  rvkFloat:
+    if ResValue.FloatUnit in cuAllLengthsAndPercent then
+    begin
+      if (not AllowNegative) and (ResValue.Float<0) then exit;
+      exit(true);
+    end
+    else if (ResValue.FloatUnit=cuNone) and (ResValue.Float=0) then
+      exit(true);
+  end;
+end;
+
+function TCSSBaseResolver.IsSymbol(Token: TCSSToken): boolean;
+begin
+  Result:=(CurComp.Kind=rvkSymbol) and (CurComp.Symbol=Token);
+end;
+
+function TCSSBaseResolver.GetCompString: TCSSString;
+var
+  StartP: PCSSChar;
+begin
+  if CurComp.Kind=rvkKeyword then
+    exit(CSSRegistry.Keywords[CurComp.KeywordID]);
+  StartP:=CurComp.StartP;
+  if (StartP=PCSSChar(CurValue)) and (CurComp.EndP-StartP = length(CurValue)) then
+    Result:=CurValue
+  else
+    SetString(Result,StartP,CurComp.EndP-StartP);
+end;
+
+function TCSSBaseResolver.GetCompString(const aValue: string; const ResValue: TCSSResCompValue
+  ): TCSSString;
+var
+  Start: PCSSChar;
+begin
+  if ResValue.Kind=rvkKeyword then
+    exit(CSSRegistry.Keywords[ResValue.KeywordID]);
+  Start:=ResValue.StartP;
+  if (Start=PCSSChar(aValue)) and (ResValue.EndP-Start = length(aValue)) then
+    Result:=aValue
+  else
+    SetString(Result,Start,ResValue.EndP-Start);
+end;
+
+class procedure TCSSBaseResolver.SkipToEndOfAttribute(var p: PCSSChar);
+begin
+  repeat
+    case p^ of
+    #0,'{','}',';': exit;
+    '''','"': SkipString(p);
+    else inc(p);
+    end;
+  until false;
+end;
+
+class function TCSSBaseResolver.SkipString(var p: PCSSChar): boolean;
+var
+  Delim, c: TCSSChar;
+begin
+  Result:=false;
+  Delim:=p^;
+  repeat
+    inc(p);
+    c:=p^;
+    if c=Delim then
+    begin
+      inc(p);
+      exit(true);
+    end else if c=#0 then
+      exit
+    else
+      inc(p);
+  until false;
+end;
+
+class function TCSSBaseResolver.SkipBrackets(var p: PCSSChar; Lvl: integer): boolean;
+const
+  CSSMaxBracketLvl = 10;
+var
+  CloseBracket: TCSSChar;
+begin
+  Result:=false;
+  if Lvl>CSSMaxBracketLvl then
+  begin
+    SkipToEndOfAttribute(p);
+    exit;
+  end;
+
+  if p^='[' then
+    CloseBracket:=']'
+  else
+    CloseBracket:=')';
+  repeat
+    inc(p);
+    case p^ of
+    #0,'{','}',';': exit;
+    '''','"': SkipString(p);
+    '(','[': SkipBrackets(p,Lvl+1);
+    ')',']':
+      if p^=CloseBracket then
+      begin
+        inc(p);
+        exit(true);
+      end else begin
+        SkipToEndOfAttribute(p);
+        exit;
+      end;
+    end;
+  until false;
+end;
+
+function TCSSBaseResolver.GetAttributeID(const aName: TCSSString; AutoCreate: boolean
+  ): TCSSNumericalID;
+begin
+  Result:=CSSRegistry.IndexOfAttributeName(aName);
+  if AutoCreate then ;
+end;
+
+function TCSSBaseResolver.GetAttributeDesc(AttrID: TCSSNumericalID): TCSSAttributeDesc;
+begin
+  if (AttrID>0) and (AttrID<CSSRegistry.AttributeCount) then
+    Result:=CSSRegistry.Attributes[AttrID]
+  else
+    Result:=nil;
+end;
+
+function TCSSBaseResolver.GetTypeID(const aName: TCSSString): TCSSNumericalID;
+begin
+  Result:=CSSRegistry.IndexOfTypeName(aName);
+end;
+
+function TCSSBaseResolver.GetPseudoClassID(const aName: TCSSString): TCSSNumericalID;
+begin
+  Result:=CSSRegistry.IndexOfPseudoClassName(aName);
+end;
+
+function TCSSBaseResolver.GetPseudoFunctionID(const aName: TCSSString): TCSSNumericalID;
+begin
+  Result:=CSSRegistry.IndexOfPseudoFunction(aName);
+end;
+
+{ TCSSResolverParser }
+
+function TCSSResolverParser.ResolveAttribute(El: TCSSResolvedIdentifierElement): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  if El.NumericalID<>CSSIDNone then
+    raise Exception.Create('20240701143234');
+  aName:=El.Name;
+  El.Kind:=nikAttribute;
+  Result:=Resolver.GetAttributeID(aName,true);
+  if Result<=CSSIDNone then
+  begin
+    El.NumericalID:=-1;
+    Log(etWarning,20240822172823,'unknown attribute "'+aName+'"',El);
+  end else
+    El.NumericalID:=Result;
+end;
+
+function TCSSResolverParser.ResolveType(El: TCSSResolvedIdentifierElement): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  if El.NumericalID<>CSSIDNone then
+    raise Exception.Create('20240822133813');
+  aName:=El.Name;
+  El.Kind:=nikType;
+  Result:=Resolver.GetTypeID(aName);
+  if Result<=CSSIDNone then
+  begin
+    El.NumericalID:=-1;
+    Log(etWarning,20240822133816,'unknown type "'+aName+'"',El);
+  end else
+    El.NumericalID:=Result;
+end;
+
+function TCSSResolverParser.ResolvePseudoClass(
+  El: TCSSResolvedPseudoClassElement): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  aName:=El.Name;
+  // pseudo classes are ASCII case insensitive
+  System.Delete(aName,1,1);
+  aName:=lowercase(aName);
+
+  if El.NumericalID<>CSSIDNone then
+    raise Exception.Create('20240701143234');
+
+  El.Kind:=nikPseudoClass;
+  Result:=Resolver.GetPseudoClassID(aName);
+  //writeln('TCSSResolverParser.ResolvePseudoClass ',aName,' ID=',Result);
+  if Result<=CSSIDNone then
+  begin
+    El.NumericalID:=-1;
+    Log(etWarning,20240822172826,'unknown pseudo class "'+aName+'"',El);
+  end else
+    El.NumericalID:=Result;
+end;
+
+function TCSSResolverParser.ResolvePseudoFunction(El: TCSSResolvedCallElement
+  ): TCSSNumericalID;
+var
+  aName: TCSSString;
+begin
+  if El.NameNumericalID<>CSSIDNone then
+    raise Exception.Create('20240701143035');
+  aName:=El.Name;
+  if aName[1]<>':' then
+    raise Exception.Create('20240701143650');
+
+  // pseudo functions are ASCII case insensitive
+  System.Delete(aName,1,1);
+  aName:=lowercase(aName);
+
+  El.Kind:=nikPseudoFunction;
+  Result:=Resolver.GetPseudoFunctionID(aName);
+  //writeln('TCSSResolverParser.ResolvePseudoFunction ',aName,' ID=',Result);
+  if Result<=CSSIDNone then
+  begin
+    El.NameNumericalID:=-1;
+    Log(etWarning,20240822172830,'unknown pseudo function "'+aName+'"',El);
+  end else
+    El.NameNumericalID:=Result;
+end;
+
+function TCSSResolverParser.ParseCall(aName: TCSSString; IsSelector: boolean
+  ): TCSSCallElement;
+var
+  CallID: TCSSNumericalID;
+begin
+  Result:=inherited ParseCall(aName, IsSelector);
+  if IsSelector then
+  begin
+    if Result.Name[1]=':' then
+    begin
+      CallID:=ResolvePseudoFunction(TCSSResolvedCallElement(Result));
+      case CallID of
+      CSSCallID_NthChild,CSSCallID_NthLastChild,
+      CSSCallID_NthOfType,CSSCallID_NthLastOfType: CheckNthChildParams(TCSSResolvedCallElement(Result));
+      end;
+    end
+    else
+      Log(etWarning,20240701222744,'invalid selector function',Result);
+  end;
+end;
+
+function TCSSResolverParser.ParseDeclaration(aIsAt: Boolean
+  ): TCSSDeclarationElement;
+var
+  aKey: TCSSElement;
+  AttrId: TCSSNumericalID;
+  Desc: TCSSAttributeDesc;
+  AttrData: TCSSAttributeKeyData;
+  i, ChildCnt: Integer;
+begin
+  Result:=inherited ParseDeclaration(aIsAt);
+  if Result.KeyCount<>1 then
+  begin
+    if Result.KeyCount<1 then
+      Log(etWarning,20231112135955,'missing keys in declaration',Result);
+    if Result.KeyCount>1 then
+      Log(etWarning,20231112140722,'too many keys in declaration',Result);
+    exit;
+  end;
+
+  aKey:=Result.Keys[0];
+  if aKey is TCSSResolvedIdentifierElement then
+  begin
+    AttrId:=ResolveAttribute(TCSSResolvedIdentifierElement(aKey));
+
+    if aKey.CustomData<>nil then
+      raise Exception.Create('20240626113536');
+    AttrData:=CSSAttributeKeyDataClass.Create;
+    aKey.CustomData:=AttrData;
+
+    ChildCnt:=Result.ChildCount;
+    if ChildCnt=0 then
+    begin
+      AttrData.Invalid:=true;
+      exit;
+    end;
+    for i:=0 to ChildCnt-1 do
+    begin
+      if (i>0) then
+        AttrData.Value+=', ';
+      AttrData.Value+=Result.Children[i].AsString;
+    end;
+
+    if AttrId>=CSSAttributeID_All then
+    begin
+      Desc:=Resolver.GetAttributeDesc(AttrId);
+
+      if Pos('var(',AttrData.Value)>0 then
+      begin
+        // cannot be parsed yet
+      end else if AttrID<Resolver.CSSRegistry.AttributeCount then
+      begin
+        if Resolver.InitParseAttr(Desc,AttrData,AttrData.Value) then
+        begin
+          if Assigned(Desc.OnCheck) then
+            AttrData.Invalid:=not Desc.OnCheck(Resolver);
+        end;
+      end;
+      {$IFDEF VerboseCSSResolver}
+      if AttrData.Invalid then
+        Log(etWarning,20240710162400,'Invalid CSS attribute value: '+Result.AsString,aKey);
+      {$ENDIF}
+    end;
+  end else begin
+    Log(etWarning,20220908230855,'Expected property name, but found '+aKey.ClassName,aKey);
+  end;
+end;
+
+function TCSSResolverParser.ParseSelector: TCSSElement;
+begin
+  Result:=inherited ParseSelector;
+  CheckSelector(Result);
+end;
+
+procedure TCSSResolverParser.CheckSelector(El: TCSSElement);
+var
+  C: TClass;
+begin
+  C:=El.ClassType;
+  if C=TCSSResolvedIdentifierElement then
+    // e.g. div {}
+    ResolveType(TCSSResolvedIdentifierElement(El))
+  else if C=TCSSHashIdentifierElement then
+    // e.g. #id {}
+  else if C=TCSSClassNameElement then
+    // e.g. .classname {}
+  else if C=TCSSResolvedPseudoClassElement then
+    // e.g. :pseudoclass {}
+    ResolvePseudoClass(TCSSResolvedPseudoClassElement(El))
+  else if C=TCSSBinaryElement then
+    CheckSelectorBinary(TCSSBinaryElement(El))
+  else if C=TCSSArrayElement then
+    CheckSelectorArray(TCSSArrayElement(El))
+  else if C=TCSSListElement then
+    CheckSelectorList(TCSSListElement(El))
+  else if C=TCSSResolvedCallElement then
+    // checked via ParseCall
+  else
+    Log(etWarning,20240625131810,'Unknown CSS selector element',El);
+end;
+
+procedure TCSSResolverParser.CheckSelectorArray(anArray: TCSSArrayElement);
+var
+  {$IFDEF VerboseCSSResolver}
+  i: integer;
+  {$ENDIF}
+  El: TCSSElement;
+  C: TClass;
+  aValue: TCSSString;
+begin
+  if anArray.Prefix<>nil then
+  begin
+    Log(etWarning,20240625134737,'Invalid CSS attribute selector prefix',anArray.Prefix);
+    exit;
+  end;
+
+  {$IFDEF VerboseCSSResolver}
+  writeln('TCSSResolverParser.CheckSelectorArray Prefix=',GetCSSObj(anArray.Prefix),' ChildCount=',anArray.ChildCount);
+  for i:=0 to anArray.ChildCount-1 do
+    writeln('TCSSResolverParser.CheckSelectorArray ',i,' ',GetCSSObj(anArray.Children[i]));
+  {$ENDIF}
+  if anArray.ChildCount<1 then
+  begin
+    Log(etWarning,20220910154033,'Invalid CSS attribute selector',anArray);
+    exit;
+  end;
+
+  if anArray.ChildCount>1 then
+  begin
+    El:=anArray.Children[1];
+    C:=El.ClassType;
+    if C=TCSSResolvedIdentifierElement then
+    begin
+      aValue:=TCSSResolvedIdentifierElement(El).Value;
+      case aValue of
+      'i','s': ;
+      else
+        Log(etWarning,20240625134931,'Invalid attribute modifier "'+aValue+'"',El);
+        exit;
+      end;
+    end else begin
+      Log(etWarning,20240625134940,'Invalid CSS attribute modifier',El);
+      exit;
+    end;
+  end;
+  if (anArray.ChildCount>2) then
+    Log(etWarning,20240625134951,'Invalid CSS attribute modifier',anArray.Children[2]);
+
+  El:=anArray.Children[0];
+  C:=El.ClassType;
+  if C=TCSSResolvedIdentifierElement then
+  begin
+    // [name]  ->  has explicit attribute
+    ResolveAttribute(TCSSResolvedIdentifierElement(El));
+  end else if C=TCSSBinaryElement then
+    CheckSelectorArrayBinary(TCSSBinaryElement(El))
+  else begin
+    Log(etWarning,20240625135119,'Invalid CSS array selector',El);
+  end;
+end;
+
+procedure TCSSResolverParser.CheckSelectorArrayBinary(aBinary: TCSSBinaryElement
+  );
+var
+  Left, Right: TCSSElement;
+  C: TClass;
+begin
+  Left:=aBinary.Left;
+  if Left.ClassType<>TCSSResolvedIdentifierElement then
+  begin
+    Log(etWarning,20240625154314,'Invalid CSS array selector, expected attribute',Left);
+    exit;
+  end;
+  ResolveAttribute(TCSSResolvedIdentifierElement(Left));
+
+  Right:=aBinary.Right;
+  C:=Right.ClassType;
+  if (C=TCSSStringElement) or (C=TCSSIntegerElement) or (C=TCSSFloatElement)
+      or (C=TCSSResolvedIdentifierElement) then
+    // ok
+  else begin
+    Log(etWarning,20240625154455,'Invalid CSS array selector, expected string',Right);
+    exit;
+  end;
+  ComputeValue(Right);
+
+  case aBinary.Operation of
+  boEquals,
+  boSquaredEqual,
+  boDollarEqual,
+  boPipeEqual,
+  boStarEqual,
+  boTildeEqual: ;
+  else
+    Log(etWarning,20240625154617,'Invalid CSS array selector operator',aBinary);
+  end;
+end;
+
+procedure TCSSResolverParser.CheckSelectorBinary(aBinary: TCSSBinaryElement);
+begin
+  case aBinary.Operation of
+  boGT,
+  boPlus,
+  boTilde,
+  boWhiteSpace: ;
+  else
+    Log(etWarning,20240625153307,'Invalid CSS binary selector '+BinaryOperators[aBinary.Operation],aBinary);
+  end;
+
+  CheckSelector(aBinary.Left);
+  CheckSelector(aBinary.Right);
+end;
+
+procedure TCSSResolverParser.CheckSelectorList(aList: TCSSListElement);
+var
+  i: Integer;
+  El: TCSSElement;
+begin
+  for i:=0 to aList.ChildCount-1 do
+  begin
+    El:=aList.Children[i];
+    {$IFDEF VerboseCSSResolver}
+    writeln('TCSSResolverParser.CheckSelectorList ',i,' ',GetCSSObj(El),' AsString=',El.AsString);
+    {$ENDIF}
+    CheckSelector(El);
+  end;
+end;
+
+procedure TCSSResolverParser.CheckNthChildParams(aCall: TCSSResolvedCallElement);
+
+  procedure NthWarn(const ID: TCSSMsgID; const Msg: string; PosEl: TCSSElement);
+  begin
+    Log(etWarning,ID,CSSSelectorCallNames[aCall.NameNumericalID]+' '+Msg,PosEl);
+  end;
+
+var
+  i, ArgCount, aModulo, aStart: Integer;
+  Arg, OffsetEl: TCSSElement;
+  Str: TCSSString;
+  UnaryEl, anUnary: TCSSUnaryElement;
+  Params: TCSSNthChildParams;
+begin
+  if aCall.Params<>nil then
+    raise Exception.Create('20240625150639');
+  ArgCount:=aCall.ArgCount;
+  {$IFDEF VerboseCSSResolver}
+  writeln('TCSSResolverParser.CheckSelectorCall_NthChild ArgCount=',aCall.ArgCount);
+  for i:=0 to aCall.ArgCount-1 do
+    writeln('TCSSResolverParser.CheckSelectorCall_NthChild Arg[',i,'] ',GetCSSObj(aCall.Args[i]),' AsString=',aCall.Args[i].AsString);
+  {$ENDIF}
+
+  // An, An+B, An+B of S, odd, even
+  i:=0;
+  aModulo:=0;
+  aStart:=1;
+  // check step
+  if ArgCount<=i then
+  begin
+    NthWarn(20220915143843,'missing arguments',aCall);
+    exit;
+  end;
+  Arg:=aCall.Args[i];
+  if Arg.ClassType=TCSSIntegerElement then
+  begin
+    aModulo:=TCSSIntegerElement(Arg).Value;
+    inc(i);
+    // check n
+    if ArgCount<=i then
+    begin
+      NthWarn(20220915143843,'missing arguments',aCall);
+      exit;
+    end;
+    Arg:=aCall.Args[i];
+    if Arg.ClassType<>TCSSResolvedIdentifierElement then
+    begin
+      NthWarn(20220915144312,'expected n',Arg);
+      exit;
+    end;
+    if TCSSResolvedIdentifierElement(Arg).Value<>'n' then
+    begin
+      NthWarn(20220915144359,'expected n',Arg);
+      exit;
+    end;
+
+  end
+  else if Arg.ClassType=TCSSResolvedIdentifierElement then
+  begin
+    Str:=TCSSResolvedIdentifierElement(Arg).Value;
+    case lowercase(Str) of
+    'even':
+      begin
+      aModulo:=2;
+      aStart:=2;
+      end;
+    'odd':
+      begin
+      aModulo:=2;
+      end;
+    'n':
+      begin
+      aModulo:=1;
+      end;
+    else
+      NthWarn(20220915150332,'expected multiplier',Arg);
+      exit;
+    end
+  end else if Arg.ClassType=TCSSUnaryElement then
+  begin
+    anUnary:=TCSSUnaryElement(Arg);
+    case anUnary.Operation of
+    uoMinus: aModulo:=-1;
+    uoPlus: aModulo:=1;
+    else
+      NthWarn(20220917080309,'expected multiplier',Arg);
+      exit;
+    end;
+    if (anUnary.Right.ClassType=TCSSResolvedIdentifierElement)
+        and (SameText(TCSSResolvedIdentifierElement(anUnary.Right).Value,'n')) then
+    begin
+      // ok
+    end else begin
+      NthWarn(20220917080154,'expected multiplier',Arg);
+      exit;
+    end;
+  end else
+  begin
+    NthWarn(20220915144056,'expected multiplier',Arg);
+    exit;
+  end;
+
+  inc(i);
+  if ArgCount>i then
+  begin
+    Arg:=aCall.Args[i];
+    if Arg.ClassType=TCSSUnaryElement then
+    begin
+      UnaryEl:=TCSSUnaryElement(Arg);
+      if not (UnaryEl.Operation in [uoMinus,uoPlus]) then
+      begin
+        NthWarn(20220915151422,'unexpected offset',UnaryEl);
+        exit;
+      end;
+      OffsetEl:=UnaryEl.Right;
+      if OffsetEl=nil then
+      begin
+        NthWarn(20220915151511,'unexpected offset',UnaryEl);
+        exit;
+      end;
+      if OffsetEl.ClassType<>TCSSIntegerElement then
+      begin
+        NthWarn(20220915151718,'unexpected offset',OffsetEl);
+        exit;
+      end;
+      aStart:=TCSSIntegerElement(OffsetEl).Value;
+      if UnaryEl.Operation=uoMinus then
+        aStart:=-aStart;
+    end else
+    begin
+      NthWarn(20220915150851,'expected offset',Arg);
+      exit;
+    end;
+  end;
+
+  Params:=CSSNthChildParamsClass.Create;
+  aCall.Params:=Params;
+  Params.Modulo:=aModulo;
+  Params.Start:=aStart;
+
+  inc(i);
+  if (i<ArgCount) then
+  begin
+    Arg:=aCall.Args[i];
+    if (Arg.ClassType=TCSSResolvedIdentifierElement)
+        and (SameText(TCSSResolvedIdentifierElement(Arg).Value,'of')) then
+    begin
+      // An+B of Selector
+      inc(i);
+      if i=ArgCount then
+      begin
+        NthWarn(20240711154813,'expected selector',Arg);
+        exit;
+      end;
+      Arg:=aCall.Args[i];
+      Params.HasOf:=true;
+      Params.OfSelector:=Arg;
+    end;
+  end;
+
+  if (aCall.NameNumericalID in [CSSCallID_NthOfType,CSSCallID_NthLastOfType]) then
+    Params.HasOf:=true;
+end;
+
+function TCSSResolverParser.ComputeValue(El: TCSSElement): TCSSString;
+var
+  ElData: TObject;
+  C: TClass;
+  StrEl: TCSSStringElement;
+  IntEl: TCSSIntegerElement;
+  FloatEl: TCSSFloatElement;
+begin
+  C:=El.ClassType;
+  if C=TCSSResolvedIdentifierElement then
+    Result:=TCSSResolvedIdentifierElement(El).Value
+  else if (C=TCSSStringElement)
+      or (C=TCSSIntegerElement)
+      or (C=TCSSFloatElement) then
+  begin
+    ElData:=El.CustomData;
+    if ElData is TCSSValueData then
+      exit(TCSSValueData(ElData).NormValue);
+    if C=TCSSStringElement then
+    begin
+      StrEl:=TCSSStringElement(El);
+      Result:=StrEl.Value;
+    end
+    else if C=TCSSIntegerElement then
+    begin
+      IntEl:=TCSSIntegerElement(El);
+      Result:=IntEl.AsString;
+    end else if C=TCSSFloatElement then
+    begin
+      FloatEl:=TCSSFloatElement(El);
+      Result:=FloatEl.AsString;
+    end;
+    ElData:=TCSSValueData.Create;
+    TCSSValueData(ElData).NormValue:=Result;
+    El.CustomData:=ElData;
+  end else begin
+    Log(etWarning,20240625162632,'TCSSResolverParser.ComputeValue not supported',El);
+  end;
+end;
+
+constructor TCSSResolverParser.Create(AScanner: TCSSScanner);
+begin
+  inherited Create(AScanner);
+  CSSIdentifierElementClass:=TCSSResolvedIdentifierElement;
+  CSSPseudoClassElementClass:=TCSSResolvedPseudoClassElement;
+  CSSCallElementClass:=TCSSResolvedCallElement;
+  CSSNthChildParamsClass:=TCSSNthChildParams;
+  CSSAttributeKeyDataClass:=TCSSAttributeKeyData;
+end;
+
+destructor TCSSResolverParser.Destroy;
+begin
+  inherited Destroy;
+end;
+
+procedure TCSSResolverParser.Log(MsgType: TEventType; const ID: TCSSMsgID;
+  const Msg: TCSSString; PosEl: TCSSElement);
+begin
+  if Assigned(OnLog) then
+    OnLog(MsgType,ID,Msg,PosEl);
+end;
+
+class function TCSSResolverParser.IsWhiteSpace(const s: TCSSString): boolean;
+var
+  i: Integer;
+begin
+  for i:=1 to length(s) do
+    if not (s[i] in [' ',#10,#13]) then
+      exit(false);
+  Result:=true;
+end;
+
+end.
+

+ 163 - 124
src/base/fcl-css/fpcssscanner.pp

@@ -79,13 +79,10 @@ Type
     ctkPIPE,
     ctkPIPEEQUAL,
     ctkDOLLAR,
-    ctkDOLLAREQUAL,
-    ctkINVALID
+    ctkDOLLAREQUAL
    );
   TCSSTokens = Set of TCSSToken;
 
-  TCSSString = UTF8String;
-
 resourcestring
   SErrInvalidCharacter = 'Invalid character ''%s''';
   SErrOpenString = 'String exceeds end of line';
@@ -130,13 +127,12 @@ Type
 
   { TCSSScanner }
 
-  TCSSScannerOption = (csoExtendedIdentifiers,csoReturnComments,csoReturnWhiteSpace);
+  TCSSScannerOption = (csoExtendedIdentifiers,csoReturnComments,csoReturnWhiteSpace,csoDisablePseudo);
   TCSSScannerOptions = set of TCSSScannerOption;
-  TCSSScannerWarnEvent = procedure(Sender: TObject; Msg: string) of object;
+  TCSSScannerWarnEvent = procedure(Sender: TObject; Msg: TCSSString) of object;
 
   TCSSScanner = class
   private
-    FDisablePseudo: Boolean;
     FOnWarn: TCSSScannerWarnEvent;
     FOptions: TCSSScannerOptions;
     FSourceFile: TLineReader;
@@ -145,7 +141,7 @@ Type
     FCurToken: TCSSToken;
     FCurTokenString: TCSSString;
     FCurLine: TCSSString;
-    TokenStr: PAnsiChar;
+    TokenStr: PCSSChar;
     FSourceStream : TStream;
     FOwnSourceFile : Boolean;
     function DoHash: TCSSToken;
@@ -156,17 +152,16 @@ Type
     function DoNumericLiteral: TCSSToken;
     function DoSingleLineComment: TCSSToken;
     function DoStringLiteral: TCSSToken;
+    function DoStringEscape: TCSSToken;
     function DoWhiteSpace: TCSSToken;
     function EatBadURL: TCSSToken;
     Function DoUnicodeRange : TCSSTOKEN;
     function FetchLine: Boolean;
     function GetCurColumn: Integer;
-    function GetReturnComments: Boolean;
-    function GetReturnWhiteSpace: Boolean;
+    function GetOption(anOption: TCSSScannerOption): Boolean;
     function ReadUnicodeEscape: WideChar;
-    procedure SetReturnComments(AValue: Boolean);
-    procedure SetReturnWhiteSpace(AValue: Boolean);
-    class function UnknownCharToStr(C: AnsiChar): TCSSString;
+    procedure SetOption(anOption: TCSSScannerOption; const AValue: Boolean);
+    class function UnknownCharToStr(C: TCSSChar): TCSSString;
   protected
     procedure DoError(const Msg: TCSSString; Args: array of const); overload;
     procedure DoError(const Msg: TCSSString); overload;
@@ -178,8 +173,8 @@ Type
     procedure OpenFile(const AFilename: TCSSString);
     Function FetchToken: TCSSToken;
     function IsUTF8BOM: boolean;
-    Property ReturnComments : Boolean Read GetReturnComments Write SetReturnComments;
-    Property ReturnWhiteSpace : Boolean Read GetReturnWhiteSpace Write SetReturnWhiteSpace;
+    Property ReturnComments : Boolean Index csoReturnComments Read GetOption Write SetOption;
+    Property ReturnWhiteSpace : Boolean Index csoReturnWhiteSpace Read GetOption Write SetOption;
     Property Options : TCSSScannerOptions Read FOptions Write FOptions;
     property SourceFile: TLineReader read FSourceFile;
     property CurFilename: TCSSString read FSourceFilename;
@@ -188,11 +183,11 @@ Type
     property CurColumn: Integer read GetCurColumn;
     property CurToken: TCSSToken read FCurToken;
     property CurTokenString: TCSSString read FCurTokenString;
-    property DisablePseudo : Boolean Read FDisablePseudo Write FDisablePseudo;
+    property DisablePseudo : Boolean Index csoDisablePseudo Read GetOption Write SetOption;
     property OnWarn: TCSSScannerWarnEvent read FOnWarn write FOnWarn;
   end;
 
-function SafeFormat(const Fmt: string; const Args: array of const): string;
+function SafeFormat(const Fmt: TCSSString; const Args: array of const): TCSSString;
 
 implementation
 
@@ -200,11 +195,11 @@ Const
   Alpha = ['A'..'Z','a'..'z'];
   Num   = ['0'..'9'];
   AlNum = Alpha+Num;
-  AlNumIden = Alpha+Num+['-'];
+  AlNumIden = AlNum+['-'];
   WhiteSpace = [' ',#9];
 
 type
-  TMessageArgs = array of string;
+  TMessageArgs = array of TCSSString;
 
 procedure CreateMsgArgs(var MsgArgs: TMessageArgs; const Args: array of const);
 var
@@ -281,7 +276,8 @@ begin
     {$endif}
 end;
 
-function SafeFormat(const Fmt: string; const Args: array of const): string;
+function SafeFormat(const Fmt: TCSSString;
+  const Args: array of const): TCSSString;
 var
   MsgArgs: TMessageArgs;
   i: Integer;
@@ -335,7 +331,7 @@ end;
 
 constructor TCSSScanner.Create(AStream: TStream);
 begin
-  FSourceStream:=ASTream;
+  FSourceStream:=AStream;
   FOwnSourceFile:=True;
   Create(TStreamLineReader.Create(AStream));
 end;
@@ -343,7 +339,7 @@ end;
 destructor TCSSScanner.Destroy;
 begin
   If FOwnSourceFile then
-    FSourceFile.Free;
+    FreeAndNil(FSourceFile);
   inherited Destroy;
 end;
 
@@ -363,7 +359,7 @@ begin
   end else
   begin
     FCurLine := FSourceFile.ReadLine;
-    TokenStr := PAnsiChar(CurLine);
+    TokenStr := PCSSChar(CurLine);
     Result := true;
     Inc(FCurRow);
   end;
@@ -387,7 +383,7 @@ end;
 function TCSSScanner.DoSingleLineComment : TCSSToken;
 
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len : Integer;
 
 begin
@@ -405,9 +401,9 @@ end;
 function TCSSScanner.DoMultiLineComment : TCSSToken;
 
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len,OLen : Integer;
-  PrevToken : AnsiChar;
+  PrevToken : TCSSChar;
 
 begin
   Inc(TokenStr);
@@ -485,28 +481,20 @@ begin
   Result:=WideChar(StrToInt('$'+S));
 end;
 
-procedure TCSSScanner.SetReturnComments(AValue: Boolean);
+procedure TCSSScanner.SetOption(anOption: TCSSScannerOption; const AValue: Boolean
+  );
 begin
   if AValue then
-    Include(FOptions,csoReturnComments)
+    Include(FOptions,anOption)
   else
-    Exclude(FOptions,csoReturnComments)
+    Exclude(FOptions,anOption);
 end;
 
-procedure TCSSScanner.SetReturnWhiteSpace(AValue: Boolean);
-begin
-  if AValue then
-    Include(FOptions,csoReturnWhiteSpace)
-  else
-    Exclude(FOptions,csoReturnWhiteSpace)
-end;
-
-
 function TCSSScanner.DoStringLiteral: TCSSToken;
 
 Var
-  Delim : AnsiChar;
-  TokenStart : PAnsiChar;
+  Delim : TCSSChar;
+  TokenStart : PCSSChar;
   Len,OLen: Integer;
   S : TCSSString;
 
@@ -541,7 +529,7 @@ begin
         Move(TokenStart^, FCurTokenString[OLen + 1], Len);
       Move(S[1],FCurTokenString[OLen + Len+1],Length(S));
       Inc(OLen, Len+Length(S));
-      // Next AnsiChar
+      // Next char
       // Inc(TokenStr);
       TokenStart := TokenStr+1;
       end;
@@ -559,58 +547,105 @@ begin
   Result := ctkSTRING;
 end;
 
-function TCSSScanner.DoNumericLiteral :TCSSToken;
+function TCSSScanner.DoStringEscape: TCSSToken;
+var
+  TokenStart: PCSSChar;
+  Len: Integer;
+begin
+  Inc(TokenStr); // skip \
+  TokenStart := TokenStr;
+  while TokenStr[0] in Num do
+    inc(TokenStr);
+  Len:=TokenStr-TokenStart;
+  Setlength(FCurTokenString, Len);
+  if (Len>0) then
+    Move(TokenStart^,FCurTokenString[1],Len);
+  Result:=ctkString;
+  FCurTokenString:=TCSSChar(StrToInt(FCurTokenString));
+end;
+
+function TCSSScanner.DoNumericLiteral: TCSSToken;
+// number: 1, 0.2, .3, 4.01, 0.0, +0.0, -0.0, .50, 2e3, -6.7E-2
+const
+  NumEnd = [#0..#31,' ',';','{','}',
+    ',',
+    ')', // e.g. calc(3*4)
+    '*','/', // e.g. 3*4, note that + and - require whitespace
+    'a'..'z','A'..'Z' // e.g. 3px
+    ];
+
+  procedure Skip;
+  begin
+    while not (TokenStr^ in NumEnd) do inc(TokenStr);
+    Result:=ctkUNKNOWN;
+  end;
 
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len : Integer;
-  isEscape : Boolean;
+  HasNumber: Boolean;
 
 begin
   Result := ctkINTEGER;
-  isEscape:=TokenStr[0]='\';
-  if IsEscape then
-    Inc(TokenStr);
   TokenStart := TokenStr;
-  while true do
+  case TokenStr^ of
+  '-': inc(TokenStr);
+  '+': if csoReturnWhiteSpace in Options then inc(TokenStr);
+  end;
+  HasNumber:=false;
+  if TokenStr^ in Num then
     begin
-    Inc(TokenStr);
-    case TokenStr[0] of
-      '.':
-        if IsEscape then
-          Break
-        else
-          begin
-            Result := ctkFLOAT;
-            if TokenStr[1] in ['0'..'9'] then
-            begin
-              Inc(TokenStr);
-              repeat
-                Inc(TokenStr);
-              until not (TokenStr[0] in ['0'..'9']);
-            end;
-            break;
-          end;
-      '0'..'9': ;
-      else
-        break;
+    // read significand
+    HasNumber:=true;
+    repeat
+      inc(TokenStr);
+    until not (TokenStr^ in Num);
     end;
-  end;
+  if TokenStr^='.' then
+    begin
+    // read fraction
+    inc(TokenStr);
+    if TokenStr^ in Num then
+      begin
+      Result := ctkFLOAT;
+      HasNumber:=true;
+      repeat
+        inc(TokenStr);
+      until not (TokenStr^ in Num)
+      end;
+    end;
+  if not HasNumber then
+    begin
+    Skip;
+    exit;
+    end;
+  if (TokenStr^ in ['e','E']) and not (TokenStr[1] in Alpha) then
+    begin
+    // read exponent
+    Result := ctkFLOAT;
+    inc(TokenStr);
+    if TokenStr^ in ['-','+'] then
+      inc(TokenStr);
+    if not (TokenStr^ in Num) then
+      begin
+      Skip;
+      exit;
+      end;
+    repeat
+      inc(TokenStr);
+    until not (TokenStr^ in Num)
+    end;
+
   Len:=TokenStr-TokenStart;
   Setlength(FCurTokenString, Len);
   if (Len>0) then
-  Move(TokenStart^,FCurTokenString[1],Len);
-  if IsEscape then
-    begin
-    Result:=ctkString;
-    FCurTokenString:=AnsiChar(StrToInt(FCurTokenString));
-    end;
+    Move(TokenStart^,FCurTokenString[1],Len);
 end;
 
 function TCSSScanner.DoHash :TCSSToken;
 
 Var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   Len : Integer;
 
 begin
@@ -629,7 +664,7 @@ end;
 function TCSSScanner.EatBadURL: TCSSToken;
 
 var
-  TokenStart : PAnsiChar;
+  TokenStart : PCSSChar;
   C : AnsiChar;
   len,oldlen : integer;
 
@@ -658,9 +693,9 @@ end;
 
 function TCSSScanner.DoUnicodeRange: TCSSTOKEN;
 Var
-  TokenStart:PAnsiChar;
+  TokenStart: PCSSChar;
   Len : Integer;
-  Tokens : Set of AnsiChar;
+  Tokens : Set of TCSSChar;
 
 begin
   Tokens:= ['A'..'F', 'a'..'f', '0'..'9', '-'];
@@ -680,7 +715,7 @@ begin
 
 end;
 
-class function TCSSScanner.UnknownCharToStr(C: AnsiChar): TCSSString;
+class function TCSSScanner.UnknownCharToStr(C: TCSSChar): TCSSString;
 
 begin
   if C=#0 then
@@ -694,7 +729,7 @@ end;
 function TCSSScanner.DoIdentifierLike : TCSSToken;
 
 Var
-  TokenStart:PAnsiChar;
+  TokenStart: PCSSChar;
   Len,oLen : Integer;
   IsEscape,IsAt, IsPseudo, IsFunc : Boolean;
 
@@ -744,7 +779,17 @@ begin
     Result:=ctkATKEYWORD
   else if CurTokenString='!important' then
     Result:=ctkIMPORTANT
-  else if (CurtokenString='url(') then
+  else if CurTokenString='!' then
+    begin
+    if (TokenStr^=' ') and CompareMem(TokenStr+1,PChar('important'),9)
+    and (TokenStr[10] in [' ',';']) then
+      begin
+      inc(TokenStr,10);
+      FCurTokenString:='!important';
+      Result:=ctkIMPORTANT;
+      end;
+    end
+  else if (CurTokenString='url(') then
     begin
     Result:=ctkURL;
     If TokenStr[0] in ['"',''''] then
@@ -769,13 +814,13 @@ end;
 
 function TCSSScanner.DoInvalidChars: TCSSToken;
 var
-  TokenStart: PAnsiChar;
+  TokenStart: PCSSChar;
   Len: SizeUInt;
 begin
-  Result:=ctkINVALID;
+  Result:=ctkUNKNOWN;
   TokenStart := TokenStr;
   repeat
-    writeln('TCSSScanner.DoInvalidChars ',hexstr(ord(TokenStr^),2));
+    //writeln('TCSSScanner.DoInvalidChars ',hexstr(ord(TokenStr^),2));
     Inc(TokenStr);
   until (TokenStr[0] in [#0,#9,#10,#13,#32..#127]);
   Len:=TokenStr-TokenStart;
@@ -792,7 +837,7 @@ var
 begin
   Repeat
     Result:=DoFetchToken;
-    if (Result=ctkINVALID) and IsUTF8BOM then
+    if (Result=ctkUNKNOWN) and IsUTF8BOM then
       CanStop:=false
     else
       CanStop:=(Not (Result in [ctkComment,ctkWhiteSpace]))
@@ -843,35 +888,35 @@ begin
   //CurPos:=TokenStr;
   FCurTokenString := '';
   case TokenStr[0] of
-    #0:         // Empty line
+    #0:         // EOL
       begin
       FetchLine;
       Result := ctkWhitespace;
       end;
     '''','"':
-       Result:=DoStringLiteral;
+      Result:=DoStringLiteral;
     '/' :
-       Result:=CommentDiv;
+      Result:=CommentDiv;
     #9, ' ':
-       Result := DoWhiteSpace;
+      Result := DoWhiteSpace;
     '#':
-       Result:=DoHash;
+      Result:=DoHash;
     '\':
-       begin
-       if TokenStr[1] in ['0'..'9'] then
-         Result:=DoNumericLiteral
-       else
-         begin
-         if (TokenStr[1] in WhiteSpace) or (TokenStr[1]=#0) then
-           DoError(SErrUnknownCharacter ,[UnknownCharToStr(TokenStr[1])])
-         else
-           Result:=DoIdentifierLike
-         end;
-       end;
+      begin
+      if TokenStr[1] in ['0'..'9'] then
+        Result:=DoStringEscape
+      else
+        begin
+        if (TokenStr[1] in WhiteSpace) or (TokenStr[1]=#0) then
+          DoError(SErrUnknownCharacter ,[UnknownCharToStr(TokenStr[1])])
+        else
+          Result:=DoIdentifierLike
+        end;
+      end;
     '0'..'9':
-       Result:=DoNumericLiteral;
+      Result:=DoNumericLiteral;
     '&': CharToken(ctkAnd);
-    '{': CharToken( ctkLBRACE);
+    '{': CharToken(ctkLBRACE);
     '}': CharToken(ctkRBRACE);
     '*': if TokenStr[1]='=' then
            TwoCharsToken(ctkSTAREQUAL)
@@ -904,7 +949,7 @@ begin
     '@': Result:=DoIdentifierLike;
     ':':
       begin
-      if DisablePseudo then
+      if csoDisablePseudo in Options then
         CharToken(ctkCOLON)
       else if (TokenStr[1]=':') then
         begin
@@ -920,8 +965,10 @@ begin
       end;
     '.':
       begin
-      if (TokenStr[1] in AlNum) then
-        Result:=Self.DoIdentifierLike
+      if TokenStr[1] in Num then
+        Result:=DoNumericLiteral  // e.g. .1 = 0.1
+      else if TokenStr[1] in Alpha then
+        Result:=DoIdentifierLike
       else
         CharToken(ctkDOT);
       end;
@@ -945,16 +992,17 @@ begin
       '0'..'9':
         Result:=DoNumericLiteral;
       '.':
-        if TokenStr[2] in ['0'..'9'] then
+        if TokenStr[2] in Num then
           Result:=DoNumericLiteral
         else
           CharToken(ctkMINUS);
-      #9,' ',#0:
+      #9,#10,#13,' ',#0:
         CharToken(ctkMINUS);
       else
         Result:=DoIdentifierLike;
       end;
-    '+': CharToken(ctkPLUS);
+    '+':
+      CharToken(ctkPLUS);
     '%': CharToken(ctkPERCENTAGE);
     '_','!',
     'a'..'z',
@@ -966,12 +1014,8 @@ begin
          Result:=DoIdentifierLike;
        end;
   else
-    writeln('TCSSScanner.DoFetchToken ',Ord(TokenStr[0]));
-    If Ord(TokenStr[0])>127 then
-      Result:=DoInvalidChars
-    else
-      DoError(SErrUnknownCharacter ,['"'+TokenStr[0]+'"']);
-
+    //writeln('TCSSScanner.DoFetchToken ',Ord(TokenStr[0]));
+    Result:=DoInvalidChars;
   end; // Case
 end;
 
@@ -995,17 +1039,12 @@ begin
   if (TokenStr=Nil) or (Length(CurLine)=0) then
     Result:=0
   else
-    Result := TokenStr - PAnsiChar(CurLine);
-end;
-
-function TCSSScanner.GetReturnComments: Boolean;
-begin
-  Result:=(csoReturnComments in FOptions);
+    Result := TokenStr - PCSSChar(CurLine);
 end;
 
-function TCSSScanner.GetReturnWhiteSpace: Boolean;
+function TCSSScanner.GetOption(anOption: TCSSScannerOption): Boolean;
 begin
-  Result:=(csoReturnWhiteSpace in FOptions);
+  Result:=anOption in Options;
 end;
 
 { TStreamLineReader }

+ 198 - 13
src/base/fcl-css/fpcsstree.pp

@@ -18,7 +18,6 @@ unit fpCSSTree;
 {$ENDIF FPC_DOTTEDUNITS}
 
 {$mode ObjFPC}{$H+}
-{$codepage utf8}
 
 interface
 
@@ -29,12 +28,195 @@ uses Contnrs, RtlConsts, SysUtils, Classes, Math;
 {$ENDIF FPC_DOTTEDUNITS}
 
 
+const
+  CSSFormatSettings: TFormatSettings = (
+    CurrencyFormat: 1;
+    NegCurrFormat: 5;
+    ThousandSeparator: ',';
+    DecimalSeparator: '.';
+    CurrencyDecimals: 2;
+    DateSeparator: '-';
+    TimeSeparator: ':';
+    ListSeparator: ',';
+    CurrencyString: '$';
+    ShortDateFormat: 'd/m/y';
+    LongDateFormat: 'dd" "mmmm" "yyyy';
+    TimeAMString: 'AM';
+    TimePMString: 'PM';
+    ShortTimeFormat: 'hh:nn';
+    LongTimeFormat: 'hh:nn:ss';
+    ShortMonthNames: ('Jan','Feb','Mar','Apr','May','Jun',
+                      'Jul','Aug','Sep','Oct','Nov','Dec');
+    LongMonthNames: ('January','February','March','April','May','June',
+                     'July','August','September','October','November','December');
+    ShortDayNames: ('Sun','Mon','Tue','Wed','Thu','Fri','Sat');
+    LongDayNames:  ('Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday');
+    TwoDigitYearCenturyWindow: 50;
+  );
+
 Type
   ECSSException = class(Exception);
 
-  TCSSString = UTF8String;
-  TCSSStringDynArray = array of TCSSString;
-  TCSSUnits = (cuNONE, cuPX,cuPERCENT,cuREM,cuEM,cuPT,cuFR,cuVW,cuVH,cuDEG);
+  {$IF FPC_FULLVERSION>30300}
+  TCSSChar = Char;
+  TCSSString = String; // can be AnsiString or UnicodeString
+  {$ELSE}
+  TCSSChar = Char;
+  TCSSString = String;
+  {$ENDIF}
+  PCSSChar = ^TCSSChar;
+  TCSSStringArray = array of TCSSString;
+
+  TCSSUnit = (
+    cuNone, // no unit
+    // absolute lengths
+    cu_px,   // pixels
+    cu_cm,   // centimeters
+    cu_mm,   // milimeters
+    cu_Q,    // quarter-milimeters
+    cu_in,   // inches
+    cu_pt,   // points (1pt = 1/72 of 1in)
+    cu_pc,   // picas (1pc = 12 pt)
+    // percentage
+    cuPercent, // percentage, context sensitive
+    // relative to element's font
+    cu_em,   // relative to the height of char "M" of element's font
+    cu_ex,   // relative to the height of char "x" of element's font
+    cu_cap,  // cap height, relative to nominal height of capital letters
+    cu_ch,   // relative to the width of the "0" (zero)
+    cu_ic,   // advance measure of the "水" glyph (CJK water ideograph, U+6C34)
+    cu_lh,   // line-height
+    // relative to root's font
+    cu_rem,  // root-em, as EM, except the font of the root element
+    cu_rex,  // root-ex
+    cu_rcap, // root-cap height
+    cu_rchh,  // root-ch
+    cu_ric,  // root-ic
+    cu_rlh,  // root-line-height
+    // relative to default viewport size
+    cu_vw,   // relative to 1% of the width of the viewport
+    cu_vh,   // relative to 1% of the height of the viewport
+    cu_vmax, // relative to 1% of viewport's larger dimension
+    cu_vmin, // relative to 1% of viewport's smaller dimension
+    cu_vb,   // relative to 1% of viewport's block axis
+    cu_vi,   // relative to 1% of viewport's inline axis
+    // relative to small viewport (when e.g. viewport is shrunk to show browser interface)
+    cu_svw,  // small-vw
+    cu_svh,  // small-vh
+    cu_svmax,// small-vmax
+    cu_svmin,// small-vmin
+    cu_svb,  // small-vb
+    cu_svi,  // small-vi
+    // relative to large viewport (when e.g. browser hides interface and viewport is expanded)
+    cu_lvw,  // large-vw
+    cu_lvh,  // large-vh
+    cu_lvmax,// large-vmax
+    cu_lvmin,// large-vmin
+    cu_lvb,  // large-vb
+    cu_lvi,  // large-vi
+    // relative to dynamic viewport size aka current size
+    cu_dvw,  // dynamic-vw
+    cu_dvh,  // dynamic-vh
+    cu_dvmax,// dynamic-vmax
+    cu_dvmin,// dynamic-vmin
+    cu_dvb,  // dynamic-vb
+    cu_dvi,  // dynamic-vi
+    // container queries
+    cu_cqw,  // relative to 1% of container's width
+    cu_cqh,  // relative to 1% of container's height
+    cu_cqb,  // relative to 1% of container's block axis dimension
+    cu_cqi,  // relative to 1% of container's inline axis dimension
+    cu_cqmin,// relative to 1% of container's smaller dimension
+    cu_cqmax,// relative to 1% of container's larger dimension
+    // angles
+    cu_deg,  // degrees, full circle is 360deg
+    cu_grad, // gradians, full circle is 400grad
+    cu_rad,  // radians, full circle is (2*pi)rad
+    cu_turn, // turns, full circle is 1turn
+    // special
+    cu_fr    // fraction of flex space
+    );
+  TCSSUnits = set of TCSSUnit;
+const
+  cuAllAbsoluteLengths = [cu_px,cu_cm,cu_mm,cu_Q,cu_in,cu_pt,cu_pc];
+  cuAllViewportLengths = [cu_vw,cu_vh,cu_vmax,cu_vmin,cu_vb,cu_vi,
+                          cu_svw,cu_svh,cu_svmax,cu_svmin,cu_svb,cu_svi,
+                          cu_lvw,cu_lvh,cu_lvmax,cu_lvmin,cu_lvb,cu_lvi,
+                          cu_dvw,cu_dvh,cu_dvmax,cu_dvmin,cu_dvb,cu_dvi];
+  cuAllRelativeFontSize = [cu_em,cu_ex,cu_cap,cu_ch,cu_ic,cu_lh,
+                           cu_rem,cu_rex,cu_rcap,cu_rchh,cu_ric,cu_rlh];
+  cuAllLengths = cuAllAbsoluteLengths+cuAllViewportLengths+cuAllRelativeFontSize;
+  cuAllLengthsAndPercent = cuAllLengths+[cuPercent];
+  cuAllAngles = [cu_deg,cu_grad,cu_rad,cu_turn];
+
+  CSSUnitNames: array[TCSSUnit] of TCSSString = (
+    '',     // no unit
+    // absolute lengths
+    'px',   // pixels
+    'cm',   // centimeters
+    'mm',   // milimeters
+    'Q',    // quarter-milimeters, Big Q!
+    'in',   // inches
+    'pt',   // points
+    'pc',   // picas
+    // %
+    '%',    // percentage
+    // relative to element's font
+    'em',   // elements font-size
+    'ex',   // elements height of "x"
+    'cap',  // cap-height
+    'ch',   // character "0"
+    'ic',   // CJK water ideograph
+    'lh',   // line-height
+    // relative to root's font
+    'rem',  // root-em
+    'rex',  // root-ex
+    'rcap', // root-cap-height
+    'rch',  // root-character "0"
+    'ric',  // root-ic
+    'rlh',  // root-line-height
+    // relative to viewport
+    'vw',   // viewport-width
+    'vh',   // viewport-height
+    'vmax', // viewport larger dimension
+    'vmin', // viewport smaller dimension
+    'vb',   // viewport block axis size
+    'vi',   // viewport inline axis size
+    'svw',  // small-vw
+    'svh',  // small-vh
+    'svmax',// small-vmax
+    'svmin',// small-vmin
+    'svb',  // small-vb
+    'svi',  // small-vi
+    'lvw',  // large-vw
+    'lvh',  // large-vh
+    'lvmax',// large-vmax
+    'lvmin',// large-vmin
+    'lvb',  // large-vb
+    'lvi',  // large-vi
+    'dvw',  // dynamic-vw
+    'dvh',  // dynamic-vh
+    'dvmax',// dynamic-vmax
+    'dvmin',// dynamic-vmin
+    'dvb',  // dynamic-vb
+    'dvi',  // dynamic-vi
+    // container queries
+    'cqw',  // container's width
+    'cqh',  // container's height
+    'cqb',  // container's block axis dimension
+    'cqi',  // container's inline axis dimension
+    'cqmin',// container's smaller dimension
+    'cqmax',// container's larger dimension
+    // angles
+    'deg',  // degrees
+    'grad', // gradians
+    'rad',  // radians
+    'turn', // turns
+    // special
+    'fr'    // fraction
+    );
+
+type
   TCSSType = (
     csstUnknown,
     csstInteger, csstString, csstFloat,
@@ -145,7 +327,7 @@ Type
   TCSSIntegerElement = class(TCSSElement)
   private
     FIsEscaped: Boolean;
-    FUnits: TCSSUnits;
+    FUnits: TCSSUnit;
     FValue: Integer;
   protected
     function GetAsString(aFormat : Boolean; const aIndent : TCSSString): TCSSString; override;
@@ -154,7 +336,7 @@ Type
     function Equals(Obj: TObject): boolean; override;
     Property Value : Integer Read FValue Write FValue;
     Property IsEscaped : Boolean Read FIsEscaped Write FIsEscaped;
-    Property Units : TCSSUnits Read FUnits Write FUnits;
+    Property Units : TCSSUnit Read FUnits Write FUnits;
   end;
   TCSSIntegerElementClass = class of TCSSIntegerElement;
 
@@ -162,7 +344,7 @@ Type
 
   TCSSFloatElement = class(TCSSElement)
   private
-    FUnits: TCSSUnits;
+    FUnits: TCSSUnit;
     FValue: Double;
   protected
     function GetAsString(aFormat : Boolean; const aIndent : TCSSString): TCSSString;override;
@@ -170,7 +352,7 @@ Type
     Class function CSSType : TCSSType; override;
     function Equals(Obj: TObject): boolean; override;
     Property Value : Double Read FValue Write FValue;
-    Property Units : TCSSUnits Read FUnits Write FUnits;
+    Property Units : TCSSUnit Read FUnits Write FUnits;
   end;
   TCSSFloatElementClass = class of TCSSFloatElement;
 
@@ -448,14 +630,14 @@ Function StringToCSSString(const S : TCSSString) : TCSSString;
 // Escapes non-identifier characters C to \C
 Function StringToIdentifier(const S : TCSSString) : TCSSString;
 
+function FloatToCSSStr(const f: double): string;
+
 Function GetCSSObj(El: TCSSElement): TCSSString;
 Function GetCSSPath(El: TCSSElement): TCSSString;
 
 Function CSSElementListEquals(ListA, ListB: TCSSElementList): boolean;
 
 Const
-  CSSUnitNames : Array[TCSSUnits] of TCSSString =
-        ('','px','%','rem','em','pt','fr','vw','vh','deg');
   UnaryOperators : Array[TCSSUnaryOperation] of TCSSString =
         ('::','-','+','/','>','~');
   BinaryOperators : Array[TCSSBinaryOperation] of TCSSString =
@@ -566,6 +748,11 @@ begin
   SetLength(Result,iOut);
 end;
 
+function FloatToCSSStr(const f: double): string;
+begin
+  Result:=FloatToStr(f,CSSFormatSettings);
+end;
+
 function GetCSSObj(El: TCSSElement): TCSSString;
 begin
   if El=nil then
@@ -1075,9 +1262,7 @@ end;
 function TCSSFloatElement.GetAsString(aFormat: Boolean;
   const aIndent: TCSSString): TCSSString;
 begin
-  Str(Value:5:2,Result);
-  Result:=TrimLeft(Result); // Space for positive numbers
-  Result:=Result+CSSUnitNames[Units];
+  Result:=FloatToCSSStr(Value)+CSSUnitNames[Units];
   if aFormat then
     Result:=aIndent+Result;
 end;

+ 26 - 4
src/base/fresnel.classes.pas

@@ -24,7 +24,7 @@ unit Fresnel.Classes;
 interface
 
 uses
-  Classes, SysUtils, Math, Types, fpCSSScanner;
+  Classes, SysUtils, Math, Types, FpImage, fpCSSScanner, fpCSSResParser;
 
 type
   {$IF FPC_FULLVERSION<30301}
@@ -41,12 +41,11 @@ type
 
   TCalcBoolean = (cbCalc,cbFalse,cbTrue);
 
-
 const
   MaxFresnelLength = TFresnelLength(high(longint));
 
 const
-  FresnelCSSFormatSettings: TFormatSettings = (
+  FresnelCSSFormatSettings: TFormatSettings = ( // todo: use fpCSSTree.CSSFormatSettings
     CurrencyFormat: 1;
     NegCurrFormat: 5;
     ThousandSeparator: ',';
@@ -175,6 +174,7 @@ type
 
 
 function FloatToCSSStr(const f: TFresnelLength): string;
+function FloatToCSSPx(const p: TFresnelLength): string;
 function CSSStrToFloat(const s: string; out l: TFresnelLength): boolean;
 function CompareFresnelPoint(const A, B: TFresnelPoint): integer;
 function CompareFresnelRect(const A, B: TFresnelRect): integer;
@@ -182,6 +182,8 @@ function CompareFresnelRect(const A, B: TFresnelRect): integer;
 function PointFre(const X, Y: TFresnelLength): TFresnelPoint; overload;
 function RectFre(const Left, Top, Right, Bottom: TFresnelLength): TFresnelRect; overload;
 
+function FPColor(c: TCSSAlphaColor): TFPColor; overload;
+
 Procedure FLLog(aType: TEventType; Const Msg : string); overload;
 Procedure FLLog(aType: TEventType; Const Fmt : string; Const Args : Array of const); overload;
 Procedure FLLog(aType: TEventType; const args : Array of string); overload;
@@ -204,6 +206,11 @@ begin
   Result:=FloatToStr(f,FresnelCSSFormatSettings);
 end;
 
+function FloatToCSSPx(const p: TFresnelLength): string;
+begin
+  Result:=FloatToStr(p,FresnelCSSFormatSettings)+'px';
+end;
+
 function CSSStrToFloat(const s: string; out l: TFresnelLength): boolean;
 var
   Code: Integer;
@@ -268,6 +275,21 @@ begin
   Result.Bottom:=Bottom;
 end;
 
+function FPColor(c: TCSSAlphaColor): TFPColor;
+begin
+  Result.Blue:=c and $ff;
+  Result.Blue:=Result.Blue or (Result.Blue shl 8);
+  c:=c shr 8;
+  Result.Green:=c and $ff;
+  Result.Green:=Result.Green or (Result.Green shl 8);
+  c:=c shr 8;
+  Result.Red:=c and $ff;
+  Result.Red:=Result.Red or (Result.Red shl 8);
+  c:=c shr 8;
+  Result.Alpha:=c and $ff;
+  Result.Alpha:=Result.Alpha or (Result.Alpha shl 8);
+end;
+
 procedure FLLog(aType: TEventType; const Msg: string);
 begin
   TFresnelComponent.DoLog(aType,Msg);
@@ -694,7 +716,7 @@ end;
 
 function TFresnelRect.ToString: string;
 begin
-  Result:=Format('Rect[%g,%g,r=%g,b=%g]',[Left,Top,Right,Bottom]);
+  Result:=Format('[%g,%g,r=%g,b=%g]',[Left,Top,Right,Bottom]);
 end;
 
 function TFresnelRect.GetHeight: TFresnelLength;

+ 114 - 224
src/base/fresnel.controls.pas

@@ -23,7 +23,7 @@ unit Fresnel.Controls;
 interface
 
 uses
-  Classes, SysUtils, Math, fpCSSResolver, fpCSSTree,
+  Classes, SysUtils, Math, fpCSSTree, fpCSSResParser,
   fpImage, fresnel.images,
   Fresnel.Classes, Fresnel.Dom;
 
@@ -36,11 +36,9 @@ type
     class var FFresnelDivTypeID: TCSSNumericalID;
     class constructor InitFresnelDivClass;
   public
-    function GetCSSInitialAttribute(const AttrID: TCSSNumericalID): TCSSString;
-      override;
-    procedure ClearCSSValues; override;
     class function CSSTypeID: TCSSNumericalID; override;
     class function CSSTypeName: TCSSString; override;
+    class function GetCSSTypeStyle: TCSSString; override;
   end;
 
   { TSpan - span element }
@@ -50,25 +48,16 @@ type
     class var FFresnelSpanTypeID: TCSSNumericalID;
     class constructor InitFresnelSpanClass;
   public
-    function GetCSSInitialAttribute(const AttrID: TCSSNumericalID): TCSSString;
-      override;
-    procedure ClearCSSValues; override;
     class function CSSTypeID: TCSSNumericalID; override;
     class function CSSTypeName: TCSSString; override;
-  end;
-
-  { TReplacedElement - base class for elements with special content and no child elements, e.g. label, video }
-
-  TReplacedElement = class(TFresnelElement)
-  public
-    procedure GetMinMaxPreferred(out MinWidth, MinHeight, PreferredWidth, PreferredHeight, MaxWidth, MaxHeight: TFresnelLength); virtual;
-    function AcceptChildrenAtDesignTime: boolean; override;
+    class function GetCSSTypeStyle: TCSSString; override;
   end;
 
   TFresnelLabelState = (
     flsMinCaptionValid,
+    flsMaxWidthValid,
     flsMinWidthValid,
-    flsSizeValid
+    flsLastSizeValid
     );
   TFresnelLabelStates = set of TFresnelLabelState;
 
@@ -77,28 +66,23 @@ type
   TCustomLabel = class(TReplacedElement)
   private
     FCaption: TFresnelCaption;
-    FRenderedCaption: TFresnelCaption;
   protected
     FLabelStates: TFresnelLabelStates;
     FMinCaption: String; // Caption with linebreak after each word
-    FMinWidth: TFresnelLength; // width of longest word of Caption
+    FMaxWidthSize: TFresnelPoint; // size for biggest width, no extra line breaks
+    FMinWidthSize: TFresnelPoint; // size for width of longest word
     FOldFont: IFresnelFont;
-    FSize: TFresnelPoint; // width+height of Caption
+    FLastMax: TFresnelPoint;
+    FLastSize: TFresnelPoint; // result for last call with fixed max width or height
     procedure ComputeMinCaption; virtual;
     function GetFont: IFresnelFont; override;
     procedure SetCaption(const AValue: TFresnelCaption); virtual;
     procedure SetName(const NewName: TComponentName); override;
     procedure DoRender(aRenderer: IFresnelRenderer); override;
   public
-    function GetMinWidthIntrinsicContentBox: TFresnelLength; override;
-    function GetPreferredContentBox_MaxWidth(MaxWidth: TFresnelLength): TFresnelPoint;
-      override;
-    function GetCSSInitialAttribute(const AttrID: TCSSNumericalID): TCSSString;
-      override;
-    procedure ClearCSSValues; override;
-    procedure UpdateRenderedAttributes; override;
+    function GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength=NaN;
+      aMaxHeight: TFresnelLength=NaN): TFresnelPoint; override;
     property Caption: TFresnelCaption read FCaption write SetCaption;
-    property RenderedCaption: TFresnelCaption read FRenderedCaption write FRenderedCaption;
   end;
 
   { TLabel - label element }
@@ -110,6 +94,7 @@ type
   public
     class function CSSTypeID: TCSSNumericalID; override;
     class function CSSTypeName: TCSSString; override;
+    class function GetCSSTypeStyle: TCSSString; override;
   published
     property Caption;
   end;
@@ -120,13 +105,10 @@ type
   private
     class var FFresnelBodyTypeID: TCSSNumericalID;
     class constructor InitFresnelBodyClass;
-  protected
-    procedure SetCSSElAttribute(Attr: TFresnelCSSAttribute; const AValue: string); override;
   public
     class function CSSTypeID: TCSSNumericalID; override;
     class function CSSTypeName: TCSSString; override;
-    function GetCSSInitialAttribute(const AttrID: TCSSNumericalID): TCSSString; override;
-    procedure ClearCSSValues; override;
+    class function GetCSSTypeStyle: TCSSString; override;
   end;
 
 
@@ -165,6 +147,7 @@ type
   public
     class function CSSTypeID: TCSSNumericalID; override;
     class function CSSTypeName: TCSSString; override;
+    class function GetCSSTypeStyle: TCSSString; override;
   Published
     Property Caption;
     Property Icon;
@@ -174,7 +157,7 @@ type
 
   { TCustomImage }
 
-  TCustomImage = class(TFresnelElement)
+  TCustomImage = class(TReplacedElement)
   private
     FImage: TImageData;
     procedure SetImage(AValue: TImageData);
@@ -183,7 +166,8 @@ type
   Public
     constructor Create(AOwner: TComponent); override;
     destructor Destroy; override;
-    function GetCSSInitialAttribute(const AttrID: TCSSNumericalID): TCSSString; override;
+    function GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength=NaN;
+      aMaxHeight: TFresnelLength=NaN): TFresnelPoint; override;
     Property Image : TImageData Read FImage Write SetImage;
   end;
 
@@ -196,59 +180,18 @@ type
   public
     class function CSSTypeID: TCSSNumericalID; override;
     class function CSSTypeName: TCSSString; override;
+    class function GetCSSTypeStyle: TCSSString; override;
   Published
     Property Image;
   end;
 
 implementation
 
-{ TReplacedElement }
-
-procedure TReplacedElement.GetMinMaxPreferred(out MinWidth, MinHeight,
-  PreferredWidth, PreferredHeight, MaxWidth, MaxHeight: TFresnelLength);
-begin
-  MinWidth:=NaN;
-  MinHeight:=NaN;
-  PreferredWidth:=NaN;
-  PreferredHeight:=NaN;
-  MaxWidth:=NaN;
-  MaxHeight:=NaN;
-end;
-
-function TReplacedElement.AcceptChildrenAtDesignTime: boolean;
-begin
-  Result:=false;
-end;
-
 { TSpan }
 
 class constructor TSpan.InitFresnelSpanClass;
 begin
-  // register type
-  FFresnelSpanTypeID:=RegisterCSSType(CSSTypeName);
-end;
-
-function TSpan.GetCSSInitialAttribute(const AttrID: TCSSNumericalID
-  ): TCSSString;
-var
-  Attr: TFresnelCSSAttribute;
-begin
-  if (AttrID<FresnelElementBaseAttrID) or (AttrID>ord(High(TFresnelCSSAttribute))+FresnelElementBaseAttrID) then
-    exit('');
-  Attr:=TFresnelCSSAttribute(AttrID-FresnelElementBaseAttrID);
-  case Attr of
-  fcaDisplayOutside: Result:='inline';
-  fcaDisplayInside: Result:='flow';
-  else
-    Result:=inherited GetCSSInitialAttribute(AttrID);
-  end;
-end;
-
-procedure TSpan.ClearCSSValues;
-begin
-  inherited ClearCSSValues;
-  FCSSAttributes[fcaDisplayOutside]:='inline';
-  FCSSAttributes[fcaDisplayInside]:='flow';
+  FFresnelSpanTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
 end;
 
 class function TSpan.CSSTypeID: TCSSNumericalID;
@@ -261,35 +204,16 @@ begin
   Result:='span';
 end;
 
-{ TDiv }
-
-class constructor TDiv.InitFresnelDivClass;
+class function TSpan.GetCSSTypeStyle: TCSSString;
 begin
-  // register type
-  FFresnelDivTypeID:=RegisterCSSType(CSSTypeName);
+  Result:='span { display: inline flow; }';
 end;
 
-function TDiv.GetCSSInitialAttribute(const AttrID: TCSSNumericalID
-  ): TCSSString;
-var
-  Attr: TFresnelCSSAttribute;
-begin
-  if (AttrID<FresnelElementBaseAttrID) or (AttrID>ord(High(TFresnelCSSAttribute))+FresnelElementBaseAttrID) then
-    exit('');
-  Attr:=TFresnelCSSAttribute(AttrID-FresnelElementBaseAttrID);
-  case Attr of
-  fcaDisplayOutside: Result:='block';
-  fcaDisplayInside: Result:='';
-  else
-    Result:=inherited GetCSSInitialAttribute(AttrID);
-  end;
-end;
+{ TDiv }
 
-procedure TDiv.ClearCSSValues;
+class constructor TDiv.InitFresnelDivClass;
 begin
-  inherited ClearCSSValues;
-  FCSSAttributes[fcaDisplayOutside]:='block';
-  FCSSAttributes[fcaDisplayInside]:='';
+  FFresnelDivTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
 end;
 
 class function TDiv.CSSTypeID: TCSSNumericalID;
@@ -302,35 +226,16 @@ begin
   Result:='div';
 end;
 
-{ TBody }
-
-procedure TBody.SetCSSElAttribute(Attr: TFresnelCSSAttribute;
-  const AValue: string);
-begin
-  case Attr of
-  fcaDisplay,
-  fcaDisplayBox,
-  fcaDisplayInside,
-  fcaDisplayOutside,
-  fcaZIndex,
-  fcaPosition,
-  fcaLeft,
-  fcaTop,
-  fcaBottom,
-  fcaRight,
-  fcaWidth,
-  fcaHeight,
-  fcaMinWidth,
-  fcaMinHeight,
-  fcaMaxWidth,
-  fcaMaxHeight: exit;
-  end;
-  inherited SetCSSElAttribute(Attr,AValue);
+class function TDiv.GetCSSTypeStyle: TCSSString;
+begin
+  Result:='div { display: block; }';
 end;
 
+{ TBody }
+
 class constructor TBody.InitFresnelBodyClass;
 begin
-  FFresnelBodyTypeID:=RegisterCSSType(CSSTypeName);
+  FFresnelBodyTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
 end;
 
 class function TBody.CSSTypeID: TCSSNumericalID;
@@ -343,29 +248,9 @@ begin
   Result:='body';
 end;
 
-function TBody.GetCSSInitialAttribute(const AttrID: TCSSNumericalID
-  ): TCSSString;
-var
-  Attr: TFresnelCSSAttribute;
-begin
-  if (AttrID<FresnelElementBaseAttrID) or (AttrID>FresnelElementBaseAttrID+ord(High(TFresnelCSSAttribute))) then
-    exit('');
-  Attr:=TFresnelCSSAttribute(AttrID-FresnelElementBaseAttrID);
-  case Attr of
-  fcaBackgroundColor: Result:='white';
-  fcaColor: Result:='black';
-  fcaDisplayOutside: Result:='block';
-  fcaPosition: Result:='absolute';
-  else
-    Result:=inherited GetCSSInitialAttribute(AttrID);
-  end;
-end;
-
-procedure TBody.ClearCSSValues;
+class function TBody.GetCSSTypeStyle: TCSSString;
 begin
-  inherited ClearCSSValues;
-  FCSSAttributes[fcaDisplayOutside]:='block';
-  FCSSAttributes[fcaPosition]:='absolute';
+  Result:='body { background-color: white; color: black; display: block; position: static; margin: 8px; }';
 end;
 
 
@@ -430,7 +315,7 @@ end;
 
 class constructor TButton.InitFresnelButtonClass;
 begin
-  FFresnelButtonTypeID:=RegisterCSSType(CSSTypeName);
+  FFresnelButtonTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
 end;
 
 class function TButton.CSSTypeID: TCSSNumericalID;
@@ -443,6 +328,11 @@ begin
   Result:='button';
 end;
 
+class function TButton.GetCSSTypeStyle: TCSSString;
+begin
+  Result:='';
+end;
+
 { TCustomImage }
 
 procedure TCustomImage.SetImage(AValue: TImageData);
@@ -471,18 +361,24 @@ begin
   inherited Destroy;
 end;
 
-function TCustomImage.GetCSSInitialAttribute(const AttrID: TCSSNumericalID): TCSSString;
-var
-  Attr: TFresnelCSSAttribute;
-begin
-  if (AttrID<FresnelElementBaseAttrID) or (AttrID>FresnelElementBaseAttrID+ord(High(TFresnelCSSAttribute))) then
-    exit('');
-  Attr:=TFresnelCSSAttribute(AttrID-FresnelElementBaseAttrID);
-  case Attr of
-    fcaDisplayOutside: Result:='block';
-    fcaDisplayInside: Result:='';
-  else
-    Result:=inherited GetCSSInitialAttribute(AttrID);
+function TCustomImage.GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength;
+  aMaxHeight: TFresnelLength): TFresnelPoint;
+begin
+  if FImage=nil then
+    exit(Default(TFresnelPoint));
+  case aMode of
+  flmMinWidth,flmMinHeight:
+    exit(Default(TFresnelPoint));
+  flmMaxWidth,flmMaxHeight:
+    begin
+      Result.X:=FImage.Width;
+      Result.Y:=FImage.Height;
+      if (Result.X=0) or (Result.Y=0) then exit;
+      if (not IsNan(aMaxWidth)) and (Result.X>aMaxWidth) and (aMaxWidth>=0) then
+        Result.Y:=Result.Y*(aMaxWidth/Result.X);
+      if (not IsNan(aMaxHeight)) and (Result.Y>aMaxHeight) and (aMaxHeight>=0) then
+        Result.X:=Result.X*(aMaxHeight/Result.Y);
+    end;
   end;
 end;
 
@@ -490,7 +386,7 @@ end;
 
 class constructor TImage.InitFresnelImageClass;
 begin
-  FFresnelImageTypeID:=RegisterCSSType(CSSTypeName);
+  FFresnelImageTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
 end;
 
 class function TImage.CSSTypeID: TCSSNumericalID;
@@ -503,6 +399,11 @@ begin
   Result:='img';
 end;
 
+class function TImage.GetCSSTypeStyle: TCSSString;
+begin
+  Result:='image { display: block; }';
+end;
+
 { TCustomLabel }
 
 procedure TCustomLabel.ComputeMinCaption;
@@ -555,7 +456,7 @@ begin
   Result:=inherited GetFont;
   if Result<>FOldFont then
   begin
-    FLabelStates:=FLabelStates-[flsMinCaptionValid,flsMinWidthValid,flsSizeValid];
+    FLabelStates:=FLabelStates-[flsMinCaptionValid,flsMinWidthValid,flsMaxWidthValid,flsLastSizeValid];
     FOldFont:=Result;
   end;
 end;
@@ -565,7 +466,7 @@ begin
   if FCaption=AValue then Exit;
   FCaption:=AValue;
   FMinCaption:='';
-  FLabelStates:=FLabelStates-[flsMinCaptionValid,flsMinWidthValid,flsSizeValid];
+  FLabelStates:=FLabelStates-[flsMinCaptionValid,flsMinWidthValid,flsMaxWidthValid,flsLastSizeValid];
   DomChanged;
 end;
 
@@ -584,22 +485,20 @@ end;
 
 procedure TCustomLabel.DoRender(aRenderer: IFresnelRenderer);
 var
-  aColor,aCaption : string;
-  aColorFP , ShadowColor: TFPColor;
+  aCaption : string;
+  aColorFP, ShadowColor: TFPColor;
   aOffsetX, aOffsetY, aRadius: TFresnelLength;
   HaveShadow : Boolean;
 begin
-  aCaption:=RenderedCaption;
+  aCaption:=Caption;
   if aCaption='' then
     exit;
-  aColor:=GetRenderedCSString(fcaColor,true);
-  if not CSSToFPColor(aColor,aColorFP) then
-    aColorFP:=colTransparent;
+  aColorFP:=GetComputedColor(fcaColor,colTransparent);
   if aColorFP.Alpha=alphaTransparent then
     exit;
 
   // Change to loop, later
-  HaveShadow:=GetRenderedCSSTextShadow(aOffsetX, aOffsetY, aRadius, ShadowColor);
+  HaveShadow:=GetComputedTextShadow(aOffsetX, aOffsetY, aRadius, ShadowColor);
   if HaveShadow then
     aRenderer.AddTextShadow(aOffsetX,aOffsetY,ShadowColor,aRadius);
 
@@ -608,71 +507,57 @@ begin
     aRenderer.ClearTextShadows;
 end;
 
-function TCustomLabel.GetMinWidthIntrinsicContentBox: TFresnelLength;
-var
-  p: TFresnelPoint;
+function TCustomLabel.GetIntrinsicContentSize(aMode: TFresnelLayoutMode; aMaxWidth: TFresnelLength;
+  aMaxHeight: TFresnelLength): TFresnelPoint;
 begin
   GetFont;
-  if not (flsMinWidthValid in FLabelStates) then
-  begin
-    if not (flsMinCaptionValid in FLabelStates) then
-      ComputeMinCaption;
-    p:=Font.TextSize(FMinCaption);
-    FMinCaption:='';
-    Exclude(FLabelStates,flsMinCaptionValid);
-    FMinWidth:=p.X;
-    Include(FLabelStates,flsMinWidthValid);
-  end;
-  Result:=FMinWidth;
-end;
 
-function TCustomLabel.GetPreferredContentBox_MaxWidth(MaxWidth: TFresnelLength
-  ): TFresnelPoint;
-begin
-  GetFont;
-  if not (flsSizeValid in FLabelStates) then
-  begin
-    FSize:=Font.TextSize(FCaption);
-    Include(FLabelStates,flsSizeValid);
-  end;
-  if MaxWidth>=FSize.X then
-    Result:=FSize
-  else
-    Result:=Font.TextSizeMaxWidth(FCaption,MaxWidth);
-end;
+  // todo writing-mode
+  if IsNan(aMaxHeight) then ;
 
-function TCustomLabel.GetCSSInitialAttribute(const AttrID: TCSSNumericalID
-  ): TCSSString;
-var
-  Attr: TFresnelCSSAttribute;
-begin
-  if (AttrID<FresnelElementBaseAttrID) or (AttrID>ord(High(TFresnelCSSAttribute))+FresnelElementBaseAttrID) then
-    exit('');
-  Attr:=TFresnelCSSAttribute(AttrID-FresnelElementBaseAttrID);
-  case Attr of
-  fcaDisplayOutside: Result:='inline';
-  else
-    Result:=inherited GetCSSInitialAttribute(AttrID);
+  case aMode of
+  flmMinWidth:
+    begin
+      // size when using the width of the longest word
+      if not (flsMinWidthValid in FLabelStates) then
+      begin
+        if not (flsMinCaptionValid in FLabelStates) then
+          ComputeMinCaption;
+        FMinWidthSize:=Font.TextSize(FMinCaption);
+        if FMinCaption<>FCaption then
+          FMinWidthSize:=Font.TextSizeMaxWidth(FCaption,FMinWidthSize.X);
+        Include(FLabelStates,flsMinWidthValid);
+      end;
+      Result:=FMinWidthSize;
+    end;
+  flmMaxHeight,flmMaxWidth,flmMinHeight:
+    begin
+      if not (flsMaxWidthValid in FLabelStates) then
+      begin
+        FMaxWidthSize:=Font.TextSize(FCaption);
+        Include(FLabelStates,flsMaxWidthValid);
+      end;
+
+      if IsNan(aMaxWidth) or (aMaxWidth<0) or (FMaxWidthSize.X<aMaxWidth) then
+        Result:=FMaxWidthSize
+      else begin
+        if (not (flsLastSizeValid in FLabelStates)) or IsNan(FLastMax.X) then
+        begin
+          FLastMax.X:=aMaxWidth;
+          FLastMax.Y:=NaN;
+          FLastSize:=Font.TextSizeMaxWidth(FCaption,aMaxWidth);
+        end;
+        Result:=FLastSize;
+      end;
+    end;
   end;
 end;
 
-procedure TCustomLabel.ClearCSSValues;
-begin
-  inherited ClearCSSValues;
-  FCSSAttributes[fcaDisplayOutside]:='inline';
-end;
-
-procedure TCustomLabel.UpdateRenderedAttributes;
-begin
-  inherited UpdateRenderedAttributes;
-  FRenderedCaption:=Caption;
-end;
-
 { TLabel }
 
 class constructor TLabel.InitFresnelLabelClass;
 begin
-  FFresnelLabelTypeID:=RegisterCSSType(CSSTypeName);
+  FFresnelLabelTypeID:=CSSRegistry.AddType(CSSTypeName).Index;
 end;
 
 class function TLabel.CSSTypeID: TCSSNumericalID;
@@ -685,5 +570,10 @@ begin
   Result:='label';
 end;
 
+class function TLabel.GetCSSTypeStyle: TCSSString;
+begin
+  Result:='label { display: inline flow; }';
+end;
+
 end.
 

File diff suppressed because it is too large
+ 736 - 135
src/base/fresnel.dom.pas


+ 13 - 11
src/base/fresnel.events.pas

@@ -93,12 +93,15 @@ Type
   TFresnelEvent = Class(TAbstractEvent)
   private
     FDefaultPrevented: Boolean;
+    FPropagationStopped: Boolean;
   public
     Class Function FresnelEventID : TEventID; virtual; abstract;
     class function StandardEventName(aEventID: TEventID): TEventName;
     class function EventName: TEventName; override;
     Procedure PreventDefault; virtual;
     Property DefaultPrevented : Boolean Read FDefaultPrevented;
+    Procedure StopPropagation; virtual;
+    Property PropagationStopped : Boolean Read FPropagationStopped;
   end;
   TFresnelEventClass = Class of TFresnelEvent;
 
@@ -145,18 +148,19 @@ Type
 
   TFresnelMouseEvent = Class(TFresnelUIEvent)
   private
-    FInit : TFresnelMouseEventInit;
     function GetShiftKey(AIndex: Integer): Boolean;
+  protected
+    FInit : TFresnelMouseEventInit;
   Public
     Constructor Create(const aInit : TFresnelMouseEventInit); overload;
     Procedure InitEvent(const aInit : TFresnelMouseEventInit);
     Property ControlX : TFresnelLength Read FInit.ControlPos.X;
     Property ControlY : TFresnelLength Read FInit.ControlPos.Y;
-    Property PageX : TFresnelLength Read FInit.PagePos.X;
+    Property PageX : TFresnelLength Read FInit.PagePos.X; // document relative
     Property PageY : TFresnelLength Read FInit.PagePos.Y;
-    Property ScreenX : TFresnelLength Read FInit.ScreenPos.X;
+    Property ScreenX : TFresnelLength Read FInit.ScreenPos.X; // screen relative
     Property ScreenY : TFresnelLength Read FInit.ScreenPos.Y;
-    Property X : TFresnelLength Read FInit.ControlPos.X;
+    Property X : TFresnelLength Read FInit.ControlPos.X; // relative to element receiving the event
     Property Y : TFresnelLength Read FInit.ControlPos.Y;
     Property Buttons: TMouseButtons Read FInit.Buttons;
     Property Button : TMouseButton Read FInit.Button;
@@ -598,6 +602,11 @@ begin
   FDefaultPrevented:=True;
 end;
 
+procedure TFresnelEvent.StopPropagation;
+begin
+  FPropagationStopped:=true;
+end;
+
 { TFresnelMouseEventInit }
 
 function TFresnelMouseEventInit.Description: String;
@@ -654,13 +663,6 @@ class procedure TFresnelEventDispatcher.RegisterFresnelEvents;
 
 begin
   R(TFresnelChangeEvent);
-  R(TFresnelMouseClickEvent);
-  R(TFresnelMouseDoubleClickEvent);
-  R(TFresnelMouseDownEvent);
-  R(TFresnelMouseMoveEvent);
-  R(TFresnelMouseLeaveEvent);
-  R(TFresnelMouseEnterEvent);
-  R(TFresnelMouseUpEvent);
   R(TFresnelFocusEvent);
   R(TFresnelFocusInEvent);
   R(TFresnelFocusOutEvent);

+ 42 - 66
src/base/fresnel.forms.pas

@@ -20,7 +20,7 @@ unit Fresnel.Forms;
 interface
 
 uses
-  Classes, SysUtils, Math, CustApp, fpCSSResolver, fpCSSTree, Contnrs,
+  Classes, SysUtils, Math, CustApp, fpCSSTree, Contnrs,
   Fresnel.StrConsts, Fresnel.Classes, Fresnel.Resources,
   Fresnel.DOM, Fresnel.Renderer, Fresnel.Layouter, Fresnel.WidgetSet,
   Fresnel.Events, FCL.Events, Fresnel.AsyncCalls;
@@ -58,8 +58,6 @@ type
     FCaption: TFresnelCaption;
     FFormStates: TFormStates;
     FFormStyle: TFormStyle;
-    fMouseDownElement: TFresnelElement;
-    fMouseUpElement : TFresnelElement;
     FOnCreate: TNotifyEvent;
     FOnDestroy: TNotifyEvent;
     FVisible: boolean;
@@ -111,7 +109,6 @@ type
     property WSForm: TFresnelWSForm read GetWSForm write SetWSForm;
     procedure WSDraw; virtual;
     procedure WSResize(const NewFormBounds: TFresnelRect; NewWidth, NewHeight: TFresnelLength); virtual;
-    procedure WSMouseXY(WSData: TFresnelMouseEventInit; MouseEventId: TEventID); virtual;
   public
     property Caption: TFresnelCaption read GetCaption write SetCaption;
     property FormStates: TFormStates read FFormStates;
@@ -242,12 +239,17 @@ type
 
   { TFresnelBaseApplication }
 
-  TFresnelBaseApplication = class(TCustomApplication)
+  TFresnelBaseApplication = class(TCustomApplication,IFresnelVPApplication)
   private
     FAsyncCall: TAsyncCallQueues;
     FEventDispatcher: TFresnelEventDispatcher;
     FHoverElements: TFresnelElementArray;
+    fMouseDownElement: TFresnelElement;
+    fMouseDownPageXY: TFresnelPoint;
+    function GetHoverElements: TFresnelElementArray;
     procedure SetHoverElements(const AValue: TFresnelElementArray);
+    function GetMouseDownElement(out PageXY: TFresnelPoint): TFresnelElement;
+    procedure SetMouseDownElement(El: TFresnelElement; const PageXY: TFresnelPoint);
   protected
     procedure DoFresnelLog(aType: TEventType; const Msg: UTF8String); virtual;
     function GetHookFresnelLog: Boolean; virtual;
@@ -258,6 +260,7 @@ type
     procedure DoProcessMessages; virtual;
     procedure ShowMainForm; virtual;
     Property AsyncCalls: TAsyncCallQueues Read FAsyncCall;
+    procedure Notification(AComponent: TComponent; Operation: TOperation); override;
   public
     constructor Create(AOwner: TComponent); override;
     destructor Destroy; override;
@@ -483,8 +486,6 @@ begin
   inherited Notification(AComponent, Operation);
   if Operation=opRemove then
   begin
-    if fMouseDownElement=AComponent then
-      fMouseDownElement:=nil;
     if FWSForm=AComponent then
     begin
       Disconnecting;
@@ -500,7 +501,6 @@ begin
   try
     ApplyCSS;
     //Layouter.WriteLayoutTree;
-    Layouter.Apply(Self);
     //FLLog(etDebug,['TFresnelCustomForm.OnQueuedLayout ',Name,':',ClassName,' After Layouter.Apply, Invalidate...']);
     Invalidate;
   finally
@@ -549,6 +549,7 @@ begin
     raise EFresnel.Create('Fresnel.Forms.Application=nil, check if unit Fresnel is in the program uses section');
   inherited Create(AOwner);
   FVisible:=true;
+  FVPApplication:=Application;
   Layouter:=TViewportLayouter.Create(nil);
   TViewportLayouter(Layouter).Viewport:=Self;
   FormManager.AddForm(Self);
@@ -650,6 +651,11 @@ procedure TFresnelCustomForm.WSDraw;
 begin
   //FLLog(etDebug,'TFresnelCustomForm.WSDraw (%s)',[ToString]);
   //FLLog(etDebug,'TFresnelCustomForm.WSDraw Have renderer: %b',[Assigned(Renderer)]);
+  LayoutQueued:=false;
+  if DomModified then
+  begin
+    ApplyCSS;
+  end;
   Renderer.Draw(Self);
 end;
 
@@ -664,64 +670,6 @@ begin
   LayoutQueued:=true;
 end;
 
-procedure TFresnelCustomForm.WSMouseXY(WSData: TFresnelMouseEventInit;
-  MouseEventId: TEventID);
-var
-  El: TFresnelElement;
-  MouseEvt: TFresnelMouseEvent;
-  X, Y: TFresnelLength;
-  ClickEvt: TFresnelMouseClickEvent;
-  NewHoverElements: TFresnelElementArray;
-begin
-  X:=WSData.PagePos.X;
-  Y:=WSData.PagePos.Y;
-
-  NewHoverElements:=GetElementsAt(X,Y);
-  Application.HoverElements:=NewHoverElements;
-  if length(NewHoverElements)>0 then
-  begin
-    El:=NewHoverElements[0];
-    WSData.ControlPos:=PageToContentPos(El,X,Y);
-  end else begin
-    El:=Self;
-    WSData.ControlPos:=WSData.PagePos;
-  end;
-
-  FLLog(etDebug,'TFresnelCustomForm.WSMouseXY(%s) El=%s PagePos=%s ControlPos=%s',[EventDispatcher.Registry.GetEventName(MouseEventID),El.ToString, WSData.PagePos.ToString, WSData.ControlPos.ToString]);
-  case MouseEventId of
-  evtMouseDown:
-    fMouseDownElement:=El;
-  evtMouseUp:
-    fMouseUpElement:=El;
-  end;
-
-  MouseEvt:=nil;
-  ClickEvt:=nil;
-  try
-    // dispatch mouse down/move/up event
-    MouseEvt:=El.EventDispatcher.CreateEvent(El,MouseEventId) as TFresnelMouseEvent;
-    MouseEvt.InitEvent(WSData);
-    El.EventDispatcher.DispatchEvent(MouseEvt);
-    FreeAndNil(MouseEvt);
-    case MouseEventId of
-    evtMouseUp:
-      begin
-        if (fMouseDownElement=El) and not (wsClick in Widgetset.Options) then
-        begin
-          // dispatch click event
-          ClickEvt:=El.EventDispatcher.CreateEvent(El,evtClick) as TFresnelMouseClickEvent;
-          ClickEvt.InitEvent(WSData);
-          El.EventDispatcher.DispatchEvent(ClickEvt);
-        end;
-        fMouseDownElement:=nil;
-      end;
-    end;
-  finally
-    MouseEvt.Free;
-    ClickEvt.Free;
-  end;
-end;
-
 { TFresnelFormCreateEvent }
 
 class function TFresnelFormCreateEvent.FresnelEventID: TEventID;
@@ -860,6 +808,11 @@ begin
     TFresnelComponent._LogHook:=Nil;
 end;
 
+function TFresnelBaseApplication.GetHoverElements: TFresnelElementArray;
+begin
+  Result:=copy(FHoverElements);
+end;
+
 procedure TFresnelBaseApplication.SetHoverElements(
   const AValue: TFresnelElementArray);
 var
@@ -894,6 +847,19 @@ begin
   end;
 end;
 
+function TFresnelBaseApplication.GetMouseDownElement(out PageXY: TFresnelPoint): TFresnelElement;
+begin
+  Result:=fMouseDownElement;
+  PageXY:=fMouseDownPageXY;
+end;
+
+procedure TFresnelBaseApplication.SetMouseDownElement(El: TFresnelElement;
+  const PageXY: TFresnelPoint);
+begin
+  fMouseDownElement:=El;
+  fMouseDownPageXY:=PageXY;
+end;
+
 procedure TFresnelBaseApplication.DoFresnelLog(aType: TEventType; const Msg: UTF8String);
 begin
   //writeln('TFresnelBaseApplication.DoFresnelLog ',aType,' ',Msg);
@@ -939,6 +905,16 @@ begin
   FormManager.MainForm.Show;
 end;
 
+procedure TFresnelBaseApplication.Notification(AComponent: TComponent; Operation: TOperation);
+begin
+  inherited Notification(AComponent, Operation);
+  if Operation=opRemove then
+  begin
+    if fMouseDownElement=AComponent then
+      fMouseDownElement:=nil;
+  end;
+end;
+
 constructor TFresnelBaseApplication.Create(AOwner: TComponent);
 begin
   inherited Create(AOwner);

File diff suppressed because it is too large
+ 662 - 539
src/base/fresnel.layouter.pas


+ 30 - 82
src/base/fresnel.renderer.pas

@@ -74,8 +74,6 @@ type
     procedure DrawElement(El: TFresnelElement); virtual;
     // Draw the children of the element
     procedure DrawChildren(El: TFresnelElement); virtual;
-    // Call UpdateRenderedAttributes on an element and all its children.
-    procedure UpdateRenderedAttributes(El: TFresnelElement); virtual;
     // Set the origin of the currently drawn element.
     procedure SetOrigin(const AValue: TFresnelPoint); virtual;
     // Get the origin of the currently drawn element.
@@ -489,91 +487,55 @@ end;
 
 procedure TFresnelRenderer.DrawElement(El: TFresnelElement);
 var
-  LNode: TSimpleFresnelLayoutNode;
-  aBackgroundColor, aBorderColor: String;
-  aLeft, aTop, aRight, aBottom,
-    aMarginLeft, aMarginTop, aMarginRight, aMarginBottom,
-    aBorderLeft, aBorderRight, aBorderTop, aBorderBottom,
-    aPaddingLeft, aPaddingRight, aPaddingTop, aPaddingBottom: TFresnelLength;
+  LNode: TUsedLayoutNode;
   aBorderBox, aContentBox: TFresnelRect;
   BorderParams: TBorderAndBackground;
   aRenderable : IFresnelRenderable;
   s: TFresnelCSSSide;
   Corner: TFresnelCSSCorner;
-
 begin
   FLLog(etDebug,'TFresnelRenderer.DrawElement %s Origin=%s',[El.GetPath,Origin.ToString]);
-  LNode:=TSimpleFresnelLayoutNode(El.LayoutNode);
+  LNode:=TUsedLayoutNode(El.LayoutNode);
   if LNode.SkipRendering then exit;
   aRenderable:=El as IFresnelRenderable;
   aRenderable.BeforeRender;
   El.Rendered:=true;
-  aLeft:=El.GetRenderedCSSLength(fcaLeft,false);
-  aTop:=El.GetRenderedCSSLength(fcaTop,false);
-  aRight:=El.GetRenderedCSSLength(fcaRight,false);
-  aBottom:=El.GetRenderedCSSLength(fcaBottom,false);
-  FLLog(etDebug,'TFresnelRenderer.DrawElement %s [(%gx%g),(%gx%g)]',[El.GetPath,aLeft,aTop,aright,aBottom]);
-
-  aMarginLeft:=El.GetRenderedCSSLength(fcaMarginLeft,false);
-  aMarginRight:=El.GetRenderedCSSLength(fcaMarginRight,false);
-  aMarginTop:=El.GetRenderedCSSLength(fcaMarginTop,false);
-  aMarginBottom:=El.GetRenderedCSSLength(fcaMarginBottom,false);
-
-  aBorderLeft:=El.GetRenderedCSSBorderWidth(fcaBorderLeftWidth);
-  aBorderRight:=El.GetRenderedCSSBorderWidth(fcaBorderRightWidth);
-  aBorderTop:=El.GetRenderedCSSBorderWidth(fcaBorderTopWidth);
-  aBorderBottom:=El.GetRenderedCSSBorderWidth(fcaBorderBottomWidth);
-
-  aPaddingLeft:=El.GetRenderedCSSLength(fcaPaddingLeft,false);
-  aPaddingRight:=El.GetRenderedCSSLength(fcaPaddingRight,false);
-  aPaddingTop:=El.GetRenderedCSSLength(fcaPaddingTop,false);
-  aPaddingBottom:=El.GetRenderedCSSLength(fcaPaddingBottom,false);
-
-  aBorderBox.Left:=aLeft+aMarginLeft;
-  aBorderBox.Top:=aTop+aMarginTop;
-  aBorderBox.Right:=aRight-aMarginRight;
-  aBorderBox.Bottom:=aBottom-aMarginBottom;
+
+  aBorderBox:=El.UsedBorderBox;
   El.RenderedBorderBox:=aBorderBox;
 
-  aContentBox.Left:=aLeft+aMarginLeft+aBorderLeft+aPaddingLeft;
-  aContentBox.Top:=aTop+aMarginTop+aBorderTop+aPaddingTop;
-  aContentBox.Right:=aRight-aMarginRight-aBorderRight-aPaddingRight;
-  aContentBox.Bottom:=aBottom-aMarginBottom-aBorderBottom-aPaddingBottom;
+  aContentBox:=El.UsedContentBox;
   El.RenderedContentBox:=aContentBox;
 
+  FLLog(etDebug,'TFresnelRenderer.DrawElement %s %s',[El.GetPath,aBorderBox.ToString]);
+
   //writeln('TFresnelRenderer.DrawElement ',El.Name,' BorderBox=',El.RenderedBorderBox.ToString,' ContentBox=',El.RenderedContentBox.ToString);
 
   BorderParams:=CreateBorderAndBackground;
   try
-    BorderParams.BoundingBox.Box:=El.RenderedBorderBox;
+    BorderParams.BoundingBox.Box:=aBorderBox;
     if not SubPixel then
       MathRoundRect(BorderParams.BoundingBox.Box);
 
     // border-width
-    BorderParams.Width[ffsLeft]:=aBorderLeft;
-    BorderParams.Width[ffsTop]:=aBorderTop;
-    BorderParams.Width[ffsRight]:=aBorderRight;
-    BorderParams.Width[ffsBottom]:=aBorderBottom;
+    BorderParams.Width[ffsLeft]:=LNode.BorderLeft;
+    BorderParams.Width[ffsTop]:=LNode.BorderTop;
+    BorderParams.Width[ffsRight]:=LNode.BorderRight;
+    BorderParams.Width[ffsBottom]:=LNode.BorderBottom;
 
     // background-color
-    aBackgroundColor:=El.CSSRenderedAttribute[fcaBackgroundColor];
-    if not CSSToFPColor(aBackgroundColor,BorderParams.BackgroundColorFP) then
-      BorderParams.BackgroundColorFP:=colTransparent;
+    BorderParams.BackgroundColorFP:=El.GetComputedColor(fcaBackgroundColor,colTransparent);
 
     // border-color
     for s in TFresnelCSSSide do
-    begin
-      aBorderColor:=El.CSSRenderedAttribute[TFresnelCSSAttribute(ord(fcaBorderLeftColor)+ord(s))];
-      if not CSSToFPColor(aBorderColor,BorderParams.Color[s]) then
-        BorderParams.Color[s]:=colTransparent;
-    end;
+      BorderParams.Color[s]:=El.GetComputedColor(TFresnelCSSAttribute(ord(fcaBorderTopColor)+ord(s)),colTransparent);
 
     // border-image
-    BorderParams.BackgroundImage:=El.GetRenderedCSSImage(fcaBackgroundImage);
+    BorderParams.BackgroundImage:=El.GetComputedImage(fcaBackgroundImage);
 
     // border-radius
     for Corner in TFresnelCSSCorner do
-      BorderParams.BoundingBox.Radii[Corner]:=El.GetRenderedCSSBorderRadius(Corner);
+      BorderParams.BoundingBox.Radii[Corner]:=El.GetComputedBorderRadius(Corner);
     // Normalize
     if PrepareBackgroundBorder(El,BorderParams) then
       begin
@@ -586,7 +548,7 @@ begin
     BorderParams.Free;
   end;
 
-  // Give element a chance to draw itself.
+  // Give element a chance to draw itself
   aRenderable.Render(Self as IFresnelRenderer);
 
   DrawChildren(El);
@@ -597,61 +559,48 @@ end;
 procedure TFresnelRenderer.DrawChildren(El: TFresnelElement);
 var
   OldOrigin: TFresnelPoint;
-  LNode: TSimpleFresnelLayoutNode;
+  LNode: TUsedLayoutNode;
   i: Integer;
   ChildEl: TFresnelElement;
 begin
-  LNode:=TSimpleFresnelLayoutNode(El.LayoutNode);
+  LNode:=TUsedLayoutNode(El.LayoutNode);
 
   OldOrigin:=Origin;
   Origin:=OldOrigin+El.RenderedContentBox.TopLeft;
   //writeln('TFresnelRenderer.DrawChildren ',El.GetPath,' Old=',OldOrigin.ToString,' Origin=',Origin.ToString);
   for i:=0 to LNode.NodeCount-1 do
   begin
-    ChildEl:=TSimpleFresnelLayoutNode(LNode.Nodes[i]).Element;
+    ChildEl:=TUsedLayoutNode(LNode.Nodes[i]).Element;
     //writeln('TFresnelRenderer.DrawChildren ',El.GetPath,' Child=',ChildEl.GetPath);
     DrawElement(ChildEl);
   end;
   Origin:=OldOrigin;
 end;
 
-procedure TFresnelRenderer.UpdateRenderedAttributes(El: TFresnelElement);
-var
-  LNode: TSimpleFresnelLayoutNode;
-  i: Integer;
-begin
-  LNode:=TSimpleFresnelLayoutNode(El.LayoutNode);
-  if LNode.SkipRendering then exit;
-
-  El.UpdateRenderedAttributes;
-  for i:=0 to LNode.NodeCount-1 do
-    UpdateRenderedAttributes(TSimpleFresnelLayoutNode(LNode.Nodes[i]).Element);
-end;
-
 procedure TFresnelRenderer.Draw(Viewport: TFresnelViewport);
 var
   aContentBox: TFresnelRect;
-  aBackgroundColor: String;
   BackgroundColorFP: TFPColor;
+  aRenderable: IFresnelRenderable;
 begin
   //debugln(['TFresnelRenderer.Draw Origin=',dbgs(Origin)]);
-  UpdateRenderedAttributes(Viewport);
-
-  Viewport.Rendered:=true;
   aContentBox.Left:=0;
   aContentBox.Top:=0;
   aContentBox.Right:=Viewport.Width;
   aContentBox.Bottom:=Viewport.Height;
-  Viewport.RenderedBorderBox:=aContentBox;
-  Viewport.RenderedContentBox:=aContentBox;
-
-  aBackgroundColor:=Viewport.CSSRenderedAttribute[fcaBackgroundColor];
-  if not CSSToFPColor(aBackgroundColor,BackgroundColorFP) then
-    BackgroundColorFP:=colWhite;
+  Viewport.UsedBorderBox:=aContentBox;
+  Viewport.UsedContentBox:=aContentBox;
+  aRenderable:=Viewport as IFresnelRenderable;
+  aRenderable.BeforeRender;
+  Viewport.Rendered:=true;
 
+  BackgroundColorFP:=Viewport.GetComputedColor(fcaBackgroundColor,colWhite);
   FillRect(BackgroundColorFP,aContentBox);
 
+  aRenderable.Render(Self as IFresnelRenderer);
   DrawChildren(Viewport);
+
+  aRenderable.AfterRender;
 end;
 
 procedure TFresnelRenderer.ClearTextShadows;
@@ -661,7 +610,6 @@ end;
 
 { TFresnelRenderer.TBorderAndBackground }
 
-
 constructor TFresnelRenderer.TBorderAndBackground.Create(aRenderer: TFresnelRenderer);
 begin
   FRenderer:=aRenderer;

+ 37 - 20
src/base/fresnel.textlayouter.pas

@@ -4,13 +4,17 @@ unit Fresnel.TextLayouter;
 {$H+}
 {$modeswitch advancedrecords}
 
+{$IF FPC_FULLVERSION>30300}
+  {$DEFINE HasTObjectToString}
+{$ENDIF}
+
 interface
 
 uses
 {$IFDEF FPC_DOTTEDUNITS}
   System.Classes, System.SysUtils, System.Types, System.Contnrs, fpImage, System.UITypes;
 {$ELSE}
-  Classes, SysUtils, Types, Contnrs, fpImage, System.UITypes;
+  Classes, SysUtils, Fresnel.Classes, Contnrs, fpImage, System.UITypes;
 {$ENDIF}
 
 Const
@@ -37,7 +41,7 @@ Type
   TOverlappingRangesAction = (oraError,oraFit);
   TCullThreshold = 1..100;
 
-  TTextUnits = single;
+  TTextUnits = TFresnelLength;
 
   { No hyphenation:
 
@@ -77,7 +81,7 @@ Type
     Width, Height : TTextUnits;
     Ascender, Descender : TTextUnits;
   end;
-  TTextPoint = {$IFDEF FPC_DOTTEDUNITS}System.{$ENDIF}Types.TPointF;
+  TTextPoint = TFresnelPoint;
 
   TFontAttribute = (faBold,faItalic,faUnderline,faStrikeOut);
   TFontAttributes = set of TFontAttribute;
@@ -201,7 +205,9 @@ Type
     // At pos is relative to the text here, zero based
     function Split(atPos : integer) : TTextBlock; virtual;
     procedure Assign(aBlock : TTextBlock); virtual;
+    {$IFDEF HasTObjectToString}
     function ToString : RTLString; override;
+    {$ENDIF}
     Procedure TrimTrailingWhiteSpace;
     // Text for this block. Calculated from offset/len
     Property Text : TTextString Read GetText;
@@ -247,7 +253,9 @@ Type
     destructor destroy; override;
     procedure Assign(Source: TPersistent); override;
     Procedure Changed;
+    {$IFDEF HasTObjectToString}
     function ToString : RTLString; override;
+    {$ENDIF}
   Published
     // Offset is 0 based and is the offset from the first character in the text.
     Property CharOffset : SizeInt Read FCharOffset Write SetCharOffSet;
@@ -277,9 +285,9 @@ Type
     FWidth: TTextUnits;
     FLayouter : TTextLayouter;
     function GetAsPoint: TTextPoint;
-    procedure SetAsPoint(AValue: TTextPoint);
-    procedure SetHeight(AValue: TTextUnits);
-    procedure SetWidth(AValue: TTextUnits);
+    procedure SetAsPoint(const AValue: TTextPoint);
+    procedure SetHeight(const AValue: TTextUnits);
+    procedure SetWidth(const AValue: TTextUnits);
   protected
     procedure Changed; virtual;
     function GetOwner: TPersistent; override;
@@ -375,7 +383,9 @@ Type
     Procedure Reset;
     // Check if ranges do not overlap.
     procedure CheckRanges;
+    {$IFDEF HasTObjectToString}
     function ToString : RTLString; override;
+    {$ENDIF}
     Property TextBlocks[aIndex : Integer] : TTextBlock Read GetBlock;
     Property TextBlockCount : Integer Read GetBlockCount;
     function Execute : integer; virtual;
@@ -386,8 +396,8 @@ Type
     Function GetMaxRight : TTextUnits;
     Function GetMinTop : TTextUnits;
     Function GetMaxBottom : TTextUnits;
-    Function GetTotalSize : TSizeF;
-    Function GetBoundsRect : TRectF;
+    Function GetTotalSize : TFresnelPoint;
+    Function GetBoundsRect : TFresnelRect;
     // Color of font
     Property FPColor : TFPColor Read GetColor Write SetColor;
   Published
@@ -488,7 +498,7 @@ begin
   Self.TextLen:=AtPos;
   // Reset formatting stuff on new
   Result.ForceNewLine:=False;
-  Result.LayoutPos:=Default(TPointF);
+  Result.LayoutPos:=Default(TTextPoint);
   Result.Size.Width:=0;
   Result.Size.Height:=0;
   Result.Size.Descender:=0;
@@ -508,11 +518,13 @@ begin
   ForceNewLine:=aBlock.ForceNewLine;
 end;
 
+{$IFDEF HasTObjectToString}
 function TTextBlock.ToString: RTLString;
 begin
   Result:=Inherited ToString;
   Result:=Result+Format(': (x: %g, y: %g, w: %g, h:%g) [Off: %d, len: %d]: >>%s<< ',[LayoutPos.X,LayoutPos.Y,Size.Width,Size.Height,TextOffset,TextLen,Text]);
 end;
+{$ENDIF}
 
 procedure TTextBlock.TrimTrailingWhiteSpace;
 
@@ -597,10 +609,12 @@ begin
     TTextLayouter(Collection.Owner).Reset;
 end;
 
+{$IFDEF HasTObjectToString}
 function TTextRange.ToString: RTLString;
 begin
   Result:=Format('[offset %d, len: %d]',[CharOffset,CharLength]);
 end;
+{$ENDIF}
 
 { TTextRangeList }
 
@@ -625,7 +639,7 @@ end;
 
 { TTextLayoutBounds }
 
-procedure TTextLayoutBounds.SetHeight(AValue: TTextUnits);
+procedure TTextLayoutBounds.SetHeight(const AValue: TTextUnits);
 begin
   if FHeight=AValue then Exit;
   FHeight:=AValue;
@@ -634,17 +648,18 @@ end;
 
 function TTextLayoutBounds.GetAsPoint: TTextPoint;
 begin
-  Result:=PointF(Width,Height);
+  Result.X:=Width;
+  Result.Y:=Height;
 end;
 
-procedure TTextLayoutBounds.SetAsPoint(AValue: TTextPoint);
+procedure TTextLayoutBounds.SetAsPoint(const AValue: TTextPoint);
 begin
   FWidth:=aValue.X;
   FHeight:=aValue.Y;
   Changed;
 end;
 
-procedure TTextLayoutBounds.SetWidth(AValue: TTextUnits);
+procedure TTextLayoutBounds.SetWidth(const AValue: TTextUnits);
 begin
   if FWidth=AValue then Exit;
   FWidth:=AValue;
@@ -1121,6 +1136,7 @@ begin
   FBlocks.Clear;
 end;
 
+{$IFDEF HasTObjectToString}
 function TTextLayouter.ToString: RTLString;
 var
   I : Integer;
@@ -1129,6 +1145,7 @@ begin
   For I:=0 to TextBlockCount-1 do
     Result:=Result+TextBlocks[I].ToString+sLineBreak;
 end;
+{$ENDIF}
 
 function TTextLayouter.AddBlock(aOffset,aLength : SizeInt; aFont : TTextFont) : TTextBlock;
 
@@ -1216,7 +1233,6 @@ var
   SplitPos : TTextSplitPoint;
   B,BN : TTextBlock;
 
-
 begin
   I:=0;
   While (I<FBlocks.Count) do
@@ -1353,7 +1369,7 @@ var
 
 begin
   Result:=False;
-  CurrPos:=Pointf(0,0);
+  CurrPos:=Default(TTextPoint);
   I:=0;
   While I<FBlocks.Count do
     begin
@@ -1594,14 +1610,15 @@ begin
   Result:=yMax;
 end;
 
-function TTextLayouter.GetTotalSize: TSizeF;
+function TTextLayouter.GetTotalSize: TFresnelPoint;
 begin
-  Result:=TSizeF.Create(GetTotalWidth,GetTotalHeight);
+  Result.X:=GetTotalWidth;
+  Result.Y:=GetTotalHeight;
 end;
 
-function TTextLayouter.GetBoundsRect: TRectF;
+function TTextLayouter.GetBoundsRect: TFresnelRect;
 begin
-  Result:=TRectF.Create(GetMinLeft,GetMinTop,GetMaxRight,GetMaxBottom);
+  Result:=TFresnelRect.Create(GetMinLeft,GetMinTop,GetMaxRight,GetMaxBottom);
 end;
 
 procedure TTextLayouter.ApplyStretchMode(const ADesiredHeight: TTextUnits);
@@ -1638,7 +1655,7 @@ var
   i: integer;
   lBlock: TTextBlock;
   MaxHeight, vPos : TTextUnits;
-  lRemainingHeight: single;
+  lRemainingHeight: TFresnelLength;
   d: single;
   doDelete : Boolean;
   aSize : TTextMeasures;

+ 9 - 0
src/base/fresnelbase.lpk

@@ -106,6 +106,15 @@
         <Filename Value="fresnel.textlayouter.pas"/>
         <UnitName Value="Fresnel.TextLayouter"/>
       </Item>
+      <Item>
+        <Filename Value="fresnel.keys.pas"/>
+        <UnitName Value="fresnel.keys"/>
+      </Item>
+      <Item>
+        <Filename Value="fcl-css/fpcssresparser.pas"/>
+        <AddToUsesPkgSection Value="False"/>
+        <UnitName Value="fpCSSResParser"/>
+      </Item>
     </Files>
     <UsageOptions>
       <UnitPath Value="$(PkgOutDir)"/>

+ 3 - 2
src/base/fresnelbase.pas

@@ -8,8 +8,9 @@ unit FresnelBase;
 interface
 
 uses
-  Fresnel.Controls, Fresnel.DOM, Fresnel.Layouter, Fresnel.Renderer, FCL.Events, Fresnel.Events, Fresnel.Forms, Fresnel.WidgetSet, 
-  Fresnel.Resources, Fresnel.StrConsts, Fresnel.Classes, Fresnel.Images, UTF8Utils, Fresnel.AsyncCalls, Fresnel.TextLayouter;
+  Fresnel.Controls, Fresnel.DOM, Fresnel.Layouter, Fresnel.Renderer, FCL.Events, Fresnel.Events, 
+  Fresnel.Forms, Fresnel.WidgetSet, Fresnel.Resources, Fresnel.StrConsts, Fresnel.Classes, 
+  Fresnel.Images, UTF8Utils, Fresnel.AsyncCalls, Fresnel.TextLayouter, fresnel.keys;
 
 implementation
 

+ 20 - 2
src/base/utf8utils.pp

@@ -18,10 +18,20 @@ unit UTF8Utils;
 
 {$mode objfpc}{$H+}{$inline on}
 
+{$IF FPC_FULLVERSION>30300}
+{$WARN 6058 off : Call to subroutine "$1" marked as inline is not inlined}
+{$ENDIF}
+
+{$IFNDEF fpc_unicodestrings}
+  {$DEFINE UTF8_RTL}
+{$ENDIF}
 
 interface
 
 uses
+  {$IFDEF Unix}
+  cwstring,
+  {$ENDIF}
   {$ifdef windows}
   Windows,
   {$endif}
@@ -187,8 +197,6 @@ var
 
 implementation
 
-
-
 {$IFDEF WinCE}
 // CP_UTF8 is missing in the windows unit of the Windows CE RTL
 const
@@ -4052,8 +4060,18 @@ begin
     FPUpChars[c]:=UpCase(c);
 end;
 
+procedure InitUTF8;
+begin
+  {$ifdef UTF8_RTL}
+  SetMultiByteConversionCodePage(CP_UTF8);
+  // SetMultiByteFileSystemCodePage(CP_UTF8); not needed, this is the default under Windows
+  SetMultiByteRTLFileSystemCodePage(CP_UTF8);
+  {$ENDIF}
+end;
 
 initialization
   InitFPUpchars;
+  InitUTF8;
+
 end.
 

+ 3 - 0
src/gtk3/fresnel.gtk3.pas

@@ -426,7 +426,9 @@ begin
     end;
   GDK_BUTTON_PRESS,GDK_DOUBLE_BUTTON_PRESS,GDK_TRIPLE_BUTTON_PRESS:
     begin
+      Result:=true;
       EventId:=evtMouseDown;
+      //writeln('TGtk3WSForm.GtkEventMouseXY ',Event^.type_,' MButton=',MButton,' button.window=',hexstr(ptruint(Event^.button.window),16),' send_event=',Event^.button.send_event,' state=',integer(Event^.button.state),' time=',Event^.button.time);
       PressRelease;
       if MButton = GTK3_LEFT_BUTTON then
       begin
@@ -438,6 +440,7 @@ begin
     end;
   GDK_BUTTON_RELEASE:
     begin
+      Result:=true;
       EventId:=evtMouseUp;
       PressRelease;
     end;

+ 18 - 6
src/lcl/fresnel.lcl.pas

@@ -37,19 +37,21 @@ type
   public
     Engine: TFresnelLCLFontEngine;
     Family: string;
-    Kerning: string;
+    Kerning: TFresnelCSSKerning;
     Size: TFresnelLength;
     Style: string;
     Variant_: string;
     Weight: TFresnelLength;
+    Width: TFresnelLength;
     LCLFont: TFont;
     destructor Destroy; override;
     function GetFamily: string;
-    function GetKerning: string;
+    function GetKerning: TFresnelCSSKerning;
     function GetSize: TFresnelLength;
     function GetStyle: string;
     function GetVariant: string;
     function GetWeight: TFresnelLength;
+    function GetWidth: TFresnelLength;
     function TextSize(const aText: string): TFresnelPoint; virtual;
     function TextSizeMaxWidth(const aText: string; MaxWidth: TFresnelLength
       ): TFresnelPoint; virtual;
@@ -183,11 +185,13 @@ begin
   if Result<>0 then exit;
   Result:=CompareText(Font1.Style,Font2.Style);
   if Result<>0 then exit;
+  Result:=CompareText(Font1.Variant_,Font2.Variant_);
+  if Result<>0 then exit;
   Result:=CompareValue(Font1.Weight,Font2.Weight);
   if Result<>0 then exit;
-  Result:=CompareText(Font1.Variant_,Font2.Variant_);
+  Result:=CompareValue(Font1.Width,Font2.Width);
   if Result<>0 then exit;
-  Result:=CompareText(Font1.Kerning,Font2.Kerning);
+  Result:=CompareValue(ord(Font1.Kerning),ord(Font2.Kerning));
 end;
 
 function CompareFresnelFontDescWithLCLFont(Key, Item: Pointer): integer;
@@ -203,9 +207,11 @@ begin
   if Result<>0 then exit;
   Result:=CompareValue(Desc^.Weight,aFont.Weight);
   if Result<>0 then exit;
+  Result:=CompareValue(Desc^.Width,aFont.Width);
+  if Result<>0 then exit;
   Result:=CompareText(Desc^.Variant_,aFont.Variant_);
   if Result<>0 then exit;
-  Result:=CompareText(Desc^.Kerning,aFont.Kerning);
+  Result:=CompareValue(ord(Desc^.Kerning),ord(aFont.Kerning));
 end;
 
 procedure FresnelRectToRect(const Src: TFresnelRect; out Dest: TRect);
@@ -717,6 +723,7 @@ begin
   aFont.Style:=Desc.Style;
   aFont.Variant_:=Desc.Variant_;
   aFont.Weight:=Desc.Weight;
+  aFont.Width:=Desc.Width;
   FFonts.Add(aFont);
   Result:=aFont;
 end;
@@ -803,7 +810,7 @@ begin
   Result:=Family;
 end;
 
-function TFresnelLCLFont.GetKerning: string;
+function TFresnelLCLFont.GetKerning: TFresnelCSSKerning;
 begin
   Result:=Kerning;
 end;
@@ -828,6 +835,11 @@ begin
   Result:=Weight;
 end;
 
+function TFresnelLCLFont.GetWidth: TFresnelLength;
+begin
+  Result:=Width;
+end;
+
 function TFresnelLCLFont.TextSize(const aText: string): TFresnelPoint;
 var
   p: TPoint;

+ 0 - 3
src/lcl/fresnel.lclcontrols.pas

@@ -62,9 +62,6 @@ end;
 
 procedure TFresnelLCLControl.OnQueuedLayout(Data: PtrInt);
 begin
-  ViewPort.ApplyCSS;
-  //Layouter.WriteLayoutTree;
-  Layouter.Apply(ViewPort);
   Invalidate;
 end;
 

+ 6 - 8
src/lcl/fresnel.lclevents.pas

@@ -89,7 +89,7 @@ begin
   aEl:=Viewport.GetElementAt(aInit.PagePos.X,aInit.PagePos.Y);
   if aEl=Nil then
     aEl:=FViewport;
-  R:=aEL.GetBoundingClientRect;
+  R:=aEL.GetBorderBoxOnViewport;
   aInit.ControlPos.X:=aInit.PagePos.X-R.Left;
   aInit.ControlPos.Y:=aInit.PagePos.Y-R.Top;
   evt:=aEl.EventDispatcher.CreateEvent(aEl,evtClick) as TFresnelMouseEvent;
@@ -105,7 +105,7 @@ procedure TFresnelLCLEventControl.HandleMouseDown(Sender: TObject; Button: Contr
 Var
   aInit : TFresnelMouseEventInit;
   aEl : TFresnelElement;
-  evt : TFresnelMouseEvent;
+  Evt : TFresnelMouseEvent;
 
 begin
   FLLog(etDebug,CLassName+': HandleMouseDown (x: %x, y: %y)',[X,y]);
@@ -113,15 +113,13 @@ begin
   aEl:=FViewport.GetElementAt(aInit.PagePos.X,aInit.PagePos.Y);
   if aEl=Nil then
     aEl:=Self.Viewport;
-  evt:=aEl.EventDispatcher.CreateEvent(aEl,evtMouseDown) as TFresnelMouseEvent;
+  Evt:=aEl.EventDispatcher.CreateEvent(aEl,evtMouseDown) as TFresnelMouseEvent;
   try
-
-    evt.initEvent(aInit);
-    aEl.EventDispatcher.DispatchEvent(evt);
+    Evt.initEvent(aInit);
+    aEl.EventDispatcher.DispatchEvent(Evt);
   finally
-    evt.Free;
+    Evt.Free;
   end;
-
 end;
 
 procedure TFresnelLCLEventControl.DoFocus(aOld,aNew : TFresnelElement);

+ 1 - 0
src/pas2js/fresnel.pas2js.wasmapi.pp

@@ -1932,6 +1932,7 @@ var
   begin
     // The instance/timer could have disappeared
     Callback:=InstanceExports['__fresnel_timer_tick'];
+    Writeln(Format('FresnelAPi.TimerTick(%d)',[aTimerID]));
     Continue:=Assigned(Callback);
     if Continue then
       Continue:=TTimerTickCallback(CallBack)(aTimerID,userData)

+ 1 - 1
src/pas2js/p2jsfresnelapi.lpk

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <Package Version="5">
-    <Name Value="p2jsfresnelapi"/>
+    <Name Value="P2jsFresnelAPI"/>
     <Type Value="RunTimeOnly"/>
     <CompilerOptions>
       <Version Value="11"/>

+ 19 - 9
src/skia/fresnel.skiarenderer.pas

@@ -47,7 +47,7 @@ type
     Engine: TFresnelSkiaFontEngine;
     SKTypeFace: ISkTypeface;
     CSSFamily: string;
-    CSSKerning: string;
+    CSSKerning: TFresnelCSSKerning;
     CSSStyle: string;
     CSSVariant_: string;
     CSSWeight: TFresnelLength;
@@ -67,13 +67,15 @@ type
     SKFont: ISkFont;
     SKMetrics: TSkFontMetrics;
     CSSSize: TFresnelLength;
+    CSSWidth: TFresnelLength;
     destructor Destroy; override;
     function GetFamily: string;
-    function GetKerning: string;
+    function GetKerning: TFresnelCSSKerning;
     function GetSize: TFresnelLength;
     function GetStyle: string;
     function GetVariant: string;
     function GetWeight: TFresnelLength;
+    function GetWidth: TFresnelLength;
     function TextSize(const aText: string): TFresnelPoint; virtual;
     function TextSizeMaxWidth(const aText: string; MaxWidth: TFresnelLength
       ): TFresnelPoint; virtual;
@@ -130,7 +132,7 @@ type
   protected
     Type
       TSkiaBorderAndBackground = class (TBorderAndBackground)
-        Radii : TSkRoundRectRadii;
+        Radii: TSkRoundRectRadii;
         procedure CalcRadii;
       end;
   protected
@@ -185,6 +187,8 @@ begin
   Result:=CompareFresnelSkiaTypeFace(Font1.TypeFace,Font2.TypeFace);
   if Result<>0 then exit;
   Result:=CompareValue(Font1.CSSSize,Font2.CSSSize);
+  if Result<>0 then exit;
+  Result:=CompareValue(Font1.CSSWidth,Font2.CSSWidth);
 end;
 
 function CompareFresnelSkiaTypeFace(Item1, Item2: Pointer): integer;
@@ -194,7 +198,7 @@ var
 begin
   Result:=CompareText(Face1.CSSFamily,Face2.CSSFamily);
   if Result<>0 then exit;
-  Result:=CompareText(Face1.CSSKerning,Face2.CSSKerning);
+  Result:=CompareValue(ord(Face1.CSSKerning),ord(Face2.CSSKerning));
   if Result<>0 then exit;
   Result:=CompareText(Face1.CSSStyle,Face2.CSSStyle);
   if Result<>0 then exit;
@@ -220,13 +224,13 @@ var
 begin
   Result:=CompareText(Desc^.Family,Face.CSSFamily);
   if Result<>0 then exit;
-  Result:=CompareText(Desc^.Kerning,Face.CSSKerning);
+  Result:=CompareValue(ord(Desc^.Kerning),ord(Face.CSSKerning));
   if Result<>0 then exit;
   Result:=CompareText(Desc^.Style,Face.CSSStyle);
   if Result<>0 then exit;
-  Result:=CompareText(Desc^.Variant_,Face.CSSVariant_);
-  if Result<>0 then exit;
   Result:=CompareValue(Desc^.Weight,Face.CSSWeight);
+  if Result<>0 then exit;
+  Result:=CompareText(Desc^.Variant_,Face.CSSVariant_);
 end;
 
 { TFresnelSkiaTypeFace }
@@ -268,7 +272,7 @@ begin
   Result:=TypeFace.CSSFamily;
 end;
 
-function TFresnelSkiaFont.GetKerning: string;
+function TFresnelSkiaFont.GetKerning: TFresnelCSSKerning;
 begin
   Result:=Typeface.CSSKerning;
 end;
@@ -293,6 +297,11 @@ begin
   Result:=TypeFace.CSSWeight;
 end;
 
+function TFresnelSkiaFont.GetWidth: TFresnelLength;
+begin
+  Result:=CSSWidth;
+end;
+
 function TFresnelSkiaFont.TextSize(const aText: string): TFresnelPoint;
 begin
   Result:=Engine.TextSize(Self,aText);
@@ -424,9 +433,10 @@ begin
   aFont.Engine:=Self;
   aFont._AddRef;
   aFont.CSSSize:=Desc.Size;
+  aFont.CSSWidth:=Desc.Width;
   aFont.TypeFace:=aTypeFace;
   FFonts.Add(aFont);
-  aFont.SKFont := TSkFont.Create(aTypeface.SKTypeFace, Desc.Size, 1);
+  aFont.SKFont := TSkFont.Create(aTypeface.SKTypeFace, Desc.Size, Desc.Width);
   aFont.SKFont.Edging := TSkFontEdging.AntiAlias;
   aFont.SKFont.GetMetrics(aFont.SKMetrics);
 

+ 16 - 4
src/wasm/fresnel.wasm.font.pp

@@ -33,17 +33,19 @@ Type
   public
     Engine: TFresnelWasmFontEngine;
     Family: string;
-    Kerning: string;
+    Kerning: TFresnelCSSKerning;
     Size: double;
     Style: string;
     Variant_: string;
     Weight: double;
+    Width: double;
     function GetFamily: string;
-    function GetKerning: string;
+    function GetKerning: TFresnelCSSKerning;
     function GetSize: TFresnelLength;
     function GetStyle: string;
     function GetVariant: string;
     function GetWeight: TFresnelLength;
+    function GetWidth: TFresnelLength;
     function TextSize(const aText: string): TFresnelPoint; virtual;
     function TextSizeMaxWidth(const aText: string; MaxWidth: TFresnelLength): TFresnelPoint; virtual;
     function GetTool: TObject;
@@ -84,9 +86,11 @@ begin
   if Result<>0 then exit;
   Result:=CompareValue(Font1.Weight,Font2.Weight);
   if Result<>0 then exit;
+  Result:=CompareValue(Font1.Width,Font2.Width);
+  if Result<>0 then exit;
   Result:=CompareText(Font1.Variant_,Font2.Variant_);
   if Result<>0 then exit;
-  Result:=CompareText(Font1.Kerning,Font2.Kerning);
+  Result:=CompareValue(ord(Font1.Kerning),ord(Font2.Kerning));
 end;
 
 function CompareFresnelFontDescWithWasmFont(Key, Item: Pointer): integer;
@@ -106,9 +110,11 @@ begin
   if Result<>0 then exit;
   Result:=CompareValue(Desc^.Weight,aFont.Weight);
   if Result<>0 then exit;
+  Result:=CompareValue(Desc^.Width,aFont.Width);
+  if Result<>0 then exit;
   Result:=CompareText(Desc^.Variant_,aFont.Variant_);
   if Result<>0 then exit;
-  Result:=CompareText(Desc^.Kerning,aFont.Kerning);
+  Result:=CompareValue(ord(Desc^.Kerning),ord(aFont.Kerning));
 end;
 
 
@@ -145,6 +151,11 @@ begin
   Result:=Weight;
 end;
 
+function TFresnelWasmFont.GetWidth: TFresnelLength;
+begin
+  Result:=Width;
+end;
+
 function TFresnelWasmFont.TextSize(const aText: string): TFresnelPoint;
 var
   p: TPoint;
@@ -228,6 +239,7 @@ begin
   aFont.Style:=Desc.Style;
   aFont.Variant_:=Desc.Variant_;
   aFont.Weight:=Desc.Weight;
+  aFont.Width:=Desc.Width;
   FFonts.Add(aFont);
   Result:=aFont;
 end;

+ 1 - 1
src/wasm/fresnelwasm.lpk

@@ -1,7 +1,7 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <CONFIG>
   <Package Version="5">
-    <Name Value="fresnelwasm"/>
+    <Name Value="FresnelWasm"/>
     <Type Value="RunTimeOnly"/>
     <CompilerOptions>
       <Version Value="11"/>

+ 349 - 0
tests/base/TCFlexLayout.pas

@@ -0,0 +1,349 @@
+unit TCFlexLayout;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, testregistry, TCFresnelCSS, Fresnel.Controls, Fresnel.DOM;
+
+type
+
+  { TTestFlexLayout }
+
+  TTestFlexLayout = class(TCustomTestFresnelCSS)
+  published
+    procedure TestFlexLayout_Empty;
+    procedure TestFlexLayout_Empty_FlexInline;
+    procedure TestFlexLayout_Row_OneItem;
+    procedure TestFlexLayout_Row_OneItem_NoGrow;
+    procedure TestFlexLayout_Row_OneItem_Grow;
+    procedure TestFlexLayout_Row_OneItem_Shrink;
+    procedure TestFlexLayout_Row_TwoItems_Grow;
+    procedure TestFlexLayout_Row_TwoItems_JustifyCenter;
+    // todo: test flex-direction:row, flex-wrap:nowrap
+    // todo: test flex-direction:row-reverse, flex-wrap:nowrap
+    // todo: test flex-direction:row, flex-wrap:wrap
+    // todo: test flex-direction:row-reverse, flex-wrap:wrap
+    // todo: test flex-direction:row, flex-wrap:wrap-reverse
+    // todo: test flex-direction:row-reverse, flex-wrap:wrap-reverse
+    // todo: test flex-direction:column, flex-wrap:nowrap
+    // todo: test flex-direction:column-reverse, flex-wrap:nowrap
+    // todo: test flex-direction:column, flex-wrap:wrap
+    // todo: test flex-direction:column-reverse, flex-wrap:wrap
+    // todo: test flex-direction:column, flex-wrap:wrap-reverse
+    // todo: test flex-direction:column-reverse, flex-wrap:wrap-reverse
+    // todo: test child visibility:collapse
+    // todo: test child visibility:hidden
+    // todo: test child position:relative
+    // todo: test child position:absolute
+    // todo: test child position:fixed
+    // todo: test child position:sticky
+    // todo: test justify-content: left, right, start, end, flex-start, flex-end, center, space-around, space-between, space-evenly
+    // todo: test align-items: stretch, normal, left, right, start, end, flex-start, flex-end, center, baseline, first baseline, last baseline
+    // todo: test column-gap, row-gap
+    // todo: test padding-left,right,top,bottom percentage uses container's width
+    // todo: test margin-left,right,top,bottom percentage uses container's width
+  end;
+
+implementation
+
+{ TTestFlexLayout }
+
+procedure TTestFlexLayout.TestFlexLayout_Empty;
+var
+  FlexDiv: TDiv;
+begin
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Viewport;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  '#FlexDiv { width: 100px; height: 100px; display: flex; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaWidth)','100px',FlexDiv.GetComputedString(fcaWidth));
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','flex',FlexDiv.GetComputedString(fcaDisplay));
+  AssertEquals('FlexDiv.ComputedDisplayInside',CSSRegistry.Keywords[CSSRegistry.kwFlex],CSSRegistry.Keywords[FlexDiv.ComputedDisplayInside]);
+  AssertEquals('FlexDiv.ComputedDisplayOutside',CSSRegistry.Keywords[CSSRegistry.kwBlock],CSSRegistry.Keywords[FlexDiv.ComputedDisplayOutside]);
+end;
+
+procedure TTestFlexLayout.TestFlexLayout_Empty_FlexInline;
+var
+  FlexDiv: TDiv;
+begin
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Viewport;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  '#FlexDiv { width: 100px; height: 100px; display: inline flex; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaWidth)','100px',FlexDiv.GetComputedString(fcaWidth));
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','inline flex',FlexDiv.GetComputedString(fcaDisplay));
+  AssertEquals('FlexDiv.ComputedDisplayInside',CSSRegistry.Keywords[CSSRegistry.kwFlex],CSSRegistry.Keywords[FlexDiv.ComputedDisplayInside]);
+  AssertEquals('FlexDiv.ComputedDisplayOutside',CSSRegistry.Keywords[CSSRegistry.kwInline],CSSRegistry.Keywords[FlexDiv.ComputedDisplayOutside]);
+end;
+
+procedure TTestFlexLayout.TestFlexLayout_Row_OneItem;
+var
+  FlexDiv, Div1: TDiv;
+  Body: TBody;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=FlexDiv;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  'body { margin: 0; }',
+  '#FlexDiv { display: flex; }',
+  '#Div1 { width: 30px; height: 20px; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','flex',FlexDiv.GetComputedString(fcaDisplay));
+
+  AssertEquals('Div1.UsedContentBox.Left',0,Div1.UsedContentBox.Left);
+  AssertEquals('Div1.UsedContentBox.Top',0,Div1.UsedContentBox.Top);
+  AssertEquals('Div1.UsedContentBox.Width',30,Div1.UsedContentBox.Width);
+  AssertEquals('Div1.UsedContentBox.Height',20,Div1.UsedContentBox.Height);
+
+  AssertEquals('FlexDiv.UsedContentBox.Left',0,FlexDiv.UsedContentBox.Left);
+  AssertEquals('FlexDiv.UsedContentBox.Top',0,FlexDiv.UsedContentBox.Top);
+  AssertEquals('FlexDiv.UsedContentBox.Width',800,FlexDiv.UsedContentBox.Width);
+  AssertEquals('FlexDiv.UsedContentBox.Height',20,FlexDiv.UsedContentBox.Height);
+end;
+
+procedure TTestFlexLayout.TestFlexLayout_Row_OneItem_NoGrow;
+var
+  FlexDiv, Div1: TDiv;
+  Body: TBody;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=FlexDiv;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  'body { margin: 0; }',
+  '#FlexDiv { display: flex; width: 200px; }',
+  '#Div1 { width: 30px; height: 20px; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','flex',FlexDiv.GetComputedString(fcaDisplay));
+  AssertEquals('Div1.GetComputedString(fcaFlexGrow)','0',Div1.GetComputedString(fcaFlexGrow));
+
+  AssertEquals('Div1.UsedContentBox.Left',0,Div1.UsedContentBox.Left);
+  AssertEquals('Div1.UsedContentBox.Top',0,Div1.UsedContentBox.Top);
+  AssertEquals('Div1.UsedContentBox.Width',30,Div1.UsedContentBox.Width);
+  AssertEquals('Div1.UsedContentBox.Height',20,Div1.UsedContentBox.Height);
+
+  AssertEquals('FlexDiv.UsedContentBox.Left',0,FlexDiv.UsedContentBox.Left);
+  AssertEquals('FlexDiv.UsedContentBox.Top',0,FlexDiv.UsedContentBox.Top);
+  AssertEquals('FlexDiv.UsedContentBox.Width',200,FlexDiv.UsedContentBox.Width);
+  AssertEquals('FlexDiv.UsedContentBox.Height',20,FlexDiv.UsedContentBox.Height);
+end;
+
+procedure TTestFlexLayout.TestFlexLayout_Row_OneItem_Grow;
+var
+  FlexDiv, Div1: TDiv;
+  Body: TBody;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=FlexDiv;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  'body { margin: 0; }',
+  '#FlexDiv { display: flex; width: 200px; }',
+  '#Div1 { width: 30px; height: 20px; flex-grow: 1; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','flex',FlexDiv.GetComputedString(fcaDisplay));
+  AssertEquals('Div1.GetComputedString(fcaFlexGrow)','1',Div1.GetComputedString(fcaFlexGrow));
+
+  AssertEquals('Div1.UsedContentBox.Left',0,Div1.UsedContentBox.Left);
+  AssertEquals('Div1.UsedContentBox.Top',0,Div1.UsedContentBox.Top);
+  AssertEquals('Div1.UsedContentBox.Width',200,Div1.UsedContentBox.Width);
+  AssertEquals('Div1.UsedContentBox.Height',20,Div1.UsedContentBox.Height);
+
+  AssertEquals('FlexDiv.UsedContentBox.Left',0,FlexDiv.UsedContentBox.Left);
+  AssertEquals('FlexDiv.UsedContentBox.Top',0,FlexDiv.UsedContentBox.Top);
+  AssertEquals('FlexDiv.UsedContentBox.Width',200,FlexDiv.UsedContentBox.Width);
+  AssertEquals('FlexDiv.UsedContentBox.Height',20,FlexDiv.UsedContentBox.Height);
+end;
+
+procedure TTestFlexLayout.TestFlexLayout_Row_OneItem_Shrink;
+var
+  FlexDiv, Div1: TDiv;
+  Body: TBody;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=FlexDiv;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  'body { margin: 0; }',
+  '#FlexDiv { display: flex; width: 10px; }',
+  '#Div1 { width: 30px; height: 20px; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','flex',FlexDiv.GetComputedString(fcaDisplay));
+
+  AssertEquals('Div1.UsedContentBox.Left',0,Div1.UsedContentBox.Left);
+  AssertEquals('Div1.UsedContentBox.Top',0,Div1.UsedContentBox.Top);
+  AssertEquals('Div1.UsedContentBox.Width',10,Div1.UsedContentBox.Width);
+  AssertEquals('Div1.UsedContentBox.Height',20,Div1.UsedContentBox.Height);
+
+  AssertEquals('FlexDiv.UsedContentBox.Left',0,FlexDiv.UsedContentBox.Left);
+  AssertEquals('FlexDiv.UsedContentBox.Top',0,FlexDiv.UsedContentBox.Top);
+  AssertEquals('FlexDiv.UsedContentBox.Width',10,FlexDiv.UsedContentBox.Width);
+  AssertEquals('FlexDiv.UsedContentBox.Height',20,FlexDiv.UsedContentBox.Height);
+end;
+
+procedure TTestFlexLayout.TestFlexLayout_Row_TwoItems_Grow;
+var
+  FlexDiv, Div1, Div2: TDiv;
+  Body: TBody;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=FlexDiv;
+
+  Div2:=TDiv.Create(Viewport);
+  Div2.Name:='Div2';
+  Div2.Parent:=FlexDiv;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  'body { margin: 0; }',
+  '#FlexDiv { display: flex; width: 200px; }',
+  '#Div1 { width: 30px; height: 20px; flex-grow: 3; }',
+  '#Div2 { width: 70px; height: 20px; flex-grow: 2; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','flex',FlexDiv.GetComputedString(fcaDisplay));
+  AssertEquals('Div1.GetComputedString(fcaFlexGrow)','3',Div1.GetComputedString(fcaFlexGrow));
+  AssertEquals('Div2.GetComputedString(fcaFlexGrow)','2',Div2.GetComputedString(fcaFlexGrow));
+
+  AssertEquals('FlexDiv.UsedContentBox.Left',0,FlexDiv.UsedContentBox.Left);
+  AssertEquals('FlexDiv.UsedContentBox.Top',0,FlexDiv.UsedContentBox.Top);
+  AssertEquals('FlexDiv.UsedContentBox.Width',200,FlexDiv.UsedContentBox.Width);
+  AssertEquals('FlexDiv.UsedContentBox.Height',20,FlexDiv.UsedContentBox.Height);
+
+  AssertEquals('Div1.UsedContentBox.Left',0,Div1.UsedContentBox.Left);
+  AssertEquals('Div1.UsedContentBox.Top',0,Div1.UsedContentBox.Top);
+  AssertEquals('Div1.UsedContentBox.Width',90,Div1.UsedContentBox.Width);
+  AssertEquals('Div1.UsedContentBox.Height',20,Div1.UsedContentBox.Height);
+
+  AssertEquals('Div2.UsedContentBox.Left',90,Div2.UsedContentBox.Left);
+  AssertEquals('Div2.UsedContentBox.Top',0,Div2.UsedContentBox.Top);
+  AssertEquals('Div2.UsedContentBox.Width',110,Div2.UsedContentBox.Width);
+  AssertEquals('Div2.UsedContentBox.Height',20,Div2.UsedContentBox.Height);
+end;
+
+procedure TTestFlexLayout.TestFlexLayout_Row_TwoItems_JustifyCenter;
+var
+  FlexDiv, Div1, Div2: TDiv;
+  Body: TBody;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  FlexDiv:=TDiv.Create(Viewport);
+  FlexDiv.Name:='FlexDiv';
+  FlexDiv.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=FlexDiv;
+
+  Div2:=TDiv.Create(Viewport);
+  Div2.Name:='Div2';
+  Div2.Parent:=FlexDiv;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  'body { margin: 0; }',
+  '#FlexDiv { display: flex; width: 200px; justify-content: center; }',
+  '#Div1 { width: 30px; height: 20px; }',
+  '#Div2 { width: 70px; height: 20px; }'
+  ]);
+
+  Viewport.Draw;
+  AssertEquals('FlexDiv.Rendered',true,FlexDiv.Rendered);
+  AssertEquals('FlexDiv.GetComputedString(fcaDisplay)','flex',FlexDiv.GetComputedString(fcaDisplay));
+  AssertEquals('FlexDiv.GetComputedString(fcaJustifyContent)','center',FlexDiv.GetComputedString(fcaJustifyContent));
+
+  AssertEquals('FlexDiv.UsedContentBox.Left',0,FlexDiv.UsedContentBox.Left);
+  AssertEquals('FlexDiv.UsedContentBox.Top',0,FlexDiv.UsedContentBox.Top);
+  AssertEquals('FlexDiv.UsedContentBox.Width',200,FlexDiv.UsedContentBox.Width);
+  AssertEquals('FlexDiv.UsedContentBox.Height',20,FlexDiv.UsedContentBox.Height);
+
+  AssertEquals('Div1.UsedContentBox.Left',50,Div1.UsedContentBox.Left);
+  AssertEquals('Div1.UsedContentBox.Top',0,Div1.UsedContentBox.Top);
+  AssertEquals('Div1.UsedContentBox.Width',30,Div1.UsedContentBox.Width);
+  AssertEquals('Div1.UsedContentBox.Height',20,Div1.UsedContentBox.Height);
+
+  AssertEquals('Div2.UsedContentBox.Left',80,Div2.UsedContentBox.Left);
+  AssertEquals('Div2.UsedContentBox.Top',0,Div2.UsedContentBox.Top);
+  AssertEquals('Div2.UsedContentBox.Width',70,Div2.UsedContentBox.Width);
+  AssertEquals('Div2.UsedContentBox.Height',20,Div2.UsedContentBox.Height);
+end;
+
+Initialization
+  RegisterTests([TTestFlexLayout]);
+end.
+

+ 538 - 0
tests/base/TCFlowLayout.pas

@@ -0,0 +1,538 @@
+unit TCFlowLayout;
+
+{$mode ObjFPC}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, testregistry, TCFresnelCSS, Fresnel.Controls, Fresnel.DOM, Fresnel.Classes;
+
+type
+
+  { TTestFlowLayout }
+
+  TTestFlowLayout = class(TCustomTestFresnelCSS)
+  published
+    procedure TestFlowLayout_BodyDiv;
+    procedure TestFlowLayout_Slider_WithoutRangePoint;
+    procedure TestFlowLayout_SliderRangePoint;
+
+    procedure TestMarginPercentage;
+    procedure TestPaddingPercentage;
+    procedure TestPositionAbsolute_Right_WidthAuto;
+    procedure TestPositionAbsolute_DivDefaultPosBehindStatic;
+    procedure TestPositionAbsolute_DivDefaultPosBehindRelative;
+    // todo procedure TestPositionAbsolute_DivTop100Percent;
+    // todo: test break line
+  end;
+
+
+implementation
+
+{ TTestFlowLayout }
+
+procedure TTestFlowLayout.TestFlowLayout_BodyDiv;
+var
+  Body: TBody;
+  Div1: TDiv;
+  r: TFresnelRect;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+  '#Div1 { width: 20px; height: 10px; }'
+  ]);
+
+  Viewport.Draw;
+
+  // body
+  AssertEquals('Body.Rendered',true,Body.Rendered);
+  AssertEquals('Body.GetComputedString(fcaBoxSizing)','content-box',Body.GetComputedString(fcaBoxSizing));
+  AssertEquals('Body.GetComputedString(fcaDisplay)','block',Body.GetComputedString(fcaDisplay));
+  AssertEquals('Body.GetComputedString(fcaFloat)','none',Body.GetComputedString(fcaFloat));
+  AssertEquals('Body.GetComputedString(fcaLineHeight)','normal',Body.GetComputedString(fcaLineHeight));
+  AssertEquals('Body.GetComputedString(fcaPosition)','static',Body.GetComputedString(fcaPosition));
+  AssertEquals('Body.GetComputedString(fcaZIndex)','auto',Body.GetComputedString(fcaZIndex));
+  AssertEquals('Body.GetComputedString(fcaMarginLeft)','8px',Body.GetComputedString(fcaMarginLeft));
+  AssertEquals('Body.GetComputedString(fcaMarginTop)','8px',Body.GetComputedString(fcaMarginTop));
+
+  r:=Body.UsedBorderBox;
+  AssertEquals('Body.UsedBorderBox.Left',8,r.Left);
+  AssertEquals('Body.UsedBorderBox.Top',8,r.Top);
+  AssertEquals('Body.UsedBorderBox.Right',792,r.Right);
+  AssertEquals('Body.UsedBorderBox.Bottom',18,r.Bottom);
+
+  // div1
+  AssertEquals('Div1.Rendered',true,Div1.Rendered);
+  AssertEquals('Div1.GetComputedString(fcaWidth)','20px',Div1.GetComputedString(fcaWidth));
+  AssertEquals('Div1.GetComputedString(fcaHeight)','10px',Div1.GetComputedString(fcaHeight));
+
+  AssertEquals('Div1.GetComputedString(fcaBoxSizing)','content-box',Div1.GetComputedString(fcaBoxSizing));
+  AssertEquals('Div1.GetComputedString(fcaDisplay)','block',Div1.GetComputedString(fcaDisplay));
+  AssertEquals('Div1.GetComputedString(fcaFloat)','none',Div1.GetComputedString(fcaFloat));
+  AssertEquals('Div1.GetComputedString(fcaLineHeight)','normal',Div1.GetComputedString(fcaLineHeight));
+  AssertEquals('Div1.GetComputedString(fcaPosition)','static',Div1.GetComputedString(fcaPosition));
+  AssertEquals('Div1.GetComputedString(fcaZIndex)','auto',Div1.GetComputedString(fcaZIndex));
+
+  r:=Div1.UsedBorderBox;
+  AssertEquals('Div1.UsedBorderBox.Left',0,r.Left);
+  AssertEquals('Div1.UsedBorderBox.Top',0,r.Top);
+  AssertEquals('Div1.UsedBorderBox.Right',20,r.Right);
+  AssertEquals('Div1.UsedBorderBox.Bottom',10,r.Bottom);
+end;
+
+procedure TTestFlowLayout.TestFlowLayout_Slider_WithoutRangePoint;
+var
+  Body: TBody;
+  SliderDiv: TDiv;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  SliderDiv:=TDiv.Create(Viewport);
+  SliderDiv.Name:='SliderDiv';
+  SliderDiv.Parent:=Body;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    '#SliderDiv {',
+    '  margin: 7px 0 5px;',
+    '  position: relative;',
+    '  border: 1px solid #5080e0;',
+    '  height: 9px;',
+    '  width: 100%;',
+    '}']);
+
+  Viewport.Draw;
+  AssertEquals('SliderDiv.Rendered',true,SliderDiv.Rendered);
+  AssertEquals('SliderDiv.GetComputedString(fcaPosition)','relative',SliderDiv.GetComputedString(fcaPosition));
+  AssertEquals('SliderDiv.RenderedBorderBox.Left',0,SliderDiv.RenderedBorderBox.Left);
+  AssertEquals('SliderDiv.RenderedBorderBox.Top',7,SliderDiv.RenderedBorderBox.Top);
+  AssertEquals('SliderDiv.RenderedBorderBox.Right',786,SliderDiv.RenderedBorderBox.Right);
+  AssertEquals('SliderDiv.RenderedBorderBox.Bottom',18,SliderDiv.RenderedBorderBox.Bottom);
+end;
+
+procedure TTestFlowLayout.TestFlowLayout_SliderRangePoint;
+var
+  Body: TBody;
+  SliderDiv, RangeDiv, PointDiv: TDiv;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  SliderDiv:=TDiv.Create(Viewport);
+  SliderDiv.Name:='SliderDiv';
+  SliderDiv.Parent:=Body;
+
+  RangeDiv:=TDiv.Create(Viewport);
+  RangeDiv.Name:='RangeDiv';
+  RangeDiv.Parent:=SliderDiv;
+
+  PointDiv:=TDiv.Create(Viewport);
+  PointDiv.Name:='PointDiv';
+  PointDiv.Parent:=SliderDiv;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    '#SliderDiv {',
+    '  margin: 7px 0 5px;',
+    '  position: relative;',
+    '  border: 1px solid #5080e0;',
+    '  height: 9px;',
+    '  width: 100%;',
+    '}',
+    '#RangeDiv {',
+    '  display: block;',
+    '  position: absolute;',
+    '  z-index: 1;',
+    '  font-size: .7em;',
+    '  border: 0;',
+    '  background-color: #5ca0cc;',
+    '  top: 0;',
+    '  height: 100%;',
+    '  width: 50%;',
+    '}',
+    '#PointDiv {',
+    '  position: absolute;',
+    '  z-index: 2;',
+    '  width: 15px;',
+    '  height: 15px;',
+    '  border: 1px solid #385590;',
+    '  border-radius: 50%;',
+    '  background-color: #fff;',
+    '  top: -.3em;',
+    '  margin-left: -.6em;',
+    '  left: 50%;',
+    '}']);
+
+  Viewport.Draw;
+  AssertEquals('SliderDiv.Rendered',true,SliderDiv.Rendered);
+
+  // first check computed values
+  AssertEquals('SliderDiv.GetComputedString(fcaPosition)','relative',SliderDiv.GetComputedString(fcaPosition));
+  AssertEquals('SliderDiv.GetComputedString(fcaWidth)','100%',SliderDiv.GetComputedString(fcaWidth));
+  AssertEquals('SliderDiv.GetComputedString(fcaHeight)','9px',SliderDiv.GetComputedString(fcaHeight));
+  AssertEquals('SliderDiv.GetComputedString(fcaMarginTop)','7px',SliderDiv.GetComputedString(fcaMarginTop));
+  AssertEquals('SliderDiv.GetComputedString(fcaMarginRight)','0',SliderDiv.GetComputedString(fcaMarginRight));
+  AssertEquals('SliderDiv.GetComputedString(fcaMarginBottom)','5px',SliderDiv.GetComputedString(fcaMarginBottom));
+  AssertEquals('SliderDiv.GetComputedString(fcaMarginLeft)','0',SliderDiv.GetComputedString(fcaMarginLeft));
+
+  AssertEquals('RangeDiv.GetComputedString(fcaDisplay)','block',RangeDiv.GetComputedString(fcaDisplay));
+  AssertEquals('RangeDiv.GetComputedString(fcaPosition)','absolute',RangeDiv.GetComputedString(fcaPosition));
+  AssertEquals('RangeDiv.GetComputedString(fcaZIndex)','1',RangeDiv.GetComputedString(fcaZIndex));
+  AssertEquals('RangeDiv.Font.GetSize',7,RangeDiv.Font.GetSize);
+  AssertEquals('RangeDiv.GetComputedString(fcaHeight)','100%',RangeDiv.GetComputedString(fcaHeight));
+  AssertEquals('RangeDiv.GetComputedString(fcaWidth)','50%',RangeDiv.GetComputedString(fcaWidth));
+
+  AssertEquals('PointDiv.GetComputedString(fcaPosition)','absolute',PointDiv.GetComputedString(fcaPosition));
+  AssertEquals('PointDiv.GetComputedString(fcaZIndex)','2',PointDiv.GetComputedString(fcaZIndex));
+  AssertEquals('PointDiv.GetComputedString(fcaWidth)','15px',PointDiv.GetComputedString(fcaWidth));
+  AssertEquals('PointDiv.GetComputedString(fcaHeight)','15px',PointDiv.GetComputedString(fcaHeight));
+  AssertEquals('PointDiv.GetComputedString(fcaTop)','-0.3em',PointDiv.GetComputedString(fcaTop));
+  AssertEquals('PointDiv.GetComputedString(fcaLeft)','50%',PointDiv.GetComputedString(fcaLeft));
+  AssertEquals('PointDiv.GetComputedString(fcaMarginLeft)','-0.6em',PointDiv.GetComputedString(fcaMarginLeft));
+
+  // then check layout values
+  //writeln('TTestFlowLayout.TestFlowLayout_SliderRangePoint Body.RenderedBorderBox=',Body.RenderedBorderBox.ToString);
+  AssertEquals('Body.RenderedBorderBox.Left',8,Body.RenderedBorderBox.Left);
+  AssertEquals('Body.RenderedBorderBox.Top',8,Body.RenderedBorderBox.Top);
+  AssertEquals('Body.RenderedContentBox.Width',784,Body.RenderedContentBox.Width);
+  AssertEquals('Body.RenderedBorderBox.Right',792,Body.RenderedBorderBox.Right); // 800-8
+  AssertEquals('Body.RenderedBorderBox.Bottom',31,Body.RenderedBorderBox.Bottom); // 8(body margin)+Slider:7(margin)+1(border)+9(height)+1(border)+5(margin)
+
+  //writeln('TTestFlowLayout.TestFlowLayout_SliderRangePoint SliderDiv.RenderedBorderBox=',SliderDiv.RenderedBorderBox.ToString);
+  AssertEquals('SliderDiv.RenderedBorderBox.Left',0,SliderDiv.RenderedBorderBox.Left);
+  AssertEquals('SliderDiv.RenderedBorderBox.Top',7,SliderDiv.RenderedBorderBox.Top);
+  AssertEquals('SliderDiv.RenderedBorderBox.Right',786,SliderDiv.RenderedBorderBox.Right); // 784(width:100%) + 2*1(border) + 0(margin)
+  AssertEquals('SliderDiv.RenderedBorderBox.Bottom',18,SliderDiv.RenderedBorderBox.Bottom);
+  AssertEquals('SliderDiv.RenderedContentBox.Width',784,SliderDiv.RenderedContentBox.Width);
+  AssertEquals('SliderDiv.RenderedContentBox.Left',1,SliderDiv.RenderedContentBox.Left);
+  AssertEquals('SliderDiv.RenderedContentBox.Top',8,SliderDiv.RenderedContentBox.Top);
+  AssertEquals('SliderDiv.RenderedContentBox.Right',785,SliderDiv.RenderedContentBox.Right);
+  AssertEquals('SliderDiv.RenderedContentBox.Bottom',17,SliderDiv.RenderedContentBox.Bottom);
+
+  //writeln('TTestFlowLayout.TestFlowLayout_SliderRangePoint RangeDiv.RenderedBorderBox=',RangeDiv.RenderedBorderBox.ToString);
+  AssertEquals('RangeDiv.Rendered',true,RangeDiv.Rendered);
+  AssertEquals('RangeDiv.RenderedBorderBox.Left',0,RangeDiv.RenderedBorderBox.Left);
+  AssertEquals('RangeDiv.RenderedBorderBox.Top',0,RangeDiv.RenderedBorderBox.Top);
+  AssertEquals('RangeDiv.RenderedBorderBox.Right',392,RangeDiv.RenderedBorderBox.Right);
+  AssertEquals('RangeDiv.RenderedBorderBox.Bottom',9,RangeDiv.RenderedBorderBox.Bottom);
+  AssertEquals('RangeDiv.RenderedContentBox.Left',0,RangeDiv.RenderedContentBox.Left);
+  AssertEquals('RangeDiv.RenderedContentBox.Top',0,RangeDiv.RenderedContentBox.Top);
+  AssertEquals('RangeDiv.RenderedContentBox.Right',392,RangeDiv.RenderedContentBox.Right);
+  AssertEquals('RangeDiv.RenderedContentBox.Bottom',9,RangeDiv.RenderedContentBox.Bottom);
+
+  AssertEquals('PointDiv.Rendered',true,PointDiv.Rendered);
+  AssertEquals('PointDiv.LayoutNode.MarginLeft',-6,PointDiv.LayoutNode.MarginLeft);
+  AssertEquals('PointDiv.LayoutNode.Width',15,PointDiv.LayoutNode.Width);
+  AssertEquals('PointDiv.LayoutNode.Height',15,PointDiv.LayoutNode.Height);
+  AssertEquals('PointDiv.LayoutNode.Top',-3,PointDiv.LayoutNode.Top);
+  AssertEquals('PointDiv.LayoutNode.Left',392,PointDiv.LayoutNode.Left);
+  AssertEquals('PointDiv.RenderedBorderBox.Left',386,PointDiv.RenderedBorderBox.Left);
+  AssertEquals('PointDiv.RenderedBorderBox.Top',-3,PointDiv.RenderedBorderBox.Top);
+  AssertEquals('PointDiv.RenderedBorderBox.Right',403,PointDiv.RenderedBorderBox.Right);
+  AssertEquals('PointDiv.RenderedBorderBox.Bottom',14,PointDiv.RenderedBorderBox.Bottom);
+  AssertEquals('PointDiv.RenderedContentBox.Left',387,PointDiv.RenderedContentBox.Left);
+  AssertEquals('PointDiv.RenderedContentBox.Top',-2,PointDiv.RenderedContentBox.Top);
+  AssertEquals('PointDiv.RenderedContentBox.Right',402,PointDiv.RenderedContentBox.Right);
+  AssertEquals('PointDiv.RenderedContentBox.Bottom',13,PointDiv.RenderedContentBox.Bottom);
+end;
+
+procedure TTestFlowLayout.TestMarginPercentage;
+var
+  Body: TBody;
+  Div1: TDiv;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    '  margin: 0;',
+    '}',
+    'div {',
+    '  margin: 5% 10% 15% 20%;',
+    '  height: 10px;',
+    '}']);
+
+  Viewport.Draw;
+  AssertEquals('Body.RenderedContentBox.Width',800,Body.RenderedContentBox.Width);
+
+  AssertEquals('Div1.Rendered',true,Div1.Rendered);
+
+  //writeln('TTestFlowLayout.TestPaddingPercentage ',Div1.RenderedBorderBox.ToString);
+  AssertEquals('Div1.LayoutNode.MarginLeft',160,Div1.LayoutNode.MarginLeft);
+  AssertEquals('Div1.LayoutNode.MarginTop',40,Div1.LayoutNode.MarginTop);
+  AssertEquals('Div1.LayoutNode.MarginRight',80,Div1.LayoutNode.MarginRight);
+  AssertEquals('Div1.LayoutNode.MarginBottom',120,Div1.LayoutNode.MarginBottom);
+  AssertEquals('Div1.RenderedBorderBox.Left',160,Div1.RenderedBorderBox.Left);
+  AssertEquals('Div1.RenderedBorderBox.Top',40,Div1.RenderedBorderBox.Top);
+  AssertEquals('Div1.RenderedBorderBox.Right',720,Div1.RenderedBorderBox.Right);
+  AssertEquals('Div1.RenderedBorderBox.Bottom',50,Div1.RenderedBorderBox.Bottom);
+end;
+
+procedure TTestFlowLayout.TestPaddingPercentage;
+var
+  Body: TBody;
+  Div1: TDiv;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    '  margin: 0;',
+    '}',
+    'div {',
+    '  padding: 5% 10% 15% 20%;',
+    '  height: 10px;',
+    '}']);
+
+  Viewport.Draw;
+  AssertEquals('Body.RenderedContentBox.Width',800,Body.RenderedContentBox.Width);
+
+  AssertEquals('Div1.Rendered',true,Div1.Rendered);
+
+  //writeln('TTestFlowLayout.TestPaddingPercentage ',Div1.RenderedBorderBox.ToString);
+  AssertEquals('Div1.LayoutNode.PaddingLeft',160,Div1.LayoutNode.PaddingLeft);
+  AssertEquals('Div1.LayoutNode.PaddingTop',40,Div1.LayoutNode.PaddingTop);
+  AssertEquals('Div1.LayoutNode.PaddingRight',80,Div1.LayoutNode.PaddingRight);
+  AssertEquals('Div1.LayoutNode.PaddingBottom',120,Div1.LayoutNode.PaddingBottom);
+  AssertEquals('Div1.RenderedContentBox.Left',160,Div1.RenderedContentBox.Left);
+  AssertEquals('Div1.RenderedContentBox.Top',40,Div1.RenderedContentBox.Top);
+  AssertEquals('Div1.RenderedContentBox.Right',720,Div1.RenderedContentBox.Right);
+  AssertEquals('Div1.RenderedContentBox.Bottom',50,Div1.RenderedContentBox.Bottom);
+end;
+
+procedure TTestFlowLayout.TestPositionAbsolute_Right_WidthAuto;
+var
+  Body: TBody;
+  Div1, Div2, Div3: TDiv;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Div2:=TDiv.Create(Viewport);
+  Div2.Name:='Div2';
+  Div2.Parent:=Div1;
+
+  Div3:=TDiv.Create(Viewport);
+  Div3.Name:='Div3';
+  Div3.Parent:=Div2;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    '  margin: 0;', // content width = 800
+    '}',
+    '#Div1 {',
+    '  position: absolute;',
+    '  height: 120px;', // content height = 120
+    '  right: 30px;',
+    // content-width is on top-down run 0, and on second run max-content, which is width of Div3, so 100px
+    '}',
+    '#Div2 {',
+    '  margin: 10%;', // 10% of 0 on top-down run!
+    '  padding: 10%;', // 10% of 0 on top-down run!
+    '  box-sizing: border-box;',
+    '  width: 80%;', // 30% of 360 = 108, content width = 108 - 2*padding = 88
+    '  height: 60%;', // same as width 30%, content height = 88
+    '}',
+    '#Div3 {',
+    '  width: 100px;',
+    '  height: 30px;',
+    '}']);
+
+  Viewport.Draw;
+  AssertEquals('Body.RenderedContentBox.Width',800,Body.RenderedContentBox.Width);
+
+  AssertEquals('Div3.Rendered',true,Div3.Rendered);
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_Right_WidthAuto Div3.RenderedBorderBox=',Div3.RenderedBorderBox.ToString);
+  AssertEquals('Div3.LayoutNode.Width',100,Div3.LayoutNode.Width);
+  AssertEquals('Div3.LayoutNode.Height',30,Div3.LayoutNode.Height);
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_Right_WidthAuto Div1.RenderedBorderBox=',Div1.RenderedBorderBox.ToString);
+  AssertEquals('Div1.GetComputedString(fcaPosition)','absolute',Div1.GetComputedString(fcaPosition));
+  AssertEquals('Div1.GetComputedString(fcaBoxSizing)','content-box',Div1.GetComputedString(fcaBoxSizing));
+  AssertEquals('Div1.GetComputedString(fcaWidth)','auto',Div1.GetComputedString(fcaWidth));
+  AssertEquals('Div1.LayoutNode.Height',120,Div1.LayoutNode.Height);
+  AssertEquals('Div1.LayoutNode.Width',100,Div1.LayoutNode.Width);
+  AssertEquals('Div1.LayoutNode.Right',30,Div1.LayoutNode.Right);
+  AssertEquals('Div1.LayoutNode.Left',670,Div1.LayoutNode.Left); // 800-30-100 = 670
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_Right_WidthAuto Div2.RenderedBorderBox=',Div2.RenderedBorderBox.ToString);
+  AssertEquals('Div2.GetComputedString(fcaPosition)','static',Div2.GetComputedString(fcaPosition));
+  AssertEquals('Div2.GetComputedString(fcaBoxSizing)','border-box',Div2.GetComputedString(fcaBoxSizing));
+  AssertEquals('Div2.GetComputedString(fcaWidth)','80%',Div2.GetComputedString(fcaWidth));
+  AssertEquals('Div2.GetComputedString(fcaHeight)','60%',Div2.GetComputedString(fcaHeight));
+  AssertEquals('Div2.GetComputedString(fcaMarginLeft)','10%',Div2.GetComputedString(fcaMarginLeft));
+  AssertEquals('Div2.GetComputedString(fcaMarginTop)','10%',Div2.GetComputedString(fcaMarginTop));
+  AssertEquals('Div2.GetComputedString(fcaMarginRight)','10%',Div2.GetComputedString(fcaMarginRight));
+  AssertEquals('Div2.GetComputedString(fcaMarginBottom)','10%',Div2.GetComputedString(fcaMarginBottom));
+  AssertEquals('Div2.GetComputedString(fcaPaddingLeft)','10%',Div2.GetComputedString(fcaPaddingLeft));
+  AssertEquals('Div2.GetComputedString(fcaPaddingTop)','10%',Div2.GetComputedString(fcaPaddingTop));
+  AssertEquals('Div2.GetComputedString(fcaPaddingRight)','10%',Div2.GetComputedString(fcaPaddingRight));
+  AssertEquals('Div2.GetComputedString(fcaPaddingBottom)','10%',Div2.GetComputedString(fcaPaddingBottom));
+  AssertEquals('Div2.LayoutNode.MarginLeft',10,Div2.LayoutNode.MarginLeft); // 10% of Div2.width 100
+  AssertEquals('Div2.LayoutNode.MarginRight',10,Div2.LayoutNode.MarginRight);
+  AssertEquals('Div2.LayoutNode.MarginTop',10,Div2.LayoutNode.MarginTop); // 10% of Div2.width 100
+  AssertEquals('Div2.LayoutNode.MarginBottom',10,Div2.LayoutNode.MarginBottom);
+  AssertEquals('Div2.LayoutNode.PaddingLeft',10,Div2.LayoutNode.PaddingLeft); // 10% of Div2.width 100
+  AssertEquals('Div2.LayoutNode.PaddingRight',10,Div2.LayoutNode.PaddingRight);
+  AssertEquals('Div2.LayoutNode.PaddingTop',10,Div2.LayoutNode.PaddingTop); // 10% of Div2.width 100
+  AssertEquals('Div2.LayoutNode.PaddingBottom',10,Div2.LayoutNode.PaddingBottom);
+  AssertEquals('Div2.LayoutNode.Width',60,Div2.LayoutNode.Width);
+  AssertEquals('Div2.LayoutNode.Height',52,Div2.LayoutNode.Height);
+end;
+
+procedure TTestFlowLayout.TestPositionAbsolute_DivDefaultPosBehindStatic;
+var
+  Body: TBody;
+  Div1: TDiv;
+  Label1, Label2: TLabel;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Label1:=TLabel.Create(Viewport);
+  Label1.Name:='Label1';
+  Label1.Caption:='Label1';
+  Label1.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Label2:=TLabel.Create(Viewport);
+  Label2.Name:='Label2';
+  Label2.Caption:='Label2';
+  Label2.Parent:=Body;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    '  margin: 0;', // content width = 800
+    '  font-size: 20px;',
+    '}',
+    '#Div1 {',
+    '  position: absolute;',
+    '  width: 30px; height: 20px;',
+    '}']);
+
+  // Div1 is absolute to the Viewport, because Body position is static
+  // Div1 default position is below Label1, because Div1 display is block
+  // Label2 is right behind Label1
+  // Body width/height includes Label1 and Label2, but not Div1
+
+  Viewport.Draw;
+  AssertEquals('Body.RenderedContentBox.Width',800,Body.RenderedContentBox.Width);
+
+  AssertEquals('Div1.Rendered',true,Div1.Rendered);
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_DivDefaultPos Label1.RenderedBorderBox=',Label1.RenderedBorderBox.ToString);
+  AssertEquals('Label1.GetComputedString(fcaPosition)','static',Label1.GetComputedString(fcaPosition));
+  AssertEquals('Label1.GetComputedString(fcaDisplay)','inline flow',Label1.GetComputedString(fcaDisplay));
+  AssertEquals('Label1.GetComputedString(fcaWidth)','auto',Label1.GetComputedString(fcaWidth));
+  AssertEquals('Label1.RenderedBorderBox.Top',0,Label1.RenderedBorderBox.Left);
+  AssertEquals('Label1.RenderedBorderBox.Left',0,Label1.RenderedBorderBox.Left);
+  AssertEquals('Label1.RenderedBorderBox.Height',23,Label1.RenderedBorderBox.Height);
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_DivDefaultPos Label2.RenderedBorderBox=',Label2.RenderedBorderBox.ToString);
+  AssertEquals('Label2.GetComputedString(fcaPosition)','static',Label2.GetComputedString(fcaPosition));
+  AssertEquals('Label2.GetComputedString(fcaDisplay)','inline flow',Label2.GetComputedString(fcaDisplay));
+  AssertEquals('Label2.GetComputedString(fcaWidth)','auto',Label2.GetComputedString(fcaWidth));
+  AssertEquals('Label2.RenderedBorderBox.Height',23,Label2.RenderedBorderBox.Height);
+  AssertEquals('Label2.RenderedBorderBox.Top',0,Label2.RenderedBorderBox.Top);
+  AssertEquals('Label2.RenderedBorderBox.Left',Label1.RenderedBorderBox.Right,Label2.RenderedBorderBox.Left);
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_DivDefaultPos Div1.RenderedBorderBox=',Div1.RenderedBorderBox.ToString);
+  AssertEquals('Div1.RenderedBorderBox.Width',30,Div1.RenderedBorderBox.Width);
+  AssertEquals('Div1.RenderedBorderBox.Height',20,Div1.RenderedBorderBox.Height);
+  AssertEquals('Div1.RenderedBorderBox.Left',0,Div1.RenderedBorderBox.Left);
+  AssertEquals('Div1.RenderedBorderBox.Top',23,Div1.RenderedBorderBox.Top);
+end;
+
+procedure TTestFlowLayout.TestPositionAbsolute_DivDefaultPosBehindRelative;
+var
+  Body: TBody;
+  Div1: TDiv;
+  Label1: TLabel;
+begin
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Label1:=TLabel.Create(Viewport);
+  Label1.Name:='Label1';
+  Label1.Caption:='Label1';
+  Label1.Parent:=Body;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    '  margin: 0;', // content width = 800
+    '  font-size: 20px;',
+    '}',
+    '#Label1 {',
+    '  position: relative; top:10px;',
+    '}',
+    '#Div1 {',
+    '  position: absolute;',
+    '  width: 30px; height: 20px;',
+    '}']);
+
+  // Div1 is absolute to the Viewport, because Body position is static
+  // Div1 default position is below Label1 static position, because Div1 display is block
+  // Body width/height includes Label1 static bounds, and not Div1
+
+  Viewport.Draw;
+  AssertEquals('Body.RenderedContentBox.Width',800,Body.RenderedContentBox.Width);
+
+  AssertEquals('Div1.Rendered',true,Div1.Rendered);
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_DivDefaultPos Label1.RenderedBorderBox=',Label1.RenderedBorderBox.ToString);
+  AssertEquals('Label1.GetComputedString(fcaPosition)','relative',Label1.GetComputedString(fcaPosition));
+  AssertEquals('Label1.GetComputedString(fcaDisplay)','inline flow',Label1.GetComputedString(fcaDisplay));
+  AssertEquals('Label1.GetComputedString(fcaWidth)','auto',Label1.GetComputedString(fcaWidth));
+  AssertEquals('Label1.GetComputedString(fcaTop)','10px',Label1.GetComputedString(fcaTop));
+  AssertEquals('Label1.RenderedBorderBox.Top',10,Label1.RenderedBorderBox.Top);
+  AssertEquals('Label1.RenderedBorderBox.Left',0,Label1.RenderedBorderBox.Left);
+  AssertEquals('Label1.RenderedBorderBox.Height',23,Label1.RenderedBorderBox.Height);
+
+  AssertEquals('Body.RenderedBorderBox.Height',23,Body.RenderedBorderBox.Height);
+
+  //writeln('TTestFlowLayout.TestPositionAbsolute_DivDefaultPos Div1.RenderedBorderBox=',Div1.RenderedBorderBox.ToString);
+  AssertEquals('Div1.RenderedBorderBox.Width',30,Div1.RenderedBorderBox.Width);
+  AssertEquals('Div1.RenderedBorderBox.Height',20,Div1.RenderedBorderBox.Height);
+  AssertEquals('Div1.RenderedBorderBox.Left',0,Div1.RenderedBorderBox.Left);
+  AssertEquals('Div1.RenderedBorderBox.Top',23,Div1.RenderedBorderBox.Top);
+end;
+
+Initialization
+  RegisterTests([TTestFlowLayout]);
+end.
+

+ 52 - 4
tests/base/TCFresnelBaseEvents.pas

@@ -13,8 +13,10 @@ unit TCFresnelBaseEvents;
 
 {$mode objfpc}
 {$H+}
+{$IF FPC_FULLVERSION>30300}
 {$modeswitch functionreferences}
 {$modeswitch nestedprocvars}
+{$ENDIF}
 
 interface
 
@@ -113,13 +115,17 @@ type
       aEvent: TAbstractEvent; aIndex, aCount: Integer);
   private
     FDispatcher: TEventDispatcher;
+    {$IFDEF HasFunctionReferences}
     FRHandler:TEventHandlerRef;
     FRHandler2:TEventHandlerRef;
+    {$ENDIF}
     FEvents : Array[1..3] of TAbstractEvent;
     FSecondEvent : TAbstractEvent;
 
     Procedure RegisterEvent2P;
+    {$IFDEF HasFunctionReferences}
     Procedure RegisterEvent2R;
+    {$ENDIF}
     Procedure RegisterEvent2O;
   protected
     Procedure SetUp; override;
@@ -131,41 +137,61 @@ type
     function RegisterHandlerO2(aEventName: String): TEventHandlerItem;
     function RegisterHandlerP(aEventName: String): TEventHandlerItem;
     function RegisterHandlerP2(aEventName: String): TEventHandlerItem;
+    {$IFDEF HasFunctionReferences}
     function RegisterHandlerR(aEventName: String): TEventHandlerItem;
     function RegisterHandlerR2(aEventName: String): TEventHandlerItem;
+    {$ENDIF}
     Property Dispatcher : TEventDispatcher Read FDispatcher;
   Published
     Procedure TestHookup;
     Procedure TestRegisterHandlerO;
+    {$IFDEF HasFunctionReferences}
     Procedure TestRegisterHandlerR;
+    {$ENDIF}
     Procedure TestRegisterHandlerP;
     Procedure TestRegisterHandlerOUnknown;
+    {$IFDEF HasFunctionReferences}
     Procedure TestRegisterHandlerRUnknown;
+    {$ENDIF}
     Procedure TestRegisterHandlerPUnknown;
     Procedure TestUnRegisterHandlerO;
+    {$IFDEF HasFunctionReferences}
     Procedure TestUnRegisterHandlerR;
+    {$ENDIF}
     Procedure TestUnRegisterHandlerP;
     Procedure TestUnRegisterHandlerOName;
+    {$IFDEF HasFunctionReferences}
     Procedure TestUnRegisterHandlerRName;
+    {$ENDIF}
     Procedure TestUnRegisterHandlerPName;
     Procedure TestUnRegisterHandlerOUnknownEvent;
+    {$IFDEF HasFunctionReferences}
     Procedure TestUnRegisterHandlerRUnknownEvent;
+    {$ENDIF}
     Procedure TestUnRegisterHandlerPUnknownEvent;
     Procedure TestUnRegisterHandlerOUnknownHandler;
+    {$IFDEF HasFunctionReferences}
     Procedure TestUnRegisterHandlerRUnknownHandler;
+    {$ENDIF}
     Procedure TestUnRegisterHandlerPUnknownHandler;
     Procedure TestUnRegisterHandlerOAllName;
+    {$IFDEF HasFunctionReferences}
     Procedure TestUnRegisterHandlerRAllName;
+    {$ENDIF}
     Procedure TestUnRegisterHandlerPAllName;
     Procedure TestUnRegisterHandlerMixedAllName;
     Procedure TestUnRegisterHandlerOAllHandler;
+    {$IFDEF HasFunctionReferences}
     Procedure TestUnRegisterHandlerRAllHandler;
+    {$ENDIF}
     Procedure TestUnRegisterHandlerPAllHandler;
     Procedure TestCreateEventByName;
     Procedure TestCreateEventByID;
     Procedure TestDispatchEvent;
     Procedure TestDispatchEventProc;
+    {$IFDEF HasFunctionReferences}
     Procedure TestDispatchEventRef;
+    {$ENDIF}
     Procedure TestDispatchEvent2Handlers;
     Procedure TestDispatchEvent2MixedHandlers;
     Procedure TestDispatchEventInEvent;
@@ -183,7 +209,9 @@ var
 
 begin
   inherited SetUp;
+  {$IFDEF HasFunctionReferences}
   FRHandler:=Nil;
+  {$ENDIF}
   FDispatcher:=TEventDispatcher.Create(Self);
   FDispatcher.Registry:=Self.Registry;
   For H in THandlerType do
@@ -199,7 +227,9 @@ procedure TCEventsDispatcher.TearDown;
 var
   I : Integer;
 begin
+  {$IFDEF HasFunctionReferences}
   FRHandler:=Nil;
+  {$ENDIF}
   FreeAndNil(FDispatcher);
   for I:=1 to 3 do
     FreeAndNil(FEvents[i]);
@@ -238,10 +268,12 @@ begin
   RegisterHandlerP('event2');
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.RegisterEvent2R;
 begin
   RegisterHandlerR('event2');
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.RegisterEvent2O;
 begin
@@ -297,6 +329,7 @@ begin
   Result:=Dispatcher.RegisterHandler(@EventHandlerP2,aEventName);
 end;
 
+{$IFDEF HasFunctionReferences}
 function TCEventsDispatcher.RegisterHandlerR(aEventName: String) : TEventHandlerItem;
 
   Procedure EventHandlerR(aEvent : TAbstractEVent);
@@ -323,6 +356,7 @@ begin
   FRHandler2:=@EventHandlerR2;
   Result:=Dispatcher.RegisterHandler(FRHandler,aEventName);
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestHookup;
 begin
@@ -344,6 +378,7 @@ begin
   AssertEquals('Count',1,Dispatcher.Count);
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestRegisterHandlerR;
 Var
   Itm : TEventHandlerItem;
@@ -356,6 +391,7 @@ begin
   AssertEquals('Event name','event1',Itm.EventName);
   AssertEquals('Count',1,Dispatcher.Count);
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestRegisterHandlerP;
 Var
@@ -376,11 +412,13 @@ begin
   AssertException('Not known',EEvents,@RegisterEvent2O,'Unknown event name: event2');
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestRegisterHandlerRUnknown;
 begin
   Register1;
   AssertException('Not known',EEvents,@RegisterEvent2R,'Unknown event name: event2');
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestRegisterHandlerPUnknown;
 begin
@@ -402,6 +440,7 @@ begin
   AssertEquals('Dispatcher count',0,Dispatcher.Count);
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestUnRegisterHandlerR;
 Var
   Itm : TEventHandlerItem;
@@ -414,6 +453,7 @@ begin
   Dispatcher.UnregisterHandler(Itm);
   AssertEquals('Dispatcher count',0,Dispatcher.Count);
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestUnRegisterHandlerP;
 
@@ -444,6 +484,7 @@ begin
   AssertEquals('Dispatcher count',0,Dispatcher.Count);
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestUnRegisterHandlerRName;
 Var
   Itm : TEventHandlerItem;
@@ -455,8 +496,8 @@ begin
   AssertEquals('Dispatcher count',1,Dispatcher.Count);
   Dispatcher.UnregisterHandler(FRHandler,'event1');
   AssertEquals('Dispatcher count',0,Dispatcher.Count);
-
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestUnRegisterHandlerPName;
 
@@ -487,6 +528,7 @@ begin
   AssertEquals('Dispatcher count',1,Dispatcher.Count);
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestUnRegisterHandlerRUnknownEvent;
 Var
   Itm : TEventHandlerItem;
@@ -501,10 +543,9 @@ begin
   Dispatcher.UnregisterHandler(FRHandler,'event2');
   AssertEquals('Dispatcher count',1,Dispatcher.Count);
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestUnRegisterHandlerPUnknownEvent;
-
-
 begin
   Register1;
   Register2;
@@ -526,6 +567,7 @@ begin
   AssertEquals('Dispatcher count after',2,Dispatcher.Count);
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestUnRegisterHandlerRUnknownHandler;
 
 begin
@@ -537,6 +579,7 @@ begin
   Dispatcher.UnregisterHandler(FRHandler2,'event1');
   AssertEquals('Dispatcher count after',2,Dispatcher.Count);
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestUnRegisterHandlerPUnknownHandler;
 Var
@@ -568,6 +611,7 @@ begin
   AssertEquals('Dispatcher count after',0,Dispatcher.Count);
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestUnRegisterHandlerRAllName;
 begin
   Register1;
@@ -577,8 +621,8 @@ begin
   AssertEquals('Dispatcher count before',2,Dispatcher.Count);
   Dispatcher.UnregisterHandler('event1');
   AssertEquals('Dispatcher count after',0,Dispatcher.Count);
-
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestUnRegisterHandlerPAllName;
 begin
@@ -614,6 +658,7 @@ begin
 
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestUnRegisterHandlerRAllHandler;
 begin
   Register1;
@@ -624,6 +669,7 @@ begin
   Dispatcher.UnregisterHandler(FRHandler);
   AssertEquals('Dispatcher count after',0,Dispatcher.Count);
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestUnRegisterHandlerPAllHandler;
 begin
@@ -695,6 +741,7 @@ begin
   AssertCalled('Event handler called',htProc,Evt,1,1);
 end;
 
+{$IFDEF HasFunctionReferences}
 procedure TCEventsDispatcher.TestDispatchEventRef;
 
 Var
@@ -714,6 +761,7 @@ begin
   AssertCalled('Event handler called',htRef,Evt,1,1);
 
 end;
+{$ENDIF}
 
 procedure TCEventsDispatcher.TestDispatchEvent2Handlers;
 Var

+ 607 - 15
tests/base/TCFresnelCSS.pas

@@ -29,15 +29,15 @@ type
     Desc: TFresnelFontDesc;
     function GetDescription: String;
     function GetFamily: string;
-    function GetKerning: string;
+    function GetKerning: TFresnelCSSKerning;
     function GetSize: double;
     function GetStyle: string;
     function GetVariant: string;
+    function GetWidth: double;
     function GetWeight: double;
     function TextSize(const aText: string): TFresnelPoint;
     function TextSizeMaxWidth(const aText: string; MaxWidth: TFresnelLength): TFresnelPoint;
     function GetTool: TObject;
-
   end;
 
   { TTestFontEngine }
@@ -56,15 +56,17 @@ type
   { TTestRenderer }
 
   TTestRenderer = class(TFresnelRenderer)
-  private
-  protected
+  public
+    procedure Arc(const aColor: TFPColor; const aCenter, aRadii: TFresnelPoint;
+      aStartAngle: TFresnelLength=0; aStopAngle: TFresnelLength=DoublePi); override;
     procedure FillRect(const aColor: TFPColor; const aRect: TFresnelRect); override;
     procedure Line(const aColor: TFPColor; const x1, y1, x2, y2: TFresnelLength); override;
+    procedure RoundRect(const aColor: TFPColor; const aRect: TFresnelRoundRect; Fill: Boolean);
+      override;
     procedure TextOut(const aLeft, aTop: TFresnelLength;
       const aFont: IFresnelFont; const aColor: TFPColor;
       const aText: string); override;
     procedure DrawImage(const aLeft, aTop, aWidth, aHeight: TFresnelLength; const aImage: TFPCustomImage); override;
-  public
     constructor Create(AOwner: TComponent); override;
   end;
 
@@ -102,12 +104,55 @@ type
   published
     procedure TestEmptyViewport;
     procedure TestBody;
+
+    procedure TestGetStyleAttr_OneValue;
+    procedure TestGetStyleAttr_TwoValues;
+    procedure TestGetStyleAttr_OneFunction;
+    procedure TestGetStyleAttr_TwoFunctions;
+    procedure TestGetStyleAttr_NestedFunctions;
+    procedure TestSetStyleAttr_NewValueEmpty;
+    procedure TestSetStyleAttr_NewValueFirst;
+    procedure TestSetStyleAttr_NewValueAppend;
+    procedure TestSetStyleAttr_NewValueAppendSemicolon;
+    procedure TestSetStyleAttr_DeleteOnlyValue;
+    procedure TestSetStyleAttr_DeleteFirstValue;
+    procedure TestSetStyleAttr_DeleteLastValue;
+    procedure TestSetStyleAttr_DeleteMiddleValue;
+    procedure TestSetStyleAttr_ReplaceOnlyValue;
+    procedure TestSetStyleAttr_ReplaceFirstValue;
+    procedure TestSetStyleAttr_ReplaceLastValue;
+    procedure TestSetStyleAttr_ReplaceMiddleValue;
+
+    procedure Test_FontSize_Percentage;
+    procedure Test_FontSize_AsString;
+    procedure Test_Font_AsString;
+    procedure Test_Overflow_AsString;
+    procedure Test_BorderColor_AsString;
+    procedure Test_BorderStyle_AsString;
+    procedure Test_BorderWidth_AsString;
+    procedure Test_Border_AsString;
+    procedure Test_BorderRadius_AsString;
+    procedure Test_Margin_AsString;
+    procedure Test_MarginBlock_AsString; // todo
+    //procedure Test_MarginInline_AsString; // todo
+    procedure Test_Padding_AsString;
+    procedure Test_BackgroundPosition_AsString;
+    // todo: Test_Gap_AsString
+    // todo: Test_PlaceContent_AsString
+    // todo: Test_PlaceItems_AsString
+    // todo: Test_PlaceSelf_AsString
+
+    procedure TestVar_NoDefault;
+    procedure TestVar_Initial;
+    procedure TestVar_Inline;
   end;
 
+function LinesToStr(const Args: array of const): string;
+
 implementation
 
 const
-  // char sizes for a font size of 100
+  // char sizes for a font size of 1000
   CharHeight = 115;
   CharWidths: array[32..126] of word = (
     278, // space
@@ -209,10 +254,10 @@ const
 
 function CompareTestFont(Desc1, Desc2: Pointer): integer;
 var
-  A: PFresnelFontDesc absolute Desc1;
-  B: PFresnelFontDesc absolute Desc2;
+  A: TTestFont absolute Desc1;
+  B: TTestFont absolute Desc2;
 begin
-  Result:=A^.Compare(B^);
+  Result:=A.Desc.Compare(B.Desc);
 end;
 
 function CompareFontDescTestFont(aDesc, aFont: Pointer): integer;
@@ -223,6 +268,28 @@ begin
   Result:=Desc^.Compare(Font.Desc);
 end;
 
+function LinesToStr(const Args: array of const): string;
+var
+  s: String;
+  i: Integer;
+begin
+  s:='';
+  for i:=Low(Args) to High(Args) do
+  begin
+    case Args[i].VType of
+      vtChar:         s += Args[i].VChar+LineEnding;
+      vtString:       s += Args[i].VString^+LineEnding;
+      vtPChar:        s += Args[i].VPChar+LineEnding;
+      vtWideChar:     s += String(Args[i].VWideChar)+LineEnding;
+      vtPWideChar:    s += String(Args[i].VPWideChar)+LineEnding;
+      vtAnsiString:   s += AnsiString(Args[i].VAnsiString)+LineEnding; // FPC uses encoding CP_UTF8 for TVarRec.VAnsiString
+      vtWidestring:   s += String(WideString(Args[i].VWideString))+LineEnding;
+      vtUnicodeString:s += String(UnicodeString(Args[i].VUnicodeString))+LineEnding;
+    end;
+  end;
+  Result:=s;
+end;
+
 { TTestFont }
 
 function TTestFont.GetFamily: string;
@@ -235,12 +302,12 @@ begin
   Result:=Desc.Family;
 end;
 
-function TTestFont.GetKerning: string;
+function TTestFont.GetKerning: TFresnelCSSKerning;
 begin
   Result:=Desc.Kerning;
 end;
 
-function TTestFont.GetSize: Double;
+function TTestFont.GetSize: double;
 begin
   Result:=Desc.Size;
 end;
@@ -255,6 +322,11 @@ begin
   Result:=Desc.Variant_;
 end;
 
+function TTestFont.GetWidth: double;
+begin
+  Result:=Desc.Width;
+end;
+
 function TTestFont.GetWeight: double;
 begin
   Result:=Desc.Weight;
@@ -316,7 +388,7 @@ begin
       end;
     32..126:
       begin
-        AddChar(aSize*CharWidths[CodePoint]/100);
+        AddChar(aSize*CharWidths[CodePoint]/1000);
         inc(p);
       end
     else
@@ -325,7 +397,7 @@ begin
       {$ELSE}
       CodePoint:=0;
       {$ENDIF}
-      AddChar(aSize*CharWidths[65]/100);
+      AddChar(aSize*CharWidths[65]/1000);
       inc(p,CodepointLen);
     end;
   end;
@@ -381,6 +453,16 @@ end;
 
 { TTestRenderer }
 
+procedure TTestRenderer.Arc(const aColor: TFPColor; const aCenter, aRadii: TFresnelPoint;
+  aStartAngle: TFresnelLength; aStopAngle: TFresnelLength);
+begin
+  if aColor=colBlack then;
+  if aCenter.X=0 then ;
+  if aRadii.X=0 then;
+  if aStartAngle=0 then;
+  if aStopAngle=0 then;
+end;
+
 procedure TTestRenderer.FillRect(const aColor: TFPColor;
   const aRect: TFresnelRect);
 begin
@@ -395,6 +477,14 @@ begin
   if x1+y1+x2+y2=0 then ;
 end;
 
+procedure TTestRenderer.RoundRect(const aColor: TFPColor; const aRect: TFresnelRoundRect;
+  Fill: Boolean);
+begin
+  if aColor=colBlack then;
+  if aRect.Box.Left=0 then;
+  if Fill then;
+end;
+
 procedure TTestRenderer.TextOut(const aLeft, aTop: TFresnelLength;
   const aFont: IFresnelFont; const aColor: TFPColor; const aText: string);
 begin
@@ -435,12 +525,15 @@ begin
   inherited Create(AOwner);
   Layouter:=TViewportLayouter.Create(nil);
   TViewportLayouter(Layouter).Viewport:=Self;
+  FontEngine:=TTestFontEngine.Create(Self);
   Renderer:=TTestRenderer.Create(Self);
 end;
 
 destructor TTestViewport.Destroy;
 begin
   FreeAndNil(FRenderer);
+  FontEngine.Free;
+  FontEngine:=nil;
   Layouter.Free;
   Layouter:=nil;
   inherited Destroy;
@@ -460,7 +553,6 @@ begin
     LayoutQueued:=false;
     ApplyCSS;
     //Layouter.WriteLayoutTree;
-    Layouter.Apply(Self);
   end;
   Renderer.Draw(Self);
 end;
@@ -494,8 +586,508 @@ begin
   Body.Name:='Body';
   Body.Parent:=Viewport;
   Viewport.Draw;
-  Body.WriteComputedAttributes('Body');
+  //Body.WriteComputedAttributes('Body');
+end;
+
+procedure TTestFresnelCSS.TestGetStyleAttr_OneValue;
+begin
+  if Viewport.Style<>'' then
+    Fail('20240820190117');
+  Viewport.Style:='padding:3px';
+  AssertEquals('padding:3px',Viewport.Style);
+  AssertEquals('3px',Viewport.GetStyleAttr('padding'));
+end;
+
+procedure TTestFresnelCSS.TestGetStyleAttr_TwoValues;
+begin
+  Viewport.Style:='padding-left:3px; padding-top: 4px';
+  AssertEquals('3px',Viewport.GetStyleAttr('padding-left'));
+  AssertEquals('4px',Viewport.GetStyleAttr('padding-top'));
+end;
+
+procedure TTestFresnelCSS.TestGetStyleAttr_OneFunction;
+begin
+  Viewport.Style:='padding-left:var(--bird)';
+  AssertEquals('var(--bird)',Viewport.GetStyleAttr('padding-left'));
+end;
+
+procedure TTestFresnelCSS.TestGetStyleAttr_TwoFunctions;
+begin
+  Viewport.Style:='padding-left:var(--bird); padding-right: min(10px, 20%) ';
+  AssertEquals('var(--bird)',Viewport.GetStyleAttr('padding-left'));
+  AssertEquals('min(10px, 20%)',Viewport.GetStyleAttr('padding-right'));
+end;
+
+procedure TTestFresnelCSS.TestGetStyleAttr_NestedFunctions;
+begin
+  Viewport.Style:='padding-left: calc(var(--bird)*10%) ; padding-right: min(max(10%,3em), 20%) min(3px,5ch)';
+  AssertEquals('calc(var(--bird)*10%)',Viewport.GetStyleAttr('padding-left'));
+  AssertEquals('min(max(10%,3em), 20%) min(3px,5ch)',Viewport.GetStyleAttr('padding-right'));
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_NewValueEmpty;
+begin
+  if not Viewport.SetStyleAttr('padding-left','') then
+    Fail('20240820193346');
+  AssertEquals('',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_NewValueFirst;
+begin
+  if not Viewport.SetStyleAttr('padding-left','3px') then
+    Fail('20240820193354');
+  AssertEquals('padding-left:3px',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_NewValueAppend;
+begin
+  Viewport.Style:='padding-left:4px';
+  if not Viewport.SetStyleAttr('padding-right','7px') then
+    Fail('20240820193401');
+  AssertEquals('padding-left:4px; padding-right:7px',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_NewValueAppendSemicolon;
+begin
+  Viewport.Style:='padding-left:4px ;';
+  if not Viewport.SetStyleAttr('padding-right','7px') then
+    Fail('20240820194710');
+  AssertEquals('padding-left:4px ; padding-right:7px',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_DeleteOnlyValue;
+begin
+  Viewport.Style:='padding-left:4px';
+  if not Viewport.SetStyleAttr('padding-left','') then
+    Fail('20240820193844');
+  AssertEquals('',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_DeleteFirstValue;
+begin
+  Viewport.Style:='padding-left:4px; padding-top:3px';
+  if not Viewport.SetStyleAttr('padding-left','') then
+    Fail('20240820193847');
+  AssertEquals('padding-top:3px',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_DeleteLastValue;
+begin
+  Viewport.Style:='padding-left:4px ; padding-top:3px';
+  if not Viewport.SetStyleAttr('padding-top','') then
+    Fail('20240820194509');
+  AssertEquals('padding-left:4px ;',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_DeleteMiddleValue;
+begin
+  Viewport.Style:='padding-left:4px ; padding-top:3px; padding-right: 2px';
+  if not Viewport.SetStyleAttr('padding-top','') then
+    Fail('20240820195100');
+  AssertEquals('padding-left:4px ;padding-right: 2px',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_ReplaceOnlyValue;
+begin
+  Viewport.Style:='padding-left: 4px;';
+  if not Viewport.SetStyleAttr('padding-left','5em') then
+    Fail('20240820195245');
+  AssertEquals('padding-left:5em;',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_ReplaceFirstValue;
+begin
+  Viewport.Style:='padding-left: 4px ; padding-top:3px';
+  if not Viewport.SetStyleAttr('padding-left','7em') then
+    Fail('20240820195924');
+  AssertEquals('padding-left:7em; padding-top:3px',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_ReplaceLastValue;
+begin
+  Viewport.Style:='padding-left: 4px ; padding-top: 3px ';
+  if not Viewport.SetStyleAttr('padding-top','7em') then
+    Fail('20240820200021');
+  AssertEquals('padding-left: 4px ; padding-top:7em',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.TestSetStyleAttr_ReplaceMiddleValue;
+begin
+  Viewport.Style:='padding-left:4px ; padding-top: 3px ; padding-right: 2px';
+  if not Viewport.SetStyleAttr('padding-top','7em') then
+    Fail('20240820200135');
+  AssertEquals('padding-left:4px ; padding-top:7em; padding-right: 2px',Viewport.Style);
+end;
+
+procedure TTestFresnelCSS.Test_FontSize_Percentage;
+var
+  Body: TBody;
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    'font-size:30px;',
+    '}',
+    'div {',
+    'font-size:200%;',
+    '}']);
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Body.Font.GetSize',Body.Font.GetSize,30);
+  AssertEquals('Div1.Font.GetSize',Div1.Font.GetSize,60);
+end;
+
+procedure TTestFresnelCSS.Test_FontSize_AsString;
+var
+  Body: TBody;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    'font-size:3em;',
+    '}']);
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Body.Font.GetSize',30,Body.Font.GetSize);
+  AssertEquals('Body.GetComputedString(fcaFontSize)','30px',Body.GetComputedString(fcaFontSize));
+end;
+
+procedure TTestFresnelCSS.Test_Font_AsString;
+var
+  Body: TBody;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    'font-family:Arial;',
+    'font-kerning:normal;',
+    'font-size:12px;',
+    'font-style:italic;',
+    'font-weight:250;',
+    'font-width:condensed;',
+    'font-variant:normal;',
+    'line-height:20px;',
+    '}']);
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+
+  AssertEquals('Body.Font.GetFamily','Arial',Body.Font.GetFamily);
+  AssertEquals('Body.GetComputedString(fcaFontFamily)','Arial',Body.GetComputedString(fcaFontFamily));
+
+  AssertEquals('Body.Font.GetKerning','normal',FresnelCSSKerningNames[Body.Font.GetKerning]);
+  AssertEquals('Body.GetComputedString(fcaFontKerning)','normal',Body.GetComputedString(fcaFontKerning));
+
+  AssertEquals('Body.Font.GetSize',12,Body.Font.GetSize);
+  AssertEquals('Body.GetComputedString(fcaFontSize)','12px',Body.GetComputedString(fcaFontSize));
+
+  AssertEquals('Body.Font.GetStyle','italic',Body.Font.GetStyle);
+  AssertEquals('Body.GetComputedString(fcaFontStyle)','italic',Body.GetComputedString(fcaFontStyle));
+
+  AssertEquals('Body.Font.GetWeight',250,Body.Font.GetWeight);
+  AssertEquals('Body.GetComputedString(fcaFontWeight)','250',Body.GetComputedString(fcaFontWeight));
+
+  AssertEquals('Body.Font.GetWidth',0.75,Body.Font.GetWidth);
+  AssertEquals('Body.GetComputedString(fcaFontWidth)','75%',Body.GetComputedString(fcaFontWidth));
+  AssertEquals('Body.GetComputedString(fcaFontStretch)','75%',Body.GetComputedString(fcaFontStretch));
+
+  AssertEquals('Body.Font.GetVariant','normal',Body.Font.GetVariant);
+  AssertEquals('Body.GetComputedString(fcaFontVariant)','normal',Body.GetComputedString(fcaFontVariant));
+
+  AssertEquals('Body.GetComputedString(fcaFont)','italic 250 12px/20px condensed Arial',Body.GetComputedString(fcaFont));
+end;
+
+procedure TTestFresnelCSS.Test_Overflow_AsString;
+var
+  Body: TBody;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    'overflow:visible hidden;',
+    '}']);
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Body.GetComputedString(fcaOverflowX)','visible',Body.GetComputedString(fcaOverflowX));
+  AssertEquals('Body.GetComputedString(fcaOverflowY)','hidden',Body.GetComputedString(fcaOverflowY));
+  AssertEquals('Body.GetComputedString(fcaOverflow)','visible hidden',Body.GetComputedString(fcaOverflow));
+end;
+
+procedure TTestFresnelCSS.Test_BorderColor_AsString;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'border-color:#111 #222 #333 #444;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaBorderTopColor)','#111',Div1.GetComputedString(fcaBorderTopColor));
+  AssertEquals('Div1.GetComputedString(fcaBorderRightColor)','#222',Div1.GetComputedString(fcaBorderRightColor));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomColor)','#333',Div1.GetComputedString(fcaBorderBottomColor));
+  AssertEquals('Div1.GetComputedString(fcaBorderLeftColor)','#444',Div1.GetComputedString(fcaBorderLeftColor));
+  AssertEquals('Div1.GetComputedString(fcaBorderColor)','#111 #222 #333 #444',Div1.GetComputedString(fcaBorderColor));
+end;
+
+procedure TTestFresnelCSS.Test_BorderStyle_AsString;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'border-style:solid dashed ridge none;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaBorderTopStyle)','solid',Div1.GetComputedString(fcaBorderTopStyle));
+  AssertEquals('Div1.GetComputedString(fcaBorderRightStyle)','dashed',Div1.GetComputedString(fcaBorderRightStyle));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomStyle)','ridge',Div1.GetComputedString(fcaBorderBottomStyle));
+  AssertEquals('Div1.GetComputedString(fcaBorderLeftStyle)','none',Div1.GetComputedString(fcaBorderLeftStyle));
+  AssertEquals('Div1.GetComputedString(fcaBorderStyle)','solid dashed ridge none',Div1.GetComputedString(fcaBorderStyle));
+end;
+
+procedure TTestFresnelCSS.Test_BorderWidth_AsString;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'border-width:1px 2px 3px 4px;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaBorderTopWidth)','1px',Div1.GetComputedString(fcaBorderTopWidth));
+  AssertEquals('Div1.GetComputedString(fcaBorderRightWidth)','2px',Div1.GetComputedString(fcaBorderRightWidth));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomWidth)','3px',Div1.GetComputedString(fcaBorderBottomWidth));
+  AssertEquals('Div1.GetComputedString(fcaBorderLeftWidth)','4px',Div1.GetComputedString(fcaBorderLeftWidth));
+  AssertEquals('Div1.GetComputedString(fcaBorderWidth)','1px 2px 3px 4px',Div1.GetComputedString(fcaBorderWidth));
+end;
 
+procedure TTestFresnelCSS.Test_Border_AsString;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'border:1px red solid;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaBorderTopWidth)','1px',Div1.GetComputedString(fcaBorderTopWidth));
+  AssertEquals('Div1.GetComputedString(fcaBorderRightWidth)','1px',Div1.GetComputedString(fcaBorderRightWidth));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomWidth)','1px',Div1.GetComputedString(fcaBorderBottomWidth));
+  AssertEquals('Div1.GetComputedString(fcaBorderLeftWidth)','1px',Div1.GetComputedString(fcaBorderLeftWidth));
+
+  AssertEquals('Div1.GetComputedString(fcaBorderTopStyle)','solid',Div1.GetComputedString(fcaBorderTopStyle));
+  AssertEquals('Div1.GetComputedString(fcaBorderRightStyle)','solid',Div1.GetComputedString(fcaBorderRightStyle));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomStyle)','solid',Div1.GetComputedString(fcaBorderBottomStyle));
+  AssertEquals('Div1.GetComputedString(fcaBorderLeftStyle)','solid',Div1.GetComputedString(fcaBorderLeftStyle));
+
+  AssertEquals('Div1.GetComputedString(fcaBorderTopColor)','red',Div1.GetComputedString(fcaBorderTopColor));
+  AssertEquals('Div1.GetComputedString(fcaBorderRightColor)','red',Div1.GetComputedString(fcaBorderRightColor));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomColor)','red',Div1.GetComputedString(fcaBorderBottomColor));
+  AssertEquals('Div1.GetComputedString(fcaBorderLeftColor)','red',Div1.GetComputedString(fcaBorderLeftColor));
+
+  AssertEquals('Div1.GetComputedString(fcaBorderTop)','red solid 1px',Div1.GetComputedString(fcaBorderTop));
+  AssertEquals('Div1.GetComputedString(fcaBorderRight)','red solid 1px',Div1.GetComputedString(fcaBorderRight));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottom)','red solid 1px',Div1.GetComputedString(fcaBorderBottom));
+  AssertEquals('Div1.GetComputedString(fcaBorderLeft)','red solid 1px',Div1.GetComputedString(fcaBorderLeft));
+
+  AssertEquals('Div1.GetComputedString(fcaBorderWidth)','red solid 1px',Div1.GetComputedString(fcaBorder));
+end;
+
+procedure TTestFresnelCSS.Test_BorderRadius_AsString;
+var
+  Div1: TDiv;
+  p: TFresnelPoint;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'border-radius:1px 2px 3px 4px / 5px 6px 7px 8px;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaBorderTopLeftRadius)','1px / 5px',Div1.GetComputedString(fcaBorderTopLeftRadius));
+  AssertEquals('Div1.GetComputedString(fcaBorderTopRightRadius)','2px / 6px',Div1.GetComputedString(fcaBorderTopRightRadius));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomRightRadius)','3px / 7px',Div1.GetComputedString(fcaBorderBottomRightRadius));
+  AssertEquals('Div1.GetComputedString(fcaBorderBottomLeftRadius)','4px / 8px',Div1.GetComputedString(fcaBorderBottomLeftRadius));
+
+  p:=Div1.GetComputedBorderRadius(fcsTopLeft);
+  AssertEquals('Div1.GetComputedBorderRadius(fcsTopLeft)','(1,5)',p.ToString);
+
+  AssertEquals('Div1.GetComputedString(fcaBorderRadius)','1px 2px 3px 4px / 5px 6px 7px 8px',Div1.GetComputedString(fcaBorderRadius));
+end;
+
+procedure TTestFresnelCSS.Test_Margin_AsString;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'margin:1px 2px 3px 4px;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaMarginTop)','1px',Div1.GetComputedString(fcaMarginTop));
+  AssertEquals('Div1.GetComputedString(fcaMarginRight)','2px',Div1.GetComputedString(fcaMarginRight));
+  AssertEquals('Div1.GetComputedString(fcaMarginBottom)','3px',Div1.GetComputedString(fcaMarginBottom));
+  AssertEquals('Div1.GetComputedString(fcaMarginLeft)','4px',Div1.GetComputedString(fcaMarginLeft));
+  AssertEquals('Div1.GetComputedString(fcaMargin)','1px 2px 3px 4px',Div1.GetComputedString(fcaMargin));
+end;
+
+procedure TTestFresnelCSS.Test_MarginBlock_AsString;
+var
+  Div1: TDiv;
+begin
+  exit;
+
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'margin-block:1px 2px;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaMarginBlockStart)','1px',Div1.GetComputedString(fcaMarginBlockStart));
+  AssertEquals('Div1.GetComputedString(fcaMarginBlockEnd)','2px',Div1.GetComputedString(fcaMarginBlockEnd));
+  AssertEquals('Div1.GetComputedString(fcaMarginTop)','3px',Div1.GetComputedString(fcaMarginTop));
+  AssertEquals('Div1.GetComputedString(fcaMarginRight)','4px',Div1.GetComputedString(fcaMarginRight));
+  AssertEquals('Div1.GetComputedString(fcaMarginBottom)','3px',Div1.GetComputedString(fcaMarginBottom));
+  AssertEquals('Div1.GetComputedString(fcaMarginLeft)','4px',Div1.GetComputedString(fcaMarginLeft));
+  AssertEquals('Div1.GetComputedString(fcaMarginWidth)','1px 2px 3px 4px',Div1.GetComputedString(fcaMargin));
+end;
+
+procedure TTestFresnelCSS.Test_Padding_AsString;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'padding:1px 2px 3px 4px;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaPaddingTop)','1px',Div1.GetComputedString(fcaPaddingTop));
+  AssertEquals('Div1.GetComputedString(fcaPaddingRight)','2px',Div1.GetComputedString(fcaPaddingRight));
+  AssertEquals('Div1.GetComputedString(fcaPaddingBottom)','3px',Div1.GetComputedString(fcaPaddingBottom));
+  AssertEquals('Div1.GetComputedString(fcaPaddingLeft)','4px',Div1.GetComputedString(fcaPaddingLeft));
+  AssertEquals('Div1.GetComputedString(fcaPadding)','1px 2px 3px 4px',Div1.GetComputedString(fcaPadding));
+end;
+
+procedure TTestFresnelCSS.Test_BackgroundPosition_AsString;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'background-position:left 10px bottom 15%;',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedString(fcaBackgroundPositionX)','left 10px',Div1.GetComputedString(fcaBackgroundPositionX));
+  AssertEquals('Div1.GetComputedString(fcaBackgroundPositionY)','bottom 15%',Div1.GetComputedString(fcaBackgroundPositionY));
+  AssertEquals('Div1.GetComputedString(fcaBackgroundPosition)','left 10px bottom 15%',Div1.GetComputedString(fcaBackgroundPosition));
+end;
+
+procedure TTestFresnelCSS.TestVar_NoDefault;
+var
+  Body: TBody;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    ':root {',
+    '--bird-color:red;',
+    '}',
+    'body {',
+    'color:var(--bird-color);',
+    '}']);
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+  Viewport.ApplyCSS;
+  AssertEquals('red',Viewport.GetComputedCSSString('--bird-color'));
+  AssertEquals('red',Body.GetComputedCSSString('--bird-color'));
+  AssertEquals('red',Body.GetComputedCSSString('color'));
+end;
+
+procedure TTestFresnelCSS.TestVar_Initial;
+var
+  Body: TBody;
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'body {',
+    'font-size:30px;',
+    '}',
+    'div {',
+    'font-size:var(--none,initial);',
+    '}']);
+  Body:=TBody.Create(Viewport);
+  Body.Name:='Body';
+  Body.Parent:=Viewport;
+
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Body;
+
+  Viewport.ApplyCSS;
+  AssertEquals('Body.GetComputedFontSize',Body.Font.GetSize,30);
+  AssertEquals('Div1.GetComputedFontSize',Div1.Font.GetSize,FresnelDefaultFontSize);
+end;
+
+procedure TTestFresnelCSS.TestVar_Inline;
+var
+  Div1: TDiv;
+begin
+  Viewport.Stylesheet.Text:=LinesToStr([
+    'div {',
+    'font-size:var(--size);',
+    '}']);
+  Div1:=TDiv.Create(Viewport);
+  Div1.Name:='Div1';
+  Div1.Parent:=Viewport;
+  Div1.Style:='--size:28px;';
+
+  Viewport.ApplyCSS;
+  AssertEquals('Div1.GetComputedFontSize',28,Div1.Font.GetSize);
 end;
 
 Initialization

+ 9 - 1
tests/base/TestFresnelBase.lpi

@@ -4,7 +4,6 @@
     <Version Value="12"/>
     <General>
       <Flags>
-        <SaveOnlyProjectUnits Value="True"/>
         <MainUnitHasCreateFormStatements Value="False"/>
         <MainUnitHasTitleStatement Value="False"/>
         <MainUnitHasScaledStatement Value="False"/>
@@ -73,6 +72,15 @@
       <Unit>
         <Filename Value="tctextlayout.pas"/>
         <IsPartOfProject Value="True"/>
+        <UnitName Value="TCTextLayout"/>
+      </Unit>
+      <Unit>
+        <Filename Value="TCFlowLayout.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
+      <Unit>
+        <Filename Value="TCFlexLayout.pas"/>
+        <IsPartOfProject Value="True"/>
       </Unit>
     </Units>
   </ProjectOptions>

+ 2 - 1
tests/base/TestFresnelBase.lpr

@@ -11,7 +11,8 @@ program TestFresnelBase;
 {$mode objfpc}{$H+}
 
 uses
-  Classes, consoletestrunner, TCFresnelCSS, TCFresnelBaseEvents, TCFresnelImages, tcTextLayout;
+  Classes, consoletestrunner, TCFresnelCSS, TCFresnelBaseEvents, TCFresnelImages, TCTextLayout,
+  TCFlowLayout, TCFlexLayout;
 
 type
 

+ 83 - 13
tests/base/tcfresnelimages.pas

@@ -1,6 +1,9 @@
 unit TCFresnelImages;
 
 {$mode ObjFPC}{$H+}
+{$IF FPC_FULLVERSION>30300}
+  {$DEFINE HasIOUtils}
+{$ENDIF}
 
 interface
 
@@ -84,7 +87,6 @@ Type
     FStore: TImageStore;
     FDir : string;
   Public
-    Procedure CopyFile(aFrom,aTo : String);
     Procedure CreateImages;
     Procedure Setup; override;
     Procedure TearDown; override;
@@ -112,9 +114,80 @@ Type
     Procedure TestHookup;
   end;
 
+procedure DeleteDir(Dir: string);
+procedure CopyFile(Src, Dest: string; Overwrite: boolean = false);
+
 implementation
 
-uses inifiles, fpreadpng, fpwritepng, System.IOUtils;
+uses inifiles,
+  {$IFDEF HasIOUtils}
+  System.IOUtils,
+  {$ENDIF}
+  fpreadpng, fpwritepng;
+
+procedure DeleteDir(Dir: string);
+{$IFDEF HasIOUtils}
+{$ELSE}
+var
+  Info: TRawByteSearchRec;
+  List: TStringList;
+  i: Integer;
+  Filename: String;
+{$ENDIF}
+begin
+  {$IFDEF HasIOUtils}
+  TDirectory.Delete(Dir,True);
+  {$ELSE}
+  if not DirectoryExists(Dir) then exit;
+  Info:=Default(TRawByteSearchRec);
+  List:=TStringList.Create;
+  try
+    if FindFirst(Dir+PathDelim+AllFilesMask,faAnyFile,Info)=0 then
+      repeat
+        if (Info.Name='.') or (Info.Name='..') then continue;
+        List.Add(Info.Name);
+      until FindNext(Info)<>0;
+    for i:=0 to List.Count-1 do
+      begin
+      Filename:=Dir+PathDelim+List[i];
+      if not DeleteFile(Filename) then
+        raise Exception.Create('20240906122331 DeleteDir failed '+Filename);
+      end;
+  finally
+    FindClose(Info);
+  end;
+  {$ENDIF}
+end;
+
+procedure CopyFile(Src, Dest: string; Overwrite: boolean);
+{$IFDEF HasIOUtils}
+{$ELSE}
+var
+  SrcFS, DestFS: TFileStream;
+{$ENDIF}
+begin
+  {$IFDEF HasIOUtils}
+  TFile.Copy(Src,Dest,Overwrite);
+  {$ELSE}
+  if Overwrite and FileExists(Dest) then
+    begin
+    if not DeleteFile(Dest) then
+      raise Exception.Create('20240906122205 failed to delete file "'+Dest+'"');
+    end;
+
+  SrcFS:=TFileStream.Create(Src,fmOpenRead or fmShareDenyNone);
+  try
+    DestFS:=TFileStream.Create(Dest,fmCreate or fmShareDenyNone);
+    try
+      DestFS.CopyFrom(SrcFS,SrcFS.Size);
+    finally
+      DestFS.Free;
+    end;
+  finally
+    SrcFS.Free;
+  end;
+  {$ENDIF}
+end;
 
 { TImageTest }
 Procedure TImageTest.ReadConfig;
@@ -279,7 +352,9 @@ begin
   AssertNotNull('Have result',R);
   AssertEquals('Correct resolution',96,R.Resolution);
   AssertFalse('Default',R.Default);
+  {$IFDEF HasFunctionReferences}
   AssertException('No second resolution',EImageData,@AddData);
+  {$ENDIF}
 end;
 
 procedure TTestResolutionList.TestFindDefaultResolution;
@@ -392,16 +467,11 @@ end;
 
 { TTestImageStore }
 
-procedure TTestImageStore.CopyFile(aFrom, aTo: String);
-begin
-  TFile.Copy(aFrom,aTo);
-end;
-
 procedure TTestImageStore.CreateImages;
 begin
-  TFile.Copy(GetStdImage(10),ImagesConfig.GetSizedImageFileName('img'));
-  TFile.Copy(GetStdImage(20),ImagesConfig.GetSizedImageFileName('img',ImagesConfig.IconSize*2));
-  TFile.Copy(GetStdImage(30),ImagesConfig.GetSizedImageFileName('img',ImagesConfig.IconSize,192));
+  CopyFile(GetStdImage(10),ImagesConfig.GetSizedImageFileName('img'));
+  CopyFile(GetStdImage(20),ImagesConfig.GetSizedImageFileName('img',ImagesConfig.IconSize*2));
+  CopyFile(GetStdImage(30),ImagesConfig.GetSizedImageFileName('img',ImagesConfig.IconSize,192));
 end;
 
 procedure TTestImageStore.Setup;
@@ -416,7 +486,7 @@ end;
 procedure TTestImageStore.TearDown;
 begin
   FreeAndNil(FFreeImg);
-  TDirectory.Delete(FDir,True);
+  DeleteDir(FDir);
   FreeAndNil(FStore);
   inherited TearDown;
 end;
@@ -473,7 +543,7 @@ end;
 procedure TTestImageStore.TestNoSize;
 begin
   CreateImages;
-  TFile.Copy(GetStdImage(30),ImagesConfig.ImageDir+'img_96.png');
+  CopyFile(GetStdImage(30),ImagesConfig.ImageDir+'img_96.png');
   Store.Size:=0;
   Store.GetImageData('img',FFreeImg,True);
   AssertNotNull('Have image',FreeImg);
@@ -484,7 +554,7 @@ end;
 procedure TTestImageStore.TestNoResolution;
 begin
   CreateImages;
-  TFile.Copy(GetStdImage(30),ImagesConfig.ImageDir+'img_24x24.png',true);
+  CopyFile(GetStdImage(30),ImagesConfig.ImageDir+'img_24x24.png',true);
   Store.Size:=24;
   Store.Resolution:=0;
   Store.GetImageData('img',FFreeImg,True);

+ 3 - 5
tests/base/tctextlayout.pas

@@ -1,11 +1,11 @@
-unit tctextlayout;
+unit TCTextLayout;
 
 {$mode objfpc}{$H+}
 
 interface
 
 uses
-  Classes, SysUtils, types, fpcunit, testutils, testregistry, fpimage, fresnel.textlayouter;
+  Classes, SysUtils, fpcunit, testregistry, fpimage, fresnel.textlayouter;
 
 const
   cHeight = 15;
@@ -291,7 +291,6 @@ begin
   inherited SetUp;
   Layouter.Text:='this text';
   FTextBlock:=CreateTextBlock(0,4);
-  FFont:=TTextFont.Create(nil);
   FTextBlock.Font:=FFont;
   Block.Font.Name:='Arial';
   Block.Font.Size:=12;
@@ -301,7 +300,6 @@ end;
 
 procedure TTestTextBlock.TearDown;
 begin
-  FreeAndNil(FFont);
   FreeAndNil(FTextBlock);
   inherited TearDown;
 end;
@@ -324,7 +322,7 @@ begin
   With aBlock do
     begin
     ForceNewLine:=True;
-    LayoutPos:=PointF(2,3);
+    LayoutPos:=TTextPoint.Create(2,3);
     Size.Width:=12.3;
     Size.Height:=8.9;
     Size.Descender:=2.3;

Some files were not shown because too many files changed in this diff