Browse Source

* Text layouter + tests

Michaël Van Canneyt 1 year ago
parent
commit
ff647b6635

+ 1861 - 0
src/base/fresnel.textlayouter.pas

@@ -0,0 +1,1861 @@
+unit Fresnel.TextLayouter;
+
+{$mode objfpc}
+{$modeswitch advancedrecords}
+
+interface
+
+uses
+{$IFDEF FPC_DOTTEDUNITS}
+  System.Classes, System.SysUtils, System.Types, System.Contnrs, fpImage, System.UITypes;
+{$ELSE}
+  Classes, SysUtils, Types, Contnrs, fpImage, System.UITypes;
+{$ENDIF}
+
+Const
+  cEllipsis : UnicodeChar = #$2026; // '…';
+
+Type
+  {$IF SIZEOF(CHAR)=1}
+  TTextString = UTF8String;
+  {$ELSE}
+  TTextString = UnicodeString;
+  {$ENDIF}
+
+  ETextLayout = Class(Exception);
+
+  TWordOverflow = (woTruncate, // truncate the word
+                   woOverflow, // Allow to overflow
+                   woAsterisk, // Replace word with * chars.
+                   woEllipsis  // Truncate word, add ... ellipsis.
+                  );
+  TTextAlign = (Leading, Center, Trailing);
+  TStretchMode    = (smDontStretch, smActualHeight, smActualHeightStretchOnly, smActualHeightShrinkOnly, smMaxHeight);
+  TOverlappingRangesAction = (oraError,oraFit);
+  TCullThreshold = 1..100;
+
+  TTextUnits = single;
+
+  { No hyphenation:
+
+    1   5   10   15
+    the  cat saw   me
+       ^    ^   ^    ^
+    4 split points:
+    offset: 3, Whitespace 2
+    offset: 9, Whitespace: 1
+    offset: 13, Whitespace: 3
+    offset: 18, Whitespace: 1 (#0 considered whitespace)
+
+    With hyphenation:
+
+    1    5   10
+    orthography
+      ^   ^ ^  ^  (or–thog–ra–phy)
+    4 split points:
+    offset 2, whitespace 0
+    offset 6, whitespace 0
+    offset 8, whitespace 0
+    offset 11, whitespace 1 (#0 considered whitespace)
+  }
+
+  { TTextSplitPoint }
+
+  TTextSplitPoint = record
+    // 0-based, relative to origin.
+    offset : SizeInt;
+    // number of whitespace characters at the start of the split point
+    whitespace : SizeInt; // when zero, it is a hyphenation point.
+    Constructor Create(aOffset,aWhiteSpace : SizeInt);
+  end;
+  TTextSplitPointArray = Array of TTextSplitPoint;
+
+  TTextMeasures = record
+    Width, Height : TTextUnits;
+    Descender : TTextUnits;
+  end;
+  TTextPoint = {$IFDEF FPC_DOTTEDUNITS}System.{$ENDIF}Types.TPointF;
+
+  TFontAttribute = (faBold,faItalic,faStrikeThrough);
+  TFontAttributes = set of TFontAttribute;
+
+  { TTextFont }
+
+  TTextFont = Class(TPersistent)
+  private
+    FOwner : TPersistent;
+    FAttrs: TFontAttributes;
+    FName: string;
+    FSize: Smallint;
+    FColor : TFPColor;
+    function GetColor: TColor;
+    procedure SetAttrs(AValue: TFontAttributes);
+    procedure SetColor(AValue: TColor);
+    procedure SetFPColor(AValue: TFPColor);
+    procedure SetName(AValue: string);
+    procedure SetSize(AValue: Smallint);
+  Public
+    procedure Assign(Source: TPersistent); override;
+    Constructor Create(aOwner : TPersistent); virtual;
+    procedure Changed;virtual;
+    Function Clone(aOwner : TPersistent=nil): TTextFont;
+    Property FPColor : TFPColor Read FColor write SetFPColor;
+  Published
+    // In name
+    Property Name : string Read FName Write SetName;
+    // In pixels
+    Property Size : Smallint Read FSize write SetSize;
+    // attributes
+    Property Attrs : TFontAttributes read FAttrs Write SetAttrs;
+    // Color. Not needed for calculations, but allows for easier management
+    Property Color : TColor read GetColor Write SetColor;
+  end;
+  TTextFontClass = class of TTextFont;
+
+  TTextLayouter = Class;
+  TTextMeasurer = class;
+  TTextSplitter = Class;
+
+  { TTextSplitter }
+
+  TTextSplitter = class (TObject)
+  private
+    FLayouter: TTextLayouter;
+  Public
+    Constructor Create(aLayouter : TTextLayouter); virtual;
+    // Get next newline, The returned position is relative to StartPos (1-based). Offset=-1 means no newline
+    Function GetNextNewLine(const aText : TTextString; aStartPos : SizeInt) : TTextSplitPoint; virtual;
+    // The returned split point is relative text origin (which is 1-based).
+    Function GetNextSplitPoint(const aText : TTextString; aStartPos : SizeInt; aAllowHyphen : Boolean) : TTextSplitPoint; virtual;
+    // Return all possible split points for a text. aStartpos is 1-Based
+    Function SplitText(const aText : TTextString; aStartPos : SizeInt; aAllowHyphen : Boolean) : TTextSplitPointArray; virtual;
+    // Return all possible lines for a text. aStartpos is 1-Based
+    Function SplitLines(const aText : TTextString; aStartPos : SizeInt; aAllowHyphen : Boolean) : TTextSplitPointArray; virtual;
+    // Layouter
+    Property Layouter : TTextLayouter Read FLayouter;
+  end;
+  TTextSplitterClass = Class of TTextSplitter;
+
+  { TTextMeasurer }
+
+
+  TTextMeasurer = class abstract (TObject)
+  private
+    FLayouter: TTextLayouter;
+    FWhiteSpaceWidth : TTextUnits;
+  public
+    Constructor Create(aLayouter : TTextLayouter); virtual;
+    // Font size in points.
+    Procedure SetFont(const aFontName : String; aSize : SmallInt; Attrs : TFontAttributes); virtual; abstract;
+    Procedure SetFont(const aFont: TTextFont);
+    Function MeasureText(aText : String) : TTextMeasures; virtual; abstract;
+    Function WhitespaceWidth : TTextUnits;
+    Property Layouter : TTextLayouter Read FLayouter;
+
+  end;
+  TTextMeasurerClass = Class of TTextMeasurer;
+
+  { TFixedSizeTextMeasurer }
+
+  TFixedSizeTextMeasurer = Class(TTextMeasurer)
+  private
+    FHeight: TTextUnits;
+    FWidth: TTextUnits;
+    FSize : SmallInt;
+    FAttrs : TFontAttributes;
+    FFontName : String;
+  Public
+    Constructor Create(aLayouter : TTextLayouter); override;
+    Procedure SetFont(const aFontName : String; aSize : SmallInt; aAttrs : TFontAttributes); override;
+    Function MeasureText(aText : String) : TTextMeasures; override;
+    Property CharHeight : TTextUnits Read FHeight Write FHeight;
+    Property CharWidth : TTextUnits Read FWidth Write FWidth;
+    Property Size : SmallInt Read FSize;
+    Property Attributes : TFontAttributes Read FAttrs;
+    Property FontName : String Read FFontName;
+  end;
+
+
+  { TTextBlock }
+
+  TTextBlock = Class(TObject)
+  private
+    FLayouter : TTextLayouter;
+    function GetText: TTextString;
+
+  public
+    LayoutPos: TTextPoint;
+    Size : TTextMeasures;
+    Font: TTextFont;
+    // Zero based
+    TextOffset : Integer;
+    TextLen : Integer;
+    ForceNewLine : Boolean;
+    Suffix : String;
+  Public
+    Constructor Create(aLayouter : TTextLayouter); overload;virtual;
+    Constructor Create(aLayouter : TTextLayouter; aOffset,aLen : SizeInt); overload;
+    // At pos is relative to the text here, zero based
+    function Split(atPos : integer) : TTextBlock; virtual;
+    procedure Assign(aBlock : TTextBlock); virtual;
+    function ToString : RTLString; override;
+    Procedure TrimTrailingWhiteSpace;
+    Property Text : TTextString Read GetText;
+    Property Layouter : TTextLayouter Read FLayouter;
+    Property Width : TTextUnits Read Size.Width;
+    Property Height : TTextUnits Read Size.Height;
+    Property Descender : TTextUnits Read Size.Descender;
+  end;
+  TTextBlockClass = Class of TTextBlock;
+
+  { FTextBlockList }
+
+  { TTextBlockList }
+
+  TTextBlockList = Class(TFPObjectList)
+  private
+    function GetBlock(aIndex : Integer): TTextBlock;
+  public
+    Property Block [aIndex : Integer] : TTextBlock Read GetBlock; default;
+  end;
+
+
+  { TTextRange }
+
+  TTextRange = class(TCollectionItem)
+  private
+    FCharLength: SizeInt;
+    FCharOffset: SizeInt;
+    FFont: TTextFont;
+    procedure SetCharLength(AValue: SizeInt);
+    procedure SetCharOffSet(AValue: SizeInt);
+    procedure SetFont(AValue: TTextFont);
+  Protected
+    function CreateTextFont: TTextFont; virtual;
+  Public
+    constructor Create(ACollection: TCollection); override;
+    destructor destroy; override;
+    procedure Assign(Source: TPersistent); override;
+    Procedure Changed;
+    function ToString : RTLString; override;
+  Published
+    // Offset is 0 based and is the offset from the first character in the text.
+    Property CharOffset : SizeInt Read FCharOffset Write SetCharOffSet;
+    // Number of characters to count from offset
+    Property CharLength : SizeInt Read FCharLength Write SetCharLength;
+    // Name of font
+    Property Font : TTextFont Read FFont Write SetFont;
+  end;
+  TTextRangeClass = Class of TTextRange;
+
+  { TTextRangeList }
+
+  TTextRangeList = class (TOwnedCollection)
+  private
+    function GetRange(aIndex : integer): TTextRange;
+    procedure SetRange(aIndex : integer; AValue: TTextRange);
+  Public
+    Function AddRange(aOffset,aCharlength : SizeInt; aFont : TTextFont) : TTextRange;
+    Property Ranges[aIndex : integer] : TTextRange Read GetRange Write SetRange; default;
+  end;
+
+  { TTextLayoutBounds }
+
+  TTextLayoutBounds = Class(TPersistent)
+  private
+    FHeight: TTextUnits;
+    FWidth: TTextUnits;
+    FLayouter : TTextLayouter;
+    function GetAsPoint: TTextPoint;
+    procedure SetAsPoint(AValue: TTextPoint);
+    procedure SetHeight(AValue: TTextUnits);
+    procedure SetWidth(AValue: TTextUnits);
+  protected
+    procedure Changed; virtual;
+    function GetOwner: TPersistent; override;
+  public
+    procedure Assign(Source: TPersistent); override;
+    Constructor Create(aLayouter : TTextLayouter);
+    Property AsPoint : TTextPoint Read GetAsPoint Write SetAsPoint;
+  Published
+    Property Width : TTextUnits Read FWidth Write SetWidth;
+    Property Height : TTextUnits Read FHeight Write SetHeight;
+  end;
+
+  { TTextLayouter }
+
+  TTextLayouter = class (TComponent)
+  Private
+    FAllowHyphenation: Boolean;
+    FCullTreshold: TCullThreshold;
+    FHorizontalAlign: TTextAlign;
+    FHyphenationChar: String;
+    FLineSpacing: TTextUnits;
+    FBounds: TTextLayoutBounds;
+    FMaxStretch: TTextUnits;
+    FOverlappingRangesAction: TOverlappingRangesAction;
+    FRanges: TTextRangeList;
+    FStretchMode: TStretchMode;
+    FText: string;
+    FTextRanges: TTextRangeList;
+    FVerticalAlign: TTextAlign;
+    FWordOverFlow: TWordOverflow;
+    FBlocks : TTextBlockList;
+    FWordWrap: Boolean;
+    FFont : TTextFont;
+    FMeasurer : TTextMeasurer;
+    FSplitter : TTextSplitter;
+    function FindLastFittingCharPos(B: TTextBlock; const aSuffix: String; out aWidth : TTextUnits): Integer;
+    function GetBlock(aIndex : Integer): TTextBlock;
+    function GetBlockCount: Integer;
+    function GetColor: TFPColor;
+    procedure SetAllowHyphenation(AValue: Boolean);
+    procedure SetColor(AValue: TFPColor);
+    procedure SetCullTreshold(AValue: TCullThreshold);
+    procedure SetFont(AValue: TTextFont);
+    procedure SetHorizontalAlign(AValue: TTextAlign);
+    procedure SetHyphenationChar(AValue: String);
+    procedure SetLineSpacing(AValue: TTextUnits);
+    procedure SetBounds(AValue: TTextLayoutBounds);
+    procedure SetMaxStretch(AValue: TTextUnits);
+    procedure SetRanges(AValue: TTextRangeList);
+    procedure SetStretchMode(AValue: TStretchMode);
+    procedure SetText(AValue: string);
+    procedure SetTextRanges(AValue: TTextRangeList);
+    procedure SetVerticalAlign(AValue: TTextAlign);
+    procedure SetWordOverFlow(AValue: TWordOverflow);
+    procedure SetWordWrap(AValue: Boolean);
+  Protected
+    class Function CreateMeasurer(aLayouter : TTextLayouter): TTextMeasurer; virtual;
+    class Function CreateSplitter(aLayouter : TTextLayouter): TTextSplitter; virtual;
+    class function CreateRanges(aLayouter: TTextLayouter): TTextRangeList; virtual;
+    class function CreateBlock(aLayouter: TTextLayouter; aOffset,aLength : SizeInt) : TTextBlock; virtual;
+    function FindWrapPosition(B: TTextBlock; S: String; var aPos: integer; var CurrPos: TTextPoint): Boolean;
+    function AddBlock(aOffset, aLength: SizeInt; aFont: TTextFont): TTextBlock; virtual;
+    procedure ApplyStretchMode(const ADesiredHeight: TTextUnits); virtual;
+    function WrapBlock(B: TTextBlock; S: String; var Idx: integer; var CurrPos: TTextPoint) : Boolean; virtual;
+    procedure CullTextHorizontally(B: TTextBlock);
+    procedure HandleRanges; virtual;
+    procedure HandleNewLines; virtual;
+    // Apply vertical text alignment
+    procedure ApplyVertTextAlignment;
+    // Apply horizontal text alignment
+    procedure ApplyHorzTextAlignment;
+    // Remove text that falls outside bounds vertically.
+    procedure CullTextOutOfBoundsVertically;
+    // Handle text that falls outside bounds horizontally, depending on WordOverFlow.
+    procedure CullTextOutOfBoundsHorizontally;
+    // Return true if a split occurred.
+    function WrapLayout: Boolean; virtual;
+    // Return True if there are multiple lines.
+    function NoWrapLayout: Boolean; virtual;
+
+    Property Measurer : TTextMeasurer Read FMeasurer;
+    Property Splitter : TTextSplitter Read FSplitter;
+  Public
+    class var _TextMeasurerClass : TTextMeasurerClass;
+    class var _TextSplitterClass : TTextSplitterClass;
+    class var _TextRangeClass : TTextRangeClass;
+    class var _TextBlockClass : TTextBlockClass;
+  Public
+    Constructor Create(aOwner: TComponent); override;
+    Destructor Destroy; override;
+    // Clear block list.
+    Procedure Reset;
+    // Check if ranges do not overlap.
+    procedure CheckRanges;
+    function ToString : RTLString; override;
+    Property TextBlocks[aIndex : Integer] : TTextBlock Read GetBlock;
+    Property TextBlockCount : Integer Read GetBlockCount;
+    function Execute : integer; virtual;
+    function Execute(const aText : String) : Integer;
+    // Color of font
+    Property FPColor : TFPColor Read GetColor Write SetColor;
+  Published
+    // Setting text will clear attribute
+    Property Text : string Read FText Write SetText;
+    // Various properties
+    Property Ranges : TTextRangeList Read FRanges Write SetRanges;
+    // Do wordwrap ?
+    Property WordWrap : Boolean Read FWordWrap Write SetWordWrap;
+    // What to do in case of overflow ?
+    Property WordOverflow : TWordOverflow Read FWordOverFlow Write SetWordOverFlow;
+    // Allow to stretch maximum size ?
+    Property StretchMode : TStretchMode Read FStretchMode Write SetStretchMode;
+    // When to cull letters
+    Property CullThreshold : TCullThreshold Read FCullTreshold Write SetCullTreshold;
+    // Text ranges with different properties than the main properties.
+    Property TextRanges : TTextRangeList Read FTextRanges Write SetTextRanges;
+    // Name of font
+    Property Font : TTextFont Read FFont Write SetFont;
+    // Line spacing.
+    Property LineSpacing : TTextUnits Read FLineSpacing Write SetLineSpacing;
+    // Maximum size
+    Property Bounds : TTextLayoutBounds Read FBounds Write SetBounds;
+    // Vertical alignment of text
+    Property VerticalAlign : TTextAlign Read FVerticalAlign Write SetVerticalAlign;
+    // Horizontal alignment of text
+    Property HorizontalAlign : TTextAlign Read FHorizontalAlign Write SetHorizontalAlign;
+    // Maximum size
+    Property MaxStretch : TTextUnits Read FMaxStretch Write SetMaxStretch;
+    // Allow hyphenation ?
+    Property AllowHyphenation : Boolean Read FAllowHyphenation Write SetAllowHyphenation;
+    // Hyphenation character
+    Property HyphenationChar : String Read FHyphenationChar Write SetHyphenationChar;
+    // What to do if ranges overlap ?
+    Property OverlappingRangesAction : TOverlappingRangesAction Read FOverlappingRangesAction Write FOverlappingRangesAction;
+  end;
+
+Function SplitPoint(aOffset, aSpaces : SizeInt) : TTextSplitPoint;
+
+
+
+implementation
+
+resourcestring
+  SErrOverlappingRanges = 'Overlapping ranges: %s and %s';
+
+Function ColorToFPColor(aColor : TColor) : TFPColor;
+
+var
+  Rec : TColorRec absolute aColor;
+
+begin
+  Result.Alpha:=(Rec.A shl 8) or Rec.A;
+  Result.Red:=(Rec.R shl 8) or Rec.R;
+  Result.Green:=(Rec.G shl 8) or Rec.G;
+  Result.Blue:=(Rec.B shl 8) or Rec.B;
+end;
+
+function FPColorToColor(aColor : TFPColor) : TColor;
+
+var
+  aCol : TColor;
+  Rec : TColorRec absolute aCol;
+
+begin
+  Rec.A:=aColor.Alpha shr 8;
+  Rec.R:=aColor.Red shr 8;
+  Rec.G:=aColor.Green shr 8;
+  Rec.B:=aColor.Blue shr 8;
+  Result:=aCol;
+end;
+
+{ TTextBlock }
+
+function TTextBlock.GetText: TTextString;
+begin
+  Result:=Copy(Layouter.Text,1+TextOffset,TextLen);
+  If Suffix<>'' then
+    Result:=Result+Suffix;
+end;
+
+constructor TTextBlock.Create(aLayouter: TTextLayouter);
+begin
+  FLayouter:=aLayouter;
+end;
+
+constructor TTextBlock.Create(aLayouter: TTextLayouter; aOffset, aLen: SizeInt);
+begin
+  Create(aLayouter);
+  TextOffset:=aOffset;
+  TextLen:=aLen;
+end;
+
+function TTextBlock.Split(atPos: integer): TTextBlock;
+begin
+  Result:=TTextBlock.Create(Self.Layouter,Self.TextOffset+atPos,Self.TextLen-atPos);
+  Result.Font:=Self.Font;
+  Self.TextLen:=AtPos;
+  // Reset formatting stuff on new
+  Result.ForceNewLine:=False;
+  Result.LayoutPos:=Default(TPointF);
+  Result.Size.Width:=0;
+  Result.Size.Height:=0;
+  Result.Size.Descender:=0;
+  // and on current
+  Size.Width:=0;
+  Size.Height:=0;
+  Size.Descender:=0;
+end;
+
+procedure TTextBlock.Assign(aBlock: TTextBlock);
+begin
+  LayoutPos:=aBlock.LayoutPos;
+  Size:=aBlock.Size;
+  Font:=aBlock.Font;
+  TextOffset:=aBlock.TextOffset;
+  TextLen:=aBlock.TextLen;
+  ForceNewLine:=aBlock.ForceNewLine;
+end;
+
+function TTextBlock.ToString: RTLString;
+begin
+  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;
+
+procedure TTextBlock.TrimTrailingWhiteSpace;
+
+Const
+  WhiteSpace = [#0..#32];
+
+var
+  Len : SizeInt;
+
+begin
+  Len:=TextLen;
+  While (Len>0) and (Layouter.Text[TextOffSet+Len] in WhiteSpace) do
+    Dec(Len);
+  TextLen:=Len;
+end;
+
+{ TTextBlockList }
+
+function TTextBlockList.GetBlock(aIndex : Integer): TTextBlock;
+begin
+  Result:=TTextBlock(Items[aIndex]);
+end;
+
+{ TTextRange }
+
+procedure TTextRange.SetCharLength(AValue: SizeInt);
+begin
+  if FCharLength=AValue then Exit;
+  FCharLength:=AValue;
+  Changed;
+end;
+
+procedure TTextRange.SetCharOffSet(AValue: SizeInt);
+begin
+  if FCharOffset=AValue then Exit;
+  FCharOffset:=AValue;
+  Changed;
+end;
+
+procedure TTextRange.SetFont(AValue: TTextFont);
+begin
+  if FFont=AValue then Exit;
+  FFont.Assign(AValue);
+  Changed;
+end;
+
+constructor TTextRange.Create(ACollection: TCollection);
+begin
+  inherited Create(ACollection);
+  FFont:=CreateTextFont;
+end;
+
+destructor TTextRange.destroy;
+begin
+  FreeAndNil(FFont);
+  inherited destroy;
+end;
+
+function TTextRange.CreateTextFont: TTextFont;
+begin
+  Result:=TTextFont.Create(Self);
+end;
+
+procedure TTextRange.Assign(Source: TPersistent);
+var
+  aSource: TTextRange absolute Source;
+begin
+  if Source is TTextRange then
+    begin
+    FCharOffset:=aSource.CharOffset;
+    FCharLength:=aSource.CharLength;
+    // Triggers change
+    Font:=aSource.Font;
+    end
+  else
+    inherited Assign(Source);
+end;
+
+procedure TTextRange.Changed;
+begin
+  if Assigned(Collection) and (Collection.Owner is TTextLayouter) then
+    TTextLayouter(Collection.Owner).Reset;
+end;
+
+function TTextRange.ToString: RTLString;
+begin
+  Result:=Format('[offset %d, len: %d]',[CharOffset,CharLength]);
+end;
+
+{ TTextRangeList }
+
+function TTextRangeList.GetRange(aIndex : integer): TTextRange;
+begin
+  Result:=TTextRange(Items[aIndex]);
+end;
+
+procedure TTextRangeList.SetRange(aIndex : integer; AValue: TTextRange);
+begin
+  Items[aIndex]:=aValue;
+end;
+
+function TTextRangeList.AddRange(aOffset, aCharlength: SizeInt; aFont: TTextFont): TTextRange;
+begin
+  Result:=add as TTextRange;
+  Result.CharOffset:=aOffset;
+  Result.CharLength:=aCharlength;
+  Result.Font:=aFont;
+end;
+
+{ TTextLayoutBounds }
+
+procedure TTextLayoutBounds.SetHeight(AValue: TTextUnits);
+begin
+  if FHeight=AValue then Exit;
+  FHeight:=AValue;
+  Changed;
+end;
+
+function TTextLayoutBounds.GetAsPoint: TTextPoint;
+begin
+  Result:=PointF(Width,Height);
+end;
+
+procedure TTextLayoutBounds.SetAsPoint(AValue: TTextPoint);
+begin
+  FWidth:=aValue.X;
+  FHeight:=aValue.Y;
+  Changed;
+end;
+
+procedure TTextLayoutBounds.SetWidth(AValue: TTextUnits);
+begin
+  if FWidth=AValue then Exit;
+  FWidth:=AValue;
+  Changed;
+end;
+
+procedure TTextLayoutBounds.Changed;
+begin
+  if assigned(FLayouter) then
+    FLayouter.Reset;
+end;
+
+function TTextLayoutBounds.GetOwner: TPersistent;
+begin
+  Result:=FLayouter;
+end;
+
+procedure TTextLayoutBounds.Assign(Source: TPersistent);
+
+var
+  aSource: TTextLayoutBounds absolute Source;
+
+begin
+  if Source is TTextLayoutBounds then
+    begin
+    Width:=aSource.Width;
+    Height:=aSource.Height;
+    end
+  else
+    inherited Assign(Source);
+end;
+
+constructor TTextLayoutBounds.Create(aLayouter: TTextLayouter);
+begin
+  FLayouter:=aLayouter;
+end;
+
+{ TTextSplitter }
+
+Function SplitPoint(aOffset, aSpaces : SizeInt) : TTextSplitPoint;
+
+begin
+  Result:=TTextSplitPoint.Create(aOffset,aSpaces);
+end;
+
+constructor TTextSplitter.Create(aLayouter: TTextLayouter);
+begin
+  FLayouter:=aLayouter;
+end;
+
+function TTextSplitter.GetNextNewLine(const aText: TTextString; aStartPos: SizeInt): TTextSplitPoint;
+
+Const
+  NewLineChars = [#10,#13];
+
+var
+  Len,I,Sp : Integer;
+
+begin
+  Len:=Length(aText);
+  I:=aStartPos;
+  While (I<=Len) and Not (aText[I] in NewLineChars) do
+    Inc(I);
+  SP:=I;
+  While (I<=Len) and (aText[i] in NewLineChars) do
+    Inc(I);
+  if SP>Len then
+    Result.Offset:=-1
+  else
+    begin
+    Result.whitespace:=I-SP;
+    Result.offset:=SP-1;
+    end;
+end;
+
+function TTextSplitter.GetNextSplitPoint(const aText: TTextString; aStartPos: SizeInt; aAllowHyphen: Boolean): TTextSplitPoint;
+
+Const
+  WhiteSpace = [0..#32];
+
+var
+  Len,I,Sp : Integer;
+
+begin
+  Len:=Length(aText);
+  I:=aStartPos;
+  While (I<=Len) and Not (aText[I] in WhiteSpace) do
+    Inc(I);
+  SP:=I;
+  While (I<=Len) and (aText[i] in WhiteSpace) do
+    Inc(I);
+  if I>Len then
+    Result.whitespace:=1
+  else
+    Result.whitespace:=I-SP;
+  Result.offset:=SP-1;
+end;
+
+function TTextSplitter.SplitText(const aText: TTextString; aStartPos: SizeInt; aAllowHyphen: Boolean): TTextSplitPointArray;
+
+var
+  aPos,MaxOffset,Idx,Len : SizeInt;
+
+begin
+  Result:=[];
+  Len:=Length(aText);
+  MaxOffset:=Len-aStartPos+1;
+  Idx:=0;
+  aPos:=0;
+  SetLength(Result,MaxOffset);
+  Repeat
+    Result[Idx]:=GetNextSplitPoint(aText,aStartPos+aPos,aAllowHyphen);
+    aPos:=Result[Idx].offset+Result[Idx].WhiteSpace;
+    Inc(Idx);
+  until (aPos>=MaxOffset);
+  SetLength(Result,Idx);
+end;
+
+function TTextSplitter.SplitLines(const aText: TTextString; aStartPos: SizeInt; aAllowHyphen: Boolean): TTextSplitPointArray;
+
+var
+  aPos,MaxOffset,Idx,Len : SizeInt;
+  aTSP : TTextSplitPoint;
+
+begin
+  Result:=[];
+  Len:=Length(aText);
+  MaxOffset:=Len-aStartPos+1;
+  Idx:=0;
+  aPos:=0;
+  SetLength(Result,MaxOffset);
+  Repeat
+    aTSP:=GetNextNewLine(aText,aStartPos+aPos);
+    if aTSP.Offset<>-1 then
+      begin
+      Result[Idx]:=aTSP;
+      aPos:=aTSP.offset+Result[Idx].WhiteSpace;
+      Inc(Idx);
+      end;
+  until (aTSP.Offset=-1);
+  SetLength(Result,Idx);
+end;
+
+{ TTextMeasurer }
+
+constructor TTextMeasurer.Create(aLayouter: TTextLayouter);
+begin
+  FLayouter:=aLayouter;
+end;
+
+procedure TTextMeasurer.SetFont(const aFont: TTextFont);
+begin
+  With aFont do
+    SetFont(Name,Size,Attrs);
+  FWhiteSpaceWidth:=0;
+end;
+
+function TTextMeasurer.WhitespaceWidth: TTextUnits;
+begin
+  if FWhiteSpaceWidth=0 then
+    FWhitespaceWidth:=MeasureText(' ').Width;
+  Result:=FWhitespaceWidth;
+end;
+
+{ TFixedSizeTextMeasurer }
+
+constructor TFixedSizeTextMeasurer.Create(aLayouter: TTextLayouter);
+begin
+  Inherited;
+  CharWidth:=8;
+  CharHeight:=12;
+end;
+
+procedure TFixedSizeTextMeasurer.SetFont(const aFontName: String; aSize: SmallInt; aAttrs: TFontAttributes);
+begin
+  FSize:=aSize;
+  FFontName:=aFontName;
+  FAttrs:=aAttrs;
+end;
+
+function TFixedSizeTextMeasurer.MeasureText(aText: String): TTextMeasures;
+
+var
+  Scale: TTextUnits;
+begin
+  Scale:=(Size/12);
+  Result.Width:=Length(aText) * CharWidth * Scale;
+  Result.Height:=CharHeight * Scale;
+  Result.Descender:=0;
+end;
+
+{ TTextSplitPoint }
+
+constructor TTextSplitPoint.Create(aOffset, aWhiteSpace: SizeInt);
+begin
+  offSet:=aOffset;
+  whitespace:=aWhiteSpace;
+end;
+
+{ TTextFont }
+
+function TTextFont.GetColor: TColor;
+begin
+  Result:=FPColorToColor(FColor);
+end;
+
+procedure TTextFont.SetAttrs(AValue: TFontAttributes);
+begin
+  if FAttrs=AValue then Exit;
+  FAttrs:=AValue;
+end;
+
+procedure TTextFont.SetColor(AValue: TColor);
+begin
+  FColor:=ColorToFPColor(aValue);
+end;
+
+procedure TTextFont.SetFPColor(AValue: TFPColor);
+begin
+  if FColor=AValue then Exit;
+  FColor:=AValue;
+  Changed;
+end;
+
+procedure TTextFont.SetName(AValue: string);
+begin
+  if FName=AValue then Exit;
+  FName:=AValue;
+  Changed;
+end;
+
+procedure TTextFont.SetSize(AValue: Smallint);
+begin
+  if FSize=AValue then Exit;
+  FSize:=AValue;
+  Changed;
+end;
+
+procedure TTextFont.Assign(Source: TPersistent);
+var
+  aSource: TTextFont absolute source;
+begin
+  if Source is TTextFont then
+    begin
+    FSize:=aSource.FSize;
+    FOwner:=aSource.FOwner;
+    FName:=aSource.FName;
+    FColor:=aSource.FColor;
+    FAttrs:=aSource.FAttrs;
+    end
+  else
+    inherited Assign(Source);
+end;
+
+constructor TTextFont.Create(aOwner: TPersistent);
+begin
+  FOwner:=aOwner;
+end;
+
+procedure TTextFont.Changed;
+begin
+  if (FOwner is TTextLayouter) then
+    TTextLayouter(FOwner).Reset;
+end;
+
+function TTextFont.Clone(aOwner : TPersistent): TTextFont;
+
+begin
+  Result:=TTextFontClass(Self.ClassType).Create(aOwner);
+  Result.Assign(Self);
+end;
+
+{ TTextLayouter }
+
+procedure TTextLayouter.SetCullTreshold(AValue: TCullThreshold);
+begin
+  if FCullTreshold=AValue then Exit;
+  FCullTreshold:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetFont(AValue: TTextFont);
+begin
+  if FFont=AValue then Exit;
+  FFont.Assign(AValue);
+  Reset;
+end;
+
+procedure TTextLayouter.SetHorizontalAlign(AValue: TTextAlign);
+begin
+  if FHorizontalAlign=AValue then Exit;
+  FHorizontalAlign:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetHyphenationChar(AValue: String);
+begin
+  if FHyphenationChar=AValue then Exit;
+  FHyphenationChar:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetLineSpacing(AValue: TTextUnits);
+begin
+  if FLineSpacing=AValue then Exit;
+  FLineSpacing:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetBounds(AValue: TTextLayoutBounds);
+begin
+  if FBounds=AValue then Exit;
+  FBounds.Assign(AValue);
+  Reset;
+end;
+
+procedure TTextLayouter.SetMaxStretch(AValue: TTextUnits);
+begin
+  if FMaxStretch=AValue then Exit;
+  FMaxStretch:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetRanges(AValue: TTextRangeList);
+begin
+  if FRanges=AValue then Exit;
+  FRanges:=AValue;
+  Reset;
+end;
+
+function TTextLayouter.GetBlock(aIndex : Integer): TTextBlock;
+begin
+  Result:=TTextBlock(FBlocks[aIndex]);
+end;
+
+function TTextLayouter.GetBlockCount: Integer;
+begin
+  Result:=FBlocks.Count;
+end;
+
+function TTextLayouter.GetColor: TFPColor;
+begin
+  Result:=FFont.FPColor;
+end;
+
+procedure TTextLayouter.SetAllowHyphenation(AValue: Boolean);
+begin
+  if FAllowHyphenation=AValue then Exit;
+  FAllowHyphenation:=AValue;
+  Reset;
+end;
+
+
+procedure TTextLayouter.SetStretchMode(AValue: TStretchMode);
+begin
+  if FStretchMode=AValue then Exit;
+  FStretchMode:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetText(AValue: string);
+begin
+  if FText=AValue then Exit;
+  FText:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetTextRanges(AValue: TTextRangeList);
+begin
+  if FTextRanges=AValue then Exit;
+  FTextRanges.Assign(AValue);
+  Reset;
+end;
+
+procedure TTextLayouter.SetVerticalAlign(AValue: TTextAlign);
+begin
+  if FVerticalAlign=AValue then Exit;
+  FVerticalAlign:=AValue;
+end;
+
+procedure TTextLayouter.SetWordOverFlow(AValue: TWordOverflow);
+begin
+  if FWordOverFlow=AValue then Exit;
+  FWordOverFlow:=AValue;
+  Reset;
+end;
+
+procedure TTextLayouter.SetWordWrap(AValue: Boolean);
+begin
+  if FWordWrap=AValue then Exit;
+  FWordWrap:=AValue;
+  Reset;
+end;
+
+class function TTextLayouter.CreateMeasurer(aLayouter: TTextLayouter): TTextMeasurer;
+
+var
+  aClass : TTextMeasurerClass;
+
+begin
+  aClass:=_TextMeasurerClass;
+  if aClass=Nil then
+    aClass:=TFixedSizeTextMeasurer;
+  Result:=aClass.Create(aLayouter);
+end;
+
+class function TTextLayouter.CreateSplitter(aLayouter: TTextLayouter): TTextSplitter;
+
+var
+  aclass  : TTextSplitterClass;
+
+begin
+  aClass:=_TextSplitterClass;
+  if aClass=Nil then
+    aClass:=TTextSplitter;
+  Result:=aClass.Create(aLayouter);
+end;
+
+constructor TTextLayouter.Create(aOwner: TComponent);
+begin
+  Inherited;
+  FBlocks:=TTextBlockList.Create(True);
+  FBounds:=TTextLayoutBounds.Create(Self);
+  FFont:=TTextFont.Create(Self);
+  FRanges:=CreateRanges(Self);
+  FMeasurer:=CreateMeasurer(Self);
+  FSplitter:=CreateSplitter(Self);
+  FLineSpacing:=1.0;
+  HyphenationChar:='-';
+  AllowHyphenation:=False;
+end;
+
+class function TTextLayouter.CreateRanges(aLayouter : TTextLayouter) : TTextRangeList;
+
+var
+  aClass : TTextRangeClass;
+
+begin
+  aClass:=_TextRangeClass;
+  if aClass=Nil then
+    aClass:=TTextRange;
+  Result:=TTextRangeList.Create(aLayouter,aClass);
+end;
+
+class function TTextLayouter.CreateBlock(aLayouter: TTextLayouter; aOffset, aLength: SizeInt): TTextBlock;
+var
+  aClass : TTextBlockClass;
+
+begin
+  aClass:=_TextBlockClass;
+  if aClass=Nil then
+    aClass:=TTextBlock;
+  Result:=aClass.Create(aLayouter,aOffset,aLength);
+end;
+
+destructor TTextLayouter.Destroy;
+begin
+  FreeAndNil(FBounds);
+  FreeAndNil(FFont);
+  FreeAndNil(FBlocks);
+  FreeAndNil(FRanges);
+  FreeAndNil(FMeasurer);
+  FreeAndNil(FSplitter);
+  inherited Destroy;
+end;
+
+procedure TTextLayouter.Reset;
+begin
+  FBlocks.Clear;
+end;
+
+function TTextLayouter.ToString: RTLString;
+var
+  I : Integer;
+begin
+  Result:='';
+  For I:=0 to TextBlockCount-1 do
+    Result:=Result+TextBlocks[I].ToString+sLineBreak;
+end;
+
+function TTextLayouter.AddBlock(aOffset,aLength : SizeInt; aFont : TTextFont) : TTextBlock;
+
+begin
+  Result:=CreateBlock(Self,aOffset,aLength);
+  Result.Font:=aFont;
+  FBlocks.Add(Result);
+end;
+
+function OffsetRange(Item1, Item2: TCollectionItem): Integer;
+begin
+  Result:=TTextRange(Item1).CharOffset-TTextRange(Item2).CharOffset;
+end;
+
+procedure TTextLayouter.CheckRanges;
+
+var
+  I,rMax : Integer;
+  R,RN : TTextRange;
+
+begin
+  Ranges.Sort(@OffsetRange);
+  If Ranges.Count=1 then
+    exit;
+  R:=Ranges[0];
+  for I:=1 to Ranges.Count-1 do
+    begin
+    RN:=Ranges[i];
+    rMax:=RN.CharOffset-R.CharOffset;
+    if R.CharLength>rMax then
+      begin
+      if OverlappingRangesAction=oraError then
+        Raise ETextLayout.CreateFmt(SErrOverlappingRanges,[R.ToString,RN.ToString]);
+      R.CharLength:=rMax;
+      end;
+    R:=RN;
+    end;
+end;
+
+
+procedure TTextLayouter.HandleRanges;
+
+var
+  I,LastOff,AddLen,MaxLen : Integer;
+  R : TTextRange;
+
+
+begin
+  MaxLen:=Length(Text);
+  if Ranges.Count=0 then
+    AddBlock(0,MaxLen,Font)
+  else
+    begin
+    CheckRanges;
+    LastOff:=0;
+    for I:=0 to Ranges.Count-1 do
+      begin
+      R:=Ranges[i];
+      if R.CharOffset>LastOff then
+        AddBlock(LastOff,R.CharOffset-LastOff,Self.Font);
+      if R.CharOffset<MaxLen then
+        begin
+        AddLen:=R.CharLength;
+        if R.CharOffset+AddLen>=MaxLen then
+          AddLen:=MaxLen-R.CharOffSet;
+        AddBlock(R.CharOffset,AddLen,R.Font);
+        LastOff:=R.CharOffset+AddLen;
+        end;
+      end;
+    If LastOff<MaxLen then
+      AddBlock(LastOff,MaxLen-LastOff,Self.Font);
+    end;
+end;
+
+procedure TTextLayouter.SetColor(AValue: TFPColor);
+begin
+  Font.FPColor:=aValue;
+end;
+
+procedure TTextLayouter.HandleNewLines;
+
+var
+  i : Integer;
+  aPos : sizeInt;
+  SplitPos : TTextSplitPoint;
+  B,BN : TTextBlock;
+  T : String;
+
+begin
+  I:=0;
+  While (I<FBlocks.Count) do
+    begin
+    B:=FBlocks[i];
+    Repeat
+      T:=B.Text;
+      SplitPos:=Splitter.GetNextNewLine(Text,1+B.TextOffset);
+      if SplitPos.Offset<>-1 then
+        begin
+        aPos:=Splitpos.offset+Splitpos.whitespace;
+        BN:=B.Split(aPos);
+        T:=BN.Text;
+        BN.ForceNewLine:=True;
+        B.TextLen:=B.TextLen-SplitPos.WhiteSpace;
+        B.TrimTrailingWhiteSpace;
+        T:=B.Text;
+        inc(I);
+        FBlocks.Insert(I,BN);
+        B:=BN;
+        end;
+    until SplitPos.Offset=-1;
+    Inc(I);
+    end;
+end;
+
+// Returns true if the line is full.
+function TTextLayouter.FindWrapPosition(B : TTextBlock; S : String; var aPos : integer; var CurrPos : TTextPoint) : Boolean;
+
+var
+  lSplit : TTextSplitPoint;
+  lSize : TTextMeasures;
+  wSpace : TTextUnits;
+  BlockWidth: TTextUnits;
+  CurrPart : String;
+  maxLen : integer;
+  useHyphen : Boolean;
+
+begin
+  maxLen:=Length(S);
+  BlockWidth:=0;
+  UseHyphen:=False;
+  Repeat
+    lSplit:=Splitter.GetNextSplitPoint(S,aPos,UseHyphen);
+    CurrPart:=Copy(S,aPos,lSplit.offset-aPos+1);
+    if UseHyphen then
+      CurrPart:=CurrPart+HyphenationChar;
+    Writeln('Curr : >',CurrPart,'<');
+    lSize:=Measurer.MeasureText(CurrPart);
+    Result:=CurrPos.X+lSize.Width>=Bounds.Width;
+    if not Result then
+      begin
+      // CurrPart still fits on Line, add it
+      BlockWidth:=BlockWidth+lSize.Width;
+      CurrPos.x:=CurrPos.X+lSize.Width;
+      // Update pos for GetNextSplitPoint.
+      aPos:=lSplit.Offset+lSplit.whitespace+1;
+      // Check if the whitespace would flow over:
+      WSpace:=lSplit.whitespace*Measurer.WhitespaceWidth;
+      Result:=(CurrPos.X+WSpace)>=Bounds.Width;
+      if UseHyphen then
+        B.Suffix:=HyphenationChar;
+      if not Result then
+        CurrPos.X:=CurrPos.X+WSpace;
+      end
+    else
+      begin
+      // Currpart will no longer fit on the line. Attempt splitting, if we were not yet splitting.
+      if (not UseHyphen) and AllowHyphenation then
+        begin
+        Result:=False;
+        UseHyphen:=True;
+        end
+      else
+        // One word and it does not fit...
+        if aPos=1 then
+          aPos:=MaxLen
+      end;
+  until Result or (aPos>=MaxLen);
+end;
+
+function TTextLayouter.WrapBlock(B: TTextBlock; S: String; var Idx: integer; var CurrPos: TTextPoint): Boolean;
+
+var
+  aPosOffset,aPos,MaxLen: integer;
+  LineFull : Boolean;
+  NB : TTextBlock;
+  T : String;
+
+begin
+  Result:=False;
+  aPos:=1;
+  aPosOffset:=1;
+  maxLen:=Length(S);
+  // We can have multiple lines
+  Repeat
+    B.LayoutPos:=CurrPos;
+    LineFull:=FindWrapPosition(B,S,aPos,CurrPos);
+    // At this point, aPos is the maximum size that will fit.
+    if aPos>=MaxLen then
+      begin
+      // Correct size.
+      B.Size:=Measurer.MeasureText(B.Text);
+      end
+    else
+      // We're not yet at the end, so we need to split
+      begin
+      Result:=True;
+      NB:=B.Split(aPos-aPosOffset);
+      T:=NB.Text;
+      // Writeln('T new: >>',T,'<<');
+      B.TrimTrailingWhiteSpace;
+      T:=B.Text;
+      // Writeln('T old: >>',T,'<<');
+      B.Size:=Measurer.MeasureText(T);
+      Inc(Idx);
+      FBlocks.Insert(Idx,NB);
+      NB.ForceNewLine:=True;
+      aPosOffset:=NB.TextOffset;
+      if LineFull then
+        begin
+        CurrPos.X:=0;
+        CurrPos.Y:=CurrPos.Y+B.Size.Height+LineSpacing;
+        end;
+      B:=NB;
+      end;
+  until (aPos>=MaxLen);
+end;
+
+function TTextLayouter.WrapLayout : Boolean;
+
+var
+  CurrPos : TTextPoint; // value in pixels
+  i: integer;
+  lSize : TTextMeasures;
+  B : TTextBlock;
+  lText : TTextString;
+
+begin
+  Result:=False;
+  CurrPos:=Pointf(0,0);
+  I:=0;
+  While I<FBlocks.Count do
+    begin
+    B:=FBlocks[i];
+    if B.ForceNewLine then
+      CurrPos.X:=0;
+    lText:=B.Text;
+    Measurer.SetFont(B.Font);
+    lSize:=Measurer.MeasureText(lText);
+    if CurrPos.X+lSize.Width>Bounds.Width then
+       Result:=WrapBlock(B,lText,I,CurrPos) or Result;
+    inc(I);
+    end;
+  B:=FBlocks[FBlocks.Count-1];
+end;
+
+function TTextLayouter.NoWrapLayout: Boolean;
+
+var
+  CurrPos : TTextPoint;
+  CurrHeight : TTextUnits;
+  i: integer;
+  B : TTextBlock;
+  lText : TTextString;
+
+begin
+  Result:=False;
+  CurrPos.X:=0;
+  CurrPos.Y:=0;
+  CurrHeight:=0;
+  I:=0;
+  While I<FBlocks.Count do
+    begin
+    B:=FBlocks[i];
+    if B.ForceNewLine then
+      begin
+      CurrPos.X:=0;
+      CurrPos.Y:=CurrPos.Y+CurrHeight+LineSpacing;
+      CurrHeight:=0;
+      Result:=True;
+      end;
+    lText:=B.Text;
+    Measurer.SetFont(B.Font);
+    B.Size:=Measurer.MeasureText(lText);
+    B.LayoutPos:=CurrPos;
+    // Shift pos
+    CurrPos.X:=CurrPos.X+B.Width;
+    if B.Height>CurrHeight then
+      CurrHeight:=B.Height;
+    inc(I);
+    end;
+end;
+
+
+function TTextLayouter.Execute: integer;
+
+  function LineCount : integer;
+
+  var
+    I : integer;
+
+  begin
+    Result:=1;
+    For I:=0 to FBlocks.Count-1 do
+      If FBlocks[i].ForceNewLine then
+        Inc(Result);
+  end;
+
+  Function CalcNeededHeight : TTextUnits;
+
+  var
+    I : Integer;
+    NewH : TTextUnits;
+
+  begin
+    Result:=0;
+    For I:=0 to TextBlockCount-1 do
+      begin
+      With TextBlocks[i] do
+        NewH:=LayoutPos.Y+Size.Height;
+      if NewH>Result then
+        Result:=NewH;
+      end;
+  end;
+
+begin
+  Reset;
+  HandleRanges;
+  HandleNewLines;
+  if WordWrap then
+    WrapLayout
+  else
+    NoWrapLayout;
+  if StretchMode = TStretchMode.smDontStretch then
+    CullTextOutOfBoundsVertically
+  else
+    ApplyStretchMode(CalcNeededHeight);
+  // We do this after vertical culling, potentially less blocks...
+  CullTextOutOfBoundsHorizontally;
+  ApplyVertTextAlignment;
+  ApplyHorzTextAlignment;
+  Result:=TextBlockCount;
+  Writeln('Result : ',ToString);
+end;
+
+function TTextLayouter.Execute(const aText: String): Integer;
+
+begin
+  Text:=aText;
+  Result:=Execute;
+end;
+
+procedure TTextLayouter.ApplyStretchMode(const ADesiredHeight: TTextUnits);
+
+begin
+  Case StretchMode of
+    smDontStretch:
+      ;
+    TStretchMode.smMaxHeight:
+      begin
+      Bounds.Height:=MaxStretch;
+      end;
+    TStretchMode.smActualHeight:
+      begin
+      Bounds.Height := aDesiredHeight;
+      end;
+    TStretchMode.smActualHeightStretchOnly:
+      begin
+      if aDesiredHeight>Bounds.Height then { only grow height if needed. We don't shrink. }
+        Bounds.Height := aDesiredHeight;
+      end;
+    TStretchMode.smActualHeightShrinkOnly:
+      begin
+      if aDesiredHeight<Bounds.Height then { only shrink height if needed. We don't grow. }
+        Bounds.Height := ADesiredHeight;
+      end;
+    end;
+end;
+
+
+procedure TTextLayouter.CullTextOutOfBoundsVertically;
+
+var
+  i: integer;
+  lBlock: TTextBlock;
+  MaxHeight, vPos : TTextUnits;
+  lRemainingHeight: single;
+  d: single;
+  doDelete : Boolean;
+  aSize : TTextMeasures;
+
+begin
+  MaxHeight:=Bounds.Height;
+  for i := FBlocks.Count-1 downto 0 do
+    begin
+    lBlock := FBlocks[i];
+    // completely out of bounds ?
+    vPos := lBlock.LayoutPos.y;
+    doDelete := (vPos >= MaxHeight);
+    aSize:=lBlock.Size;
+    // partially out of bounds ?
+    if not DoDelete and ((vPos + aSize.Height + aSize.Descender) > MaxHeight) then
+      begin
+      lRemainingHeight := (MaxHeight - vPos);
+      { calculate % of text [height] that falls inside the bounderies of the Memo. }
+      d := (lRemainingHeight / (aSize.Height + aSize.Descender)) * 100;
+      {$IFDEF gDEBUG}
+      writeln(Format('Memo Culling: %2.2f%% of line height is visible', [d]));
+      {$ENDIF}
+      DoDelete:=CullThreshold > d;
+      end;
+    if DoDelete then
+      FBlocks.Delete(i);
+    end;
+end;
+
+function TTextLayouter.FindLastFittingCharPos(B: TTextBlock; const aSuffix: String; out aWidth: TTextUnits): Integer;
+
+var
+  lWidth,SuffWidth,avgWidth,aMaxWidth : TTextUnits;
+  aStart,aEnd,aPivot : Integer;
+
+begin
+  SuffWidth:=Measurer.MeasureText(aSuffix).Width;
+  aMaxWidth:=Bounds.Width-SuffWidth;
+  // Get a starting point
+  AvgWidth:=B.Width/B.TextLen;
+  aStart:=0;
+  aEnd:=B.Textlen;
+  aPivot:=Round(Bounds.Width/AvgWidth);
+  lWidth:=Measurer.MeasureText(Copy(B.Text,1,aPivot)).Width;
+  While (aStart<=aEnd) do
+    begin
+    if lWidth=aMaxWidth then
+      aStart:=aEnd+1
+    else
+      begin
+      if lWidth>aMaxWidth then
+        aEnd:=aPivot-1
+      else
+        aStart:=aPivot+1;
+      aPivot:=(aEnd+aStart) div 2;
+      end;
+    lWidth:=Measurer.MeasureText(Copy(B.Text,1,aPivot)).Width;
+    end;
+  Result:=aPivot;
+  aWidth:=lWidth+SuffWidth;
+end;
+
+procedure TTextLayouter.CullTextHorizontally(B : TTextBlock);
+
+var
+  P : Integer;
+  Suff : String;
+  Width : TTextUnits;
+
+begin
+  if WordOverflow=TWordOverflow.woOverflow then
+    exit;
+  Suff:='';
+  Case WordOverflow of
+    TWordOverflow.woOverflow: ; // Silence compiler warning
+    TWordOverflow.woTruncate:
+      begin
+      P:=FindLastFittingCharPos(B,'',Width);
+      end;
+    TWordOverflow.woEllipsis:
+      begin
+      {$IF SIZEOF(CHAR)=2}
+      P:=FindLastFittingCharPos(B,cEllipsis);
+      Suff:=cEllipsis;
+      {$ELSE}
+      P:=FindLastFittingCharPos(B,UTF8Encode(cEllipsis),Width);
+      Suff:=UTF8Encode(cEllipsis);
+      {$ENDIF}
+      end;
+    TWordOverflow.woAsterisk:
+      begin
+      P:=FindLastFittingCharPos(B,'*',Width);
+      Suff:='*';
+      end;
+  end;
+  B.TextLen:=P;
+  B.Suffix:=Suff;
+  B.Size.Width:=Width;
+end;
+
+procedure TTextLayouter.CullTextOutOfBoundsHorizontally;
+
+var
+  B : TTextBlock;
+  i : Integer;
+
+begin
+  For I:=0 to TextBlockCount-1 do
+    begin
+    B:=TextBlocks[I];
+    if (B.LayoutPos.X+B.Size.Width>Bounds.Width) then
+      CullTextHorizontally(B);
+    end;
+end;
+
+{ this affects only X coordinate of text blocks }
+procedure TTextLayouter.ApplyHorzTextAlignment;
+
+var
+  i: integer;
+  tb: TTextBlock;
+  lList: TFPList;
+  lLastYPos: TTextUnits;
+
+  procedure ProcessRightJustified;
+  var
+    idx: integer;
+    b: TTextBlock;
+    lXOffset: TTextUnits;
+  begin
+    lXOffset := Bounds.Width;
+    for idx := lList.Count-1 downto 0 do
+    begin
+      b := TTextBlock(lList[idx]);
+      b.LayoutPos.X := lXOffset - b.Size.Width;
+      lXOffset := b.LayoutPos.X;
+    end;
+  end;
+
+  procedure ProcessCentered;
+  var
+    idx: integer;
+    b: TTextBlock;
+    lXOffset: TTextUnits;
+    lTotalWidth: TTextUnits;
+  begin
+    lTotalWidth := 0;
+    for idx := 0 to lList.Count-1 do
+    begin
+      b := TTextBlock(lList[idx]);
+      lTotalWidth := lTotalWidth + b.Width;
+    end;
+    lXOffset := (Bounds.Width - lTotalWidth) / 2;
+    if lXOffset < 0.0 then { it should never be, but lets play it safe }
+      lXOffset := 0.0;
+    for idx := 0 to lList.Count-1 do
+    begin
+      b := TTextBlock(lList[idx]);
+      b.LayoutPos.X := lXOffset;
+      lXOffset := lXOffset + b.Width;
+    end;
+  end;
+
+  (* TODO : Justify
+  // This requires splitting the blocks into words
+  procedure ProcessWidth;
+  var
+    idx: integer;
+    b: TTextBlock;
+    lXOffset: TTextUnits;
+    lSpace: TTextUnits;
+    lTotalWidth: TTextUnits;
+  begin
+
+    lTotalWidth := 0;
+    for idx := 0 to lList.Count-1 do
+    begin
+      b := TTextBlock(lList[idx]);
+      lTotalWidth := lTotalWidth + b.Width;
+    end;
+    lSpace := (Bounds.Width - lTotalWidth) / (lList.Count-1);
+    { All the text blocks must move by LeftMargin to the right. }
+    lXOffset := Padding.Right;
+    for idx := 0 to lList.Count-1 do
+    begin
+      b := TTextBlock(lList[idx]);
+      b.Pos.X := lXOffset;
+      lXOffset := lXOffset + b.Width + lSpace;
+    end;
+  end;
+  *)
+
+begin
+  lList := TFPList.Create;
+  try
+  lLastYPos := 0;
+  i := 0;
+  While I<FBlocks.Count do
+  begin
+    tb := FBlocks[i];
+    if tb.LayoutPos.Y = lLastYPos then // still on the same text line
+      lList.Add(tb)
+    else
+    begin
+      { a new line has started - process what we have collected in lList }
+      case HorizontalAlign of
+        TTextAlign.Leading:   ; // Nothing to do
+        TTextAlign.Trailing:  ProcessRightJustified;
+        TTextAlign.Center:    ProcessCentered;
+        // taWidth:           ProcessWidth;
+      end;
+      lList.Clear;
+      lLastYPos := tb.LayoutPos.Y;
+      lList.Add(tb)
+    end; { if..else }
+    inc(I);
+  end; { while i<fblocks.count }
+
+  { process the last text line's items }
+  if lList.Count > 0 then
+  begin
+    case HorizontalAlign of
+      TTextAlign.Leading:   ; // Nothing to do
+      TTextAlign.Trailing:  ProcessRightJustified;
+      TTextAlign.Center:    ProcessCentered;
+      // taWidth:           ProcessWidth;
+    end;
+  end;
+
+  finally
+    llist.Free;
+  end;
+end;
+
+{ this affects only Y coordinate of text blocks }
+procedure TTextLayouter.ApplyVertTextAlignment;
+var
+  i: integer;
+  tb: TTextBlock;
+  lList: TFPList;
+  lLastYPos: TTextUnits;
+  lTotalHeight: TTextUnits;
+  lYOffset: TTextUnits;
+
+  procedure ProcessTop;
+  var
+    idx: integer;
+    b: TTextBlock;
+  begin
+    if lList.Count = 0 then
+      Exit;
+    for idx := 0 to lList.Count-1 do
+    begin
+      b := TTextBlock(lList[idx]);
+      b.LayoutPos.Y := lYOffset;
+    end;
+    lYOffset := lYOffset + LineSpacing + b.Height + b.Descender;
+  end;
+
+  procedure ProcessCenter;
+  var
+    idx: integer;
+    b: TTextBlock;
+  begin
+    for idx := 0 to lList.Count-1 do
+    begin
+      b := TTextBlock(lList[idx]);
+      b.LayoutPos.Y := lYOffset;
+    end;
+    lYOffset := lYOffset + LineSpacing + b.Height + b.Descender;
+  end;
+
+  procedure ProcessBottom;
+  var
+    idx: integer;
+    b: TTextBlock;
+  begin
+    for idx := 0 to lList.Count-1 do
+    begin
+      b := TTextBlock(lList[idx]);
+      b.LayoutPos.Y := lYOffset;
+    end;
+    lYOffset := lYOffset - LineSpacing - b.Height - b.Descender;
+  end;
+
+begin
+  if FBlocks.Count = 0 then
+    Exit;
+  lList := TFPList.Create;
+  try
+    lLastYPos := FBlocks[FBlocks.Count-1].LayoutPos.Y;  // last textblock's Y coordinate
+    lTotalHeight := 0;
+
+    Case VerticalAlign of
+    TTextAlign.Leading:
+      begin
+      // Nothing to do
+      end;
+
+    TTextAlign.Trailing:
+      begin
+      lYOffset := Bounds.Height;
+      for i := FBlocks.Count-1 downto 0 do
+        begin
+        tb := FBlocks[i];
+        if i = FBlocks.Count-1 then
+          lYOffset := lYOffset - tb.Height - tb.Descender;  // only need to do this for one line
+        if tb.LayoutPos.Y = lLastYPos then // still on the same text line
+          lList.Add(tb)
+        else
+          begin
+          { a new line has started - process what we have collected in lList }
+          ProcessBottom;
+
+          lList.Clear;
+          lLastYPos := tb.LayoutPos.Y;
+          lList.Add(tb)
+          end; { if..else }
+        end; { for i }
+      end; // TTextAlign.Trailing:
+
+    TTextAlign.Center:
+      begin
+      { First, collect the total height of all the text lines }
+      lTotalHeight := 0;
+      lLastYPos := 0;
+      for i := 0 to FBlocks.Count-1 do
+        begin
+        tb := FBlocks[i];
+        if i = 0 then  // do this only for the first block
+          lTotalHeight := tb.Height + tb.Descender;
+        if tb.LayoutPos.Y = lLastYPos then // still on the same text line
+          Continue
+        else
+          begin
+          { a new line has started - process what we have collected in lList }
+          lTotalHeight := lTotalHeight + LineSpacing + tb.Height + tb.Descender;
+          end; { if..else }
+        lLastYPos := tb.LayoutPos.Y;
+        end; { for i }
+
+      { Now process them line-by-line }
+      lList.Clear;
+      lYOffset := (Bounds.Height - lTotalHeight) / 2;
+      lLastYPos := 0;
+      for i := 0 to FBlocks.Count-1 do
+        begin
+        tb := FBlocks[i];
+        if tb.LayoutPos.Y = lLastYPos then // still on the same text line
+          lList.Add(tb)
+        else
+        begin
+          { a new line has started - process what we have collected in lList }
+          ProcessCenter;
+
+          lList.Clear;
+          lLastYPos := tb.LayoutPos.Y;
+          lList.Add(tb)
+        end; { if..else }
+        end; { for i }
+      end; // TTextAlign.Center
+    end; // Case
+
+    { process the last text line's items }
+    if lList.Count > 0 then
+    begin
+      case VerticalAlign of
+        TTextAlign.Leading:  ProcessTop;
+        TTextAlign.Center:   ProcessCenter;
+        TTextAlign.Trailing: ProcessBottom;
+      end;
+    end;
+  finally
+    lList.Free;
+  end;
+end;
+
+
+end.
+

+ 4 - 5
src/base/fresnelbase.lpk

@@ -14,11 +14,6 @@
           <AllowLabel Value="False"/>
         </SyntaxOptions>
       </Parsing>
-      <Linking>
-        <Debugging>
-          <GenerateDebugInfo Value="False"/>
-        </Debugging>
-      </Linking>
     </CompilerOptions>
     <Description Value="The abstract Fresnel Framework - providing CSS components.
 "/>
@@ -107,6 +102,10 @@
         <AddToUsesPkgSection Value="False"/>
         <UnitName Value="fpcssutils"/>
       </Item>
+      <Item>
+        <Filename Value="fresnel.textlayouter.pas"/>
+        <UnitName Value="Fresnel.TextLayouter"/>
+      </Item>
     </Files>
     <UsageOptions>
       <UnitPath Value="$(PkgOutDir)"/>

+ 1 - 1
src/base/fresnelbase.pas

@@ -9,7 +9,7 @@ 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.Resources, Fresnel.StrConsts, Fresnel.Classes, Fresnel.Images, UTF8Utils, Fresnel.AsyncCalls, Fresnel.TextLayouter;
 
 implementation
 

+ 13 - 6
tests/base/TCFresnelCSS.pas

@@ -27,15 +27,17 @@ type
   TTestFont = class(TInterfacedObject,IFresnelFont)
   public
     Desc: TFresnelFontDesc;
+    function GetDescription: String;
     function GetFamily: string;
     function GetKerning: string;
-    function GetSize: string;
+    function GetSize: double;
     function GetStyle: string;
     function GetVariant: string;
-    function GetWeight: string;
+    function GetWeight: double;
     function TextSize(const aText: string): TFresnelPoint;
     function TextSizeMaxWidth(const aText: string; MaxWidth: TFresnelLength): TFresnelPoint;
     function GetTool: TObject;
+
   end;
 
   { TTestFontEngine }
@@ -228,12 +230,17 @@ begin
   Result:=Desc.Family;
 end;
 
+function TTestFont.GetDescription: String;
+begin
+  Result:=Desc.Family;
+end;
+
 function TTestFont.GetKerning: string;
 begin
   Result:=Desc.Kerning;
 end;
 
-function TTestFont.GetSize: string;
+function TTestFont.GetSize: Double;
 begin
   Result:=Desc.Size;
 end;
@@ -248,7 +255,7 @@ begin
   Result:=Desc.Variant_;
 end;
 
-function TTestFont.GetWeight: string;
+function TTestFont.GetWeight: double;
 begin
   Result:=Desc.Weight;
 end;
@@ -282,9 +289,9 @@ var
   end;
 
 begin
-  aSize:=StrToFloat(Desc.Size);
+  aSize:=Desc.Size;
   if aSize<0 then
-    raise EFresnelFont.Create('font size negative "'+Desc.Size+'"');
+    raise EFresnelFont.CreateFmt('font size negative "%g"',[Desc.Size]);
   Result.X:=0;
   Result.Y:=0;
   if (aText='') or (SameValue(aSize,0)) then

+ 4 - 0
tests/base/TestFresnelBase.lpi

@@ -70,6 +70,10 @@
         <IsPartOfProject Value="True"/>
         <UnitName Value="TCFresnelImages"/>
       </Unit>
+      <Unit>
+        <Filename Value="tctextlayout.pas"/>
+        <IsPartOfProject Value="True"/>
+      </Unit>
     </Units>
   </ProjectOptions>
   <CompilerOptions>

+ 1 - 1
tests/base/TestFresnelBase.lpr

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

+ 890 - 0
tests/base/tctextlayout.pas

@@ -0,0 +1,890 @@
+unit tctextlayout;
+
+{$mode objfpc}{$H+}
+
+interface
+
+uses
+  Classes, SysUtils, types, fpcunit, testutils, testregistry, fpimage, fresnel.textlayouter;
+
+const
+  cHeight = 15;
+  cWidth  = 10;
+  cFontSize = 12;
+  cFontName = 'Arial';
+  cFontAttr : TFontAttributes = [];
+  cDelta  = 0.01;
+  cLineSpacing = 2;
+
+type
+
+  { TTestLayouter }
+
+  TTestLayouter = class(TTextLayouter)
+  public
+    class function CreateMeasurer(aLayouter: TTextLayouter): TTextMeasurer; override;
+    class function CreateSPlitter(aLayouter: TTextLayouter): TTextSplitter; override;
+  end;
+
+  { TFixedListSplitter }
+
+  TFixedListSplitter = class(TTextSplitter)
+  public
+    Function GetNextSplitPoint(const aText : TTextString; aStartPos : SizeInt; aAllowHyphen : Boolean) : TTextSplitPoint; override;
+  end;
+
+  { TLayoutTestCase }
+
+  TLayoutTestCase = class(TTestCase)
+  private
+    FLayouter: TTextLayouter;
+    FFont : TTextFont;
+  protected
+    procedure SetUp; override;
+    procedure TearDown; override;
+    function CreateTextBlock(const aOffset, aLength: SizeInt): TTextBlock;
+    class procedure AssertEquals(aMsg : String; aExpected,aActual : TTextSplitPoint); overload;
+    property Layouter : TTextLayouter Read FLayouter;
+    Property Font : TTextFont Read FFont;
+  end;
+
+  { TTestSplitter }
+
+  TTestSplitter= class(TLayoutTestCase)
+  private
+    FSplitter: TTextSplitter;
+  protected
+    procedure SetUp; override;
+    procedure TearDown; override;
+    procedure CheckPoints(Msg : String; aRes : TTextSplitPointArray; aPoints : Array of TTextSplitPoint);
+    procedure TestSplit(Msg : String; aText : TTextString; aStartPos : SizeInt; aPoints : Array of TTextSplitPoint);
+    procedure TestNewLineSplit(Msg : String; aText : TTextString; aStartPos : SizeInt; aPoints : Array of TTextSplitPoint);
+    Property Splitter : TTextSplitter Read FSplitter;
+  published
+    procedure TestHookUp;
+    procedure TestOneWord;
+    procedure Test2Words;
+    procedure Test3Words;
+    procedure Test4Words;
+    procedure TestLineBreak;
+    procedure Test2LineBreaks;
+    procedure Test2LineBreakChars;
+  end;
+
+  { TTestTextBlock }
+
+  TTestTextBlock = class(TLayoutTestCase)
+  private
+    FTextBlock: TTextBlock;
+    procedure FakeLayoutBlock(aBlock: TTextBlock);
+  Protected
+    Procedure SetUp; override;
+    Procedure TearDown; override;
+    Property Block : TTextBlock Read FTextBlock;
+  Published
+    procedure TestHookUp;
+    procedure TestAssign;
+    procedure TestSplit;
+    procedure TestSplit2;
+  end;
+
+  { TTestTextLayouter }
+
+  TTestTextLayouter = class(TLayoutTestCase)
+  private
+    class procedure AssertBlock(Msg: String; aBlock: TTextBlock; atext: String; aPosX, aPosY, aWidth, aHeight: TTextUnits; aLineBreak : Boolean = False);
+    procedure AssertBlock(Msg: String; Idx: Integer; atext: String; aPosX, aPosY, aWidth, aHeight: TTextUnits; aLineBreak : Boolean = False);
+  published
+    procedure TestHookUp;
+    procedure TestSimpleText;
+    procedure TestSimpleMultiLineText;
+    procedure TestSimpleMultiLineTextCRLF;
+    procedure TestSimpleMultiLineTextWhiteSpace;
+    procedure TestSimpleMultiLineTextWhiteSpaceCRLF;
+    procedure TestSimpleWrapText;
+    procedure TestSimpleWrapTextHyphen;
+    procedure TestSimpleWrapTextOnewordOverflow;
+    procedure TestSimpleWrapTextOnewordTruncate;
+    procedure TestSimpleWrapTextOnewordTruncateEmpty;
+    procedure TestSimpleWrapTextOnewordEllipsis;
+    procedure TestSimpleWrapTextOnewordAsterisk;
+    procedure TestRangeOverlap;
+    procedure TestRangeOverlapFit;
+    procedure TestRangeText;
+    procedure TestRangeTextOffset;
+    procedure TestRangeTextOffset2;
+    procedure TestRangeTextOffset3;
+  end;
+
+
+
+
+implementation
+
+{ TTestLayouter }
+
+class function TTestLayouter.CreateMeasurer(aLayouter: TTextLayouter): TTextMeasurer;
+
+var
+  M : TFixedSizeTextMeasurer;
+
+
+begin
+  M:=TFixedSizeTextMeasurer.Create(aLayouter);
+  // For easier calculation
+  M.CharWidth:=cWidth;
+  M.CharHeight:=cHeight;
+  Result:=M;
+end;
+
+class function TTestLayouter.CreateSPlitter(aLayouter: TTextLayouter): TTextSplitter;
+begin
+  Result:=TFixedListSplitter.Create(aLayouter);
+end;
+
+{ TFixedListSplitter }
+
+function TFixedListSplitter.GetNextSplitPoint(const aText : TTextString; aStartPos : SizeInt; aAllowHyphen : Boolean) : TTextSplitPoint;
+
+Const
+  SplText = 'spaces.';
+
+begin
+  if not aAllowHyphen then
+    Result:=Inherited GetNextSplitPoint(aText,aStartPos,aAllowHyphen)
+  else
+    if (Copy(aText,aStartPos,Length(splText))=SplText) then
+      Result:=SplitPoint(aStartPos+2,0)
+    else
+      Result:=Inherited GetNextSplitPoint(aText,aStartPos,aAllowHyphen)
+
+end;
+
+
+{ TLayoutTestCase }
+
+procedure TLayoutTestCase.SetUp;
+begin
+  inherited SetUp;
+  FLayouter:=TTestLayouter.Create(nil);
+  FFont:=TTextFont.Create(Nil);
+  Font.Name:=cFontName;
+  Font.Size:=cFontSize;
+  Font.Attrs:=cFontAttr;
+  FLayouter.Font:=FFont;
+  FLayouter.LineSpacing:=cLineSpacing;
+end;
+
+procedure TLayoutTestCase.TearDown;
+begin
+  FreeAndNil(FLayouter);
+  FreeAndNil(FFont);
+  inherited TearDown;
+end;
+
+function TLayoutTestCase.CreateTextBlock(const aOffset,aLength : SizeInt): TTextBlock;
+begin
+  Result:=TTextBlock.Create(Layouter,aOffset,aLength);
+end;
+
+class procedure TLayoutTestCase.AssertEquals(aMsg: String; aExpected, aActual: TTextSplitPoint);
+begin
+  AssertEquals(aMsg+': offset',aExpected.offset,aActual.Offset);
+  AssertEquals(aMsg+': whitespace',aExpected.WhiteSpace,aActual.WhiteSpace);
+end;
+
+procedure TTestSplitter.TestHookUp;
+begin
+  AssertNotNull('Have splitter',Splitter);
+end;
+
+procedure TTestSplitter.TestOneWord;
+begin
+  TestSplit('one word','one',1,[SplitPoint(3,1)]);
+end;
+
+procedure TTestSplitter.Test2Words;
+begin
+  TestSplit('two words','one two',1,[SplitPoint(3,1),SplitPoint(7,1)]);
+end;
+
+procedure TTestSplitter.Test3Words;
+begin
+  //                       0    5    10   15
+  TestSplit('three words','one  two   three ',1,[SplitPoint(3,2),SplitPoint(8,3),SplitPoint(16,1)]);
+end;
+
+procedure TTestSplitter.Test4Words;
+begin
+  // Pos (=offset+1)      1   5    10   15
+  TestSplit('Four words','the  cat saw   me',1,[SplitPoint(3,2),SplitPoint(8,1),SplitPoint(12,3),SplitPoint(17,1)]);
+  //                         ^    ^   ^    ^
+end;
+
+procedure TTestSplitter.TestLineBreak;
+begin
+  // Pos (=offset+1)            1   5        10   15
+  TestNewLineSplit('One split','the cat'#10'saw me',1,[SplitPoint(7,1)]);
+  //                                        ^
+end;
+
+procedure TTestSplitter.Test2LineBreaks;
+begin
+  // Pos (=offset+1)            1   5        10   15
+  TestNewLineSplit('Two split','the cat'#10'saw me'#10'and ran',1,[SplitPoint(7,1),SplitPoint(14,1)]);
+  //                                        ^
+end;
+
+procedure TTestSplitter.Test2LineBreakChars;
+begin
+  // Pos (=offset+1)            1   5          10   15
+  TestNewLineSplit('Two split','the cat'#13#10'saw me'#10'and ran',1,[SplitPoint(7,2),SplitPoint(15,1)]);
+  //                                        ^
+end;
+
+
+procedure TTestSplitter.SetUp;
+begin
+  Inherited;
+  FSplitter:=TTextSplitter.Create(Layouter);
+end;
+
+procedure TTestSplitter.TearDown;
+begin
+  FreeAndNil(FSplitter);
+  Inherited;
+end;
+
+procedure TTestSplitter.CheckPoints(Msg : String; aRes: TTextSplitPointArray; aPoints: array of TTextSplitPoint);
+
+var
+  I : integer;
+
+begin
+  AssertEquals(Msg+': Splitpoint count',Length(aPoints),Length(aRes));
+  For I:=0 to Length(aRes)-1 do
+    AssertEquals(Msg+Format(': Element %d',[I]),aPoints[i],aRes[I]);
+end;
+
+procedure TTestSplitter.TestSplit(Msg : String; aText: TTextString; aStartPos: SizeInt; aPoints: array of TTextSplitPoint);
+
+var
+  Res : TTextSplitPointArray;
+
+begin
+  Res:=Splitter.SplitText(aText,aStartPos,False);
+  CheckPoints(Msg,Res,aPoints);
+end;
+
+procedure TTestSplitter.TestNewLineSplit(Msg: String; aText: TTextString; aStartPos: SizeInt; aPoints: array of TTextSplitPoint);
+var
+  Res : TTextSplitPointArray;
+begin
+  Res:=Splitter.SplitLines(aText,aStartPos,False);
+  CheckPoints(Msg,Res,aPoints);
+end;
+
+{ TTestTextBlock }
+
+procedure TTestTextBlock.SetUp;
+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;
+  Block.Font.Attrs:=[faBold];
+  Block.Font.FPColor:=colBlack;
+end;
+
+procedure TTestTextBlock.TearDown;
+begin
+  FreeAndNil(FFont);
+  FreeAndNil(FTextBlock);
+  inherited TearDown;
+end;
+
+procedure TTestTextBlock.TestHookUp;
+begin
+  AssertNotNull('Have block',Block);
+  AssertEquals('block offset',0,Block.TextOffset);
+  AssertEquals('block length',4,Block.TextLen);
+  AssertEquals('block font name','Arial',Block.Font.Name);
+  AssertEquals('block font Size',12,Block.Font.Size);
+  AssertTrue('block font attrs',[faBold]=Block.Font.Attrs);
+  AssertTrue('block font color',Block.Font.FPColor=colBlack);
+  AssertEquals('Get text','this',Block.Text);
+end;
+
+procedure TTestTextBlock.FakeLayoutBlock(aBlock : TTextBlock);
+
+begin
+  With aBlock do
+    begin
+    ForceNewLine:=True;
+    LayoutPos:=PointF(2,3);
+    Size.Width:=12.3;
+    Size.Height:=8.9;
+    Size.Descender:=2.3;
+    Font:=Self.FFont;
+    end;
+end;
+
+procedure TTestTextBlock.TestAssign;
+
+var
+  Blk : TTextBlock;
+
+begin
+  FakeLayoutBlock(Block);
+
+  Blk:=CreateTextBlock(0,0);
+  try
+    Blk.Assign(Block);
+    AssertEquals('block offset',Block.TextOffset,Blk.TextOffset);
+    AssertEquals('block length',Block.TextLen,Blk.TextLen);
+    AssertEquals('block forcenewline',Block.ForceNewLine,Blk.ForceNewLine);
+
+    AssertEquals('block font name',Block.Font.Name,Blk.Font.Name);
+    AssertEquals('block font Size',12,Block.Font.Size,Blk.Font.Size);
+    AssertTrue('block font attrs',Block.Font.Attrs=Blk.Font.Attrs);
+    AssertTrue('block font color',FTextBlock.Font.Color=Blk.Font.Color);
+    AssertEquals('block layout pos X',Block.LayoutPos.X,Blk.LayoutPos.X);
+    AssertEquals('block layout pos Y',Block.LayoutPos.Y,Blk.LayoutPos.Y);
+    AssertEquals('block width',Block.Width,Blk.Width);
+    AssertEquals('block height',Block.Height,Blk.Height);
+    AssertEquals('block descender',Block.Descender,Blk.Descender);
+  finally
+    Blk.Free;
+  end;
+end;
+
+procedure TTestTextBlock.TestSplit;
+var
+  Blk : TTextBlock;
+
+begin
+  FakeLayoutBlock(Block);
+
+  Blk:=Block.Split(2);
+  try
+    AssertEquals('old block offset',0,Block.TextOffset);
+    AssertEquals('old block length',2,Block.TextLen);
+    AssertEquals('new block offset',2,Blk.TextOffset);
+    AssertEquals('new block length',2,Blk.TextLen);
+    AssertEquals('block font name',Block.Font.Name,Blk.Font.Name);
+    AssertEquals('block font Size',12,Block.Font.Size,Blk.Font.Size);
+    AssertTrue('block font attrs',Block.Font.Attrs=Blk.Font.Attrs);
+    AssertTrue('block font color',FTextBlock.Font.Color=Blk.Font.Color);
+    AssertEquals('block layout pos X',0,Blk.LayoutPos.X);
+    AssertEquals('block layout pos Y',0,Blk.LayoutPos.Y);
+    AssertEquals('block width',0,Blk.Width);
+    AssertEquals('block height',0,Blk.Height);
+    AssertEquals('block descender',0,Blk.Descender);
+    AssertEquals('block Forcenewline',False,Blk.ForceNewLine);
+  finally
+    Blk.Free;
+  end;
+end;
+
+procedure TTestTextBlock.TestSplit2;
+var
+  Blk : TTextBlock;
+
+begin
+  Layouter.Text:='one for the road';
+  Block.TextOffset:=4;
+  Block.TextLen:=7;
+  AssertEquals('Orig block Text','for the',Block.Text);
+  FakeLayoutBlock(Block);
+  Blk:=Block.Split(4);
+  try
+    AssertEquals('old block offset',4,Block.TextOffset);
+    AssertEquals('old block length',4,Block.TextLen);
+    AssertEquals('old block Text','for ',Block.Text);
+    AssertEquals('new block offset',8,Blk.TextOffset);
+    AssertEquals('new block length',3,Blk.TextLen);
+    AssertEquals('new block Text','the',Blk.Text);
+    AssertEquals('block font name',Block.Font.Name,Blk.Font.Name);
+    AssertEquals('block font Size',12,Block.Font.Size,Blk.Font.Size);
+    AssertTrue('block font attrs',Block.Font.Attrs=Blk.Font.Attrs);
+    AssertTrue('block font color',FTextBlock.Font.Color=Blk.Font.Color);
+    AssertEquals('block layout pos X',0,Blk.LayoutPos.X);
+    AssertEquals('block layout pos Y',0,Blk.LayoutPos.Y);
+    AssertEquals('block width',0,Blk.Width);
+    AssertEquals('block height',0,Blk.Height);
+    AssertEquals('block descender',0,Blk.Descender);
+    AssertEquals('block Forcenewline',False,Blk.ForceNewLine);
+  finally
+    Blk.Free;
+  end;
+end;
+
+{ TTestTextLayouter }
+
+procedure TTestTextLayouter.TestHookUp;
+begin
+  AssertNotNull('Have layouter',Layouter);
+  AssertEquals('blocks',0,Layouter.TextBlockCount);
+  AssertEquals('bounds height',0.0,Layouter.Bounds.Height,0.01);
+  AssertEquals('bounds width',0.0,Layouter.Bounds.Width,0.01);
+  AssertEquals('Font name','Arial',cFontName);
+  AssertEquals('Font size',12,cFontSize);
+  AssertEquals('Line spacing',cLineSpacing,FLayouter.LineSpacing);
+  AssertTrue('Font attrs',[]=cFontAttr);
+end;
+
+
+class procedure TTestTextLayouter.AssertBlock(Msg: String; aBlock : TTextBlock; atext : String; aPosX,aPosY,aWidth,aHeight : TTextUnits; aLineBreak: Boolean = False);
+
+begin
+  Msg:=Msg+': ';
+  AssertNotNull(Msg+'Have block',aBlock);
+  AssertEquals(Msg+'Text',aText,aBlock.Text);
+  AssertEquals(Msg+'Pos.X',aPosX,aBlock.LayoutPos.X,cDelta);
+  AssertEquals(Msg+'Pos.Y',aPosY,aBlock.LayoutPos.Y,cDelta);
+  AssertEquals(Msg+'width',aWidth,aBlock.Width,cDelta);
+  AssertEquals(Msg+'height',aHeight,aBlock.Height,cDelta);
+  AssertEquals(Msg+'linebreak',aLineBreak,aBlock.ForceNewLine);
+end;
+
+procedure TTestTextLayouter.AssertBlock(Msg: String; Idx : Integer; atext : String; aPosX,aPosY,aWidth,aHeight : TTextUnits; aLineBreak: Boolean = False);
+
+begin
+  AssertTrue(Msg+Format('Block index %d OK',[Idx]),(Idx>=0) and (Idx<Layouter.TextBlockCount));
+  AssertBlock(Msg,Layouter.TextBlocks[Idx],aText,aPosX,aPosY,aWidth,aHeight,aLineBreak);
+end;
+
+procedure TTestTextLayouter.TestSimpleText;
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:='Some text with spaces.';
+  MyWidth:=Length(Layouter.Text)*cWidth;
+  MyHeight:=cHeight;
+  Layouter.Bounds.Width:=myWidth*1.1;
+  Layouter.Bounds.Height:=cHeight*1.1;
+  AssertEquals('block count',1,Layouter.Execute);
+  AssertBlock('Block',0,Layouter.Text,0,0,MyWidth,MyHeight);
+end;
+
+procedure TTestTextLayouter.TestSimpleMultiLineText;
+
+const
+  cLine1 = 'Some text';
+  cLine2 = 'with spaces.';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1+#10+cLine2;
+  MyWidth:=Length(Layouter.Text)*cWidth;
+  MyHeight:=(cHeight+cLineSpacing)*2;
+  Layouter.Bounds.Width:=myWidth*1.1;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  // First block
+  MyWidth:=Length(cLine1)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block',0,cLine1,0,0,MyWidth,MyHeight);
+  // Second block
+  MyWidth:=Length(cLine2)*cWidth;
+  AssertBlock('Block',1,cLine2,0,cHeight+cLineSpacing,MyWidth,MyHeight,True);
+
+end;
+
+procedure TTestTextLayouter.TestSimpleMultiLineTextCRLF;
+
+const
+  cLine1 = 'Some text';
+  cLine2 = 'with spaces.';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1+#13#10+cLine2;
+  MyWidth:=Length(Layouter.Text)*cWidth;
+  MyHeight:=(cHeight+cLineSpacing)*2;
+  Layouter.Bounds.Width:=myWidth*1.1;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  // First block
+  MyWidth:=Length(cLine1)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block',0,cLine1,0,0,MyWidth,MyHeight);
+  // Second block
+  MyWidth:=Length(cLine2)*cWidth;
+  AssertBlock('Block',1,cLine2,0,cHeight+cLineSpacing,MyWidth,MyHeight,True);
+end;
+
+procedure TTestTextLayouter.TestSimpleMultiLineTextWhiteSpace;
+
+const
+  cLine1 = 'Some text';
+  cLine2 = 'with spaces.';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1+' '#10+cLine2;
+  MyWidth:=Length(Layouter.Text)*cWidth;
+  MyHeight:=(cHeight+cLineSpacing)*2;
+  Layouter.Bounds.Width:=myWidth*1.1;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  // First block
+  MyWidth:=Length(cLine1)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block',0,cLine1,0,0,MyWidth,MyHeight);
+  // Second block
+  MyWidth:=Length(cLine2)*cWidth;
+  AssertBlock('Block',1,cLine2,0,cHeight+cLineSpacing,MyWidth,MyHeight,True);
+end;
+
+procedure TTestTextLayouter.TestSimpleMultiLineTextWhiteSpaceCRLF;
+const
+  cLine1 = 'Some text';
+  cLine2 = 'with spaces.';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1+'  '#13#10+cLine2;
+  MyWidth:=Length(Layouter.Text)*cWidth;
+  MyHeight:=(cHeight+cLineSpacing)*2;
+  Layouter.Bounds.Width:=myWidth*1.1;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  // First block
+  MyWidth:=Length(cLine1)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block',0,cLine1,0,0,MyWidth,MyHeight);
+  // Second block
+  MyWidth:=Length(cLine2)*cWidth;
+  AssertBlock('Block',1,cLine2,0,cHeight+cLineSpacing,MyWidth,MyHeight,True);
+end;
+
+procedure TTestTextLayouter.TestSimpleWrapText;
+const
+  cLine1 = 'Some text';
+  cLine2 = 'with spaces.';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1+' '+cLine2;
+  Layouter.WordWrap:=True;
+  MyWidth:=(Length(cLine2)+0.5)*cWidth;
+  MyHeight:=(cHeight+cLineSpacing)*2;
+  Layouter.Bounds.Width:=myWidth;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  // First block
+  MyWidth:=Length(cLine1)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block 1',0,cLine1,0,0,MyWidth,MyHeight);
+  // Second block
+  MyWidth:=Length(cLine2)*cWidth;
+  AssertBlock('Block 2',1,cLine2,0,cHeight+cLineSpacing,MyWidth,MyHeight,True);
+end;
+
+procedure TTestTextLayouter.TestSimpleWrapTextHyphen;
+
+const
+  cLine1 = 'Some text';
+  cLine2 = 'with spaces.';
+  cSplit1 = 'Some text with spa-';
+  cSplit2 = 'ces.';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1+' '+cLine2;
+  Layouter.WordWrap:=True;
+  Layouter.AllowHyphenation:=True;
+  MyWidth:=(Length(cSplit1)+0.5)*cWidth;
+  MyHeight:=(cHeight+cLineSpacing)*2;
+  Layouter.Bounds.Width:=myWidth;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  // First block
+  MyWidth:=Length(cSplit1)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block 1',0,cSplit1,0,0,MyWidth,MyHeight);
+  // Second block
+  MyWidth:=Length(cSplit2)*cWidth;
+  AssertBlock('Block 2',1,cSplit2,0,cHeight+cLineSpacing,MyWidth,MyHeight,True);
+end;
+
+procedure TTestTextLayouter.TestSimpleWrapTextOnewordOverflow;
+// One word, too long for wordwrap.
+// Overflow
+const
+  cLine1 = 'longevity';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1;
+  Layouter.WordWrap:=True;
+  Layouter.WordOverflow:=woOverflow;
+  MyWidth:=5*cWidth;
+  MyHeight:=(cHeight+cLineSpacing);
+  Layouter.Bounds.Width:=myWidth*1.1;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',1,Layouter.Execute);
+  MyWidth:=Length(cLine1)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block 1',0,cLine1,0,0,MyWidth,MyHeight);
+end;
+
+procedure TTestTextLayouter.TestSimpleWrapTextOnewordTruncate;
+// One word, too long for wordwrap.
+// Explicitly set truncate (although it is the default)
+
+
+const
+  cLine1 = 'longevity';
+  cRes   = 'longe';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1;
+  Layouter.WordOverflow:=woTruncate;
+  MyWidth:=5.5*cWidth;
+  MyHeight:=(cHeight+cLineSpacing);
+  Layouter.Bounds.Width:=myWidth;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',1,Layouter.Execute);
+  MyWidth:=Length(cRes)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block 1',0,cRes,0,0,MyWidth,MyHeight);
+end;
+
+procedure TTestTextLayouter.TestSimpleWrapTextOnewordTruncateEmpty;
+// One word, too long for wordwrap.
+// Explicitly set truncate (although it is the default)
+
+const
+  cLine1 = 'longevity';
+
+var
+  myHeight,myWidth : TTextUnits;
+
+begin
+  Layouter.Text:=cLine1;
+  Layouter.WordOverflow:=woTruncate;
+  MyWidth:=0.5*cWidth;
+  MyHeight:=(cHeight+cLineSpacing);
+  Layouter.Bounds.Width:=myWidth;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',1,Layouter.Execute);
+  MyWidth:=0;
+  MyHeight:=cHeight;
+  AssertBlock('Block 1',0,'',0,0,MyWidth,MyHeight);
+end;
+
+procedure TTestTextLayouter.TestSimpleWrapTextOnewordEllipsis;
+// One word, too long for wordwrap.
+
+const
+  cLine1 = 'longevity';
+
+var
+  myHeight,myWidth : TTextUnits;
+  cRes : String;
+
+begin
+  Layouter.Text:=cLine1;
+  Layouter.WordOverflow:=woEllipsis;
+  MyWidth:=5.5*cWidth;
+  MyHeight:=(cHeight+cLineSpacing);
+  Layouter.Bounds.Width:=myWidth;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',1,Layouter.Execute);
+  cRes:='lo'+UTF8Encode(cEllipsis); // 3 characters in UTF8
+  MyWidth:=Length(cRes)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block 1',0,cRes,0,0,MyWidth,MyHeight);
+end;
+
+procedure TTestTextLayouter.TestSimpleWrapTextOnewordAsterisk;
+// One word, too long for wordwrap.
+
+const
+  cLine1 = 'longevity';
+
+var
+  myHeight,myWidth : TTextUnits;
+  cRes : String;
+
+begin
+  Layouter.Text:=cLine1;
+  Layouter.WordOverflow:=woAsterisk;
+  MyWidth:=5.5*cWidth;
+  MyHeight:=(cHeight+cLineSpacing);
+  Layouter.Bounds.Width:=myWidth;
+  Layouter.Bounds.Height:=myHeight*1.1;
+  AssertEquals('block count',1,Layouter.Execute);
+  cRes:='long*';
+  MyWidth:=Length(cRes)*cWidth;
+  MyHeight:=cHeight;
+  AssertBlock('Block 1',0,cRes,0,0,MyWidth,MyHeight);
+end;
+
+procedure TTestTextLayouter.TestRangeOverlap;
+begin
+  Layouter.Text:='Some text with spaces';
+  Layouter.Ranges.AddRange(0,10,Font);
+  Layouter.Ranges.AddRange(9,Length(Layouter.Text)-9,Font);
+  AssertException('Ranges cannot overlap',ETextLayout,@Layouter.CheckRanges);
+end;
+
+procedure TTestTextLayouter.TestRangeOverlapFit;
+
+var
+  I : Integer;
+
+begin
+  Layouter.Text:='Some text with spaces';
+  Layouter.Ranges.AddRange(9,Length(Layouter.Text)-9,Font);
+  Layouter.Ranges.AddRange(0,10,Font);
+  Layouter.OverlappingRangesAction:=oraFit;
+  Layouter.CheckRanges;
+  For I:=0 to Layouter.Ranges.Count-1 do
+    Writeln(i,' : ',Layouter.Ranges[i].ToString);
+  With Layouter.Ranges[0] do
+    begin
+    AssertEquals('Range 0 offset',0,CharOffset);
+    AssertEquals('Range 0 length',9,CharLength);
+    end;
+  With Layouter.Ranges[1] do
+    begin
+    AssertEquals('Range 1 offset',9,CharOffset);
+    AssertEquals('Range 1 length',Length(Layouter.Text)-9,CharLength);
+    end;
+end;
+
+procedure TTestTextLayouter.TestRangeText;
+
+Const
+  P1 = 'Some text';
+  P2 = ' with spaces.';
+
+var
+
+  myHeight1,myHeight2,myWidth1,myWidth2 : TTextUnits;
+  F2 : TTextFont;
+
+begin
+  Layouter.Text:=P1+P2;
+  F2:=Font.Clone;
+  try
+    F2.Size:=24;
+    // 'Some text' in large font.
+    Layouter.Ranges.AddRange(0,Length(P1),F2);
+  finally
+    F2.Free;
+  end;
+  MyWidth1:=Length(P1)*(cWidth*2);
+  MyWidth2:=Length(P2)*(cWidth);
+  MyHeight1:=cHeight*2;
+  MyHeight2:=cHeight;
+  Layouter.Bounds.Width:=(myWidth1+MyWidth2)*1.1;
+  Layouter.Bounds.Height:=MyHeight2*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  AssertBlock('Block 1',0,P1,0,0,MyWidth1,MyHeight1);
+  AssertBlock('Block 2',1,P2,MyWidth1,0,MyWidth2,MyHeight2);
+end;
+
+procedure TTestTextLayouter.TestRangeTextOffset;
+Const
+  P1 = 'Some text';
+  P2 = ' with spaces.';
+
+var
+
+  myHeight1,myHeight2,myWidth1,myWidth2 : TTextUnits;
+  F2 : TTextFont;
+
+begin
+  Layouter.Text:=P1+P2;
+  F2:=Font.Clone;
+  try
+    F2.Size:=24;
+    // ' with spaces' in large font.
+    Layouter.Ranges.AddRange(Length(P1),Length(P2),F2);
+  finally
+    F2.Free;
+  end;
+  MyWidth1:=Length(P1)*(cWidth);
+  MyWidth2:=Length(P2)*(cWidth*2);
+  MyHeight1:=cHeight;
+  MyHeight2:=cHeight*2;
+  Layouter.Bounds.Width:=(myWidth1+MyWidth2)*1.1;
+  Layouter.Bounds.Height:=MyHeight2*1.1;
+  AssertEquals('block count',2,Layouter.Execute);
+  AssertBlock('Block 1',0,P1,0,0,MyWidth1,MyHeight1);
+  AssertBlock('Block 2',1,P2,MyWidth1,0,MyWidth2,MyHeight2);
+end;
+
+procedure TTestTextLayouter.TestRangeTextOffset2;
+
+Const
+  P1 = 'Some text ';
+  P2 = 'with';
+  P3 = ' spaces.';
+
+var
+  myHeight1,myHeight2,myHeight3,
+  myWidth1,myWidth2,myWidth3 : TTextUnits;
+  F2 : TTextFont;
+
+begin
+  // 'with' in regular font, 'Some text ' and ' spaces.' in large font
+  Layouter.Text:=P1+P2+P3;
+  F2:=Font.Clone;
+  try
+    F2.Size:=24;
+    Layouter.Ranges.AddRange(0,Length(P1),F2);
+    Layouter.Ranges.AddRange(Length(P1)+Length(P2),Length(P3),F2);
+  finally
+    F2.Free;
+  end;
+  MyWidth1:=Length(P1)*(cWidth*2);
+  MyWidth2:=Length(P2)*(cWidth);
+  MyWidth3:=Length(P3)*(cWidth*2);
+  MyHeight1:=cHeight*2;
+  MyHeight2:=cHeight;
+  MyHeight3:=cHeight*2;
+  Layouter.Bounds.Width:=(myWidth1+MyWidth2+myWidth3)*1.1;
+  Layouter.Bounds.Height:=MyHeight3*1.1;
+  AssertEquals('block count',3,Layouter.Execute);
+  AssertBlock('Block 1',0,P1,0,0,MyWidth1,MyHeight1);
+  AssertBlock('Block 2',1,P2,MyWidth1,0,MyWidth2,MyHeight2);
+  AssertBlock('Block 3',2,P3,MyWidth1+MyWidth2,0,MyWidth3,MyHeight3);
+end;
+
+procedure TTestTextLayouter.TestRangeTextOffset3;
+begin
+
+end;
+
+initialization
+
+  RegisterTests([TTestSplitter,TTestTextBlock,TTestTextLayouter]);
+end.
+