unit utsTextBlock; {$IFDEF FPC} {$mode objfpc}{$H+} {$ENDIF} interface uses Classes, SysUtils, utsUtils, utsTypes, utsFont, utsCharCache, utsContext; type //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TtsLineItemType = ( tsItemTypeUnknown, tsItemTypeFont, tsItemTypeColor, tsItemTypeText, tsItemTypeSpace, tsItemTypeLineBreak, tsItemTypeTab, tsItemTypeSpacing); //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TtsLineFlag = ( tsLastItemIsSpace, // is set if the last item was a space item tsMetaValid, // is set if the line meta data is valid tsAutoLineBreak // is set if the linebreak was set automatically ); TtsLineFlags = set of TtsLineFlag; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// PtsLineItem = ^TtsLineItem; TtsLineItem = packed record Next: PtsLineItem; Prev: PtsLineItem; ItemType: TtsLineItemType; case TtsLineItemType of tsItemTypeFont: ( Font: TtsFont ); tsItemTypeColor: ( Color: TtsColor4f; ); tsItemTypeText, tsItemTypeSpace: ( Text: PWideChar; // text of this item TextWidth: Integer; // width of text (in pixel) ); tsItemTypeSpacing: ( Spacing: Integer; ); tsItemTypeTab: ( TabWidth: Integer; // with of tab (in pixel) ); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// PtsBlockLine = ^TtsBlockLine; TtsBlockLine = packed record Next: PtsBlockLine; First: PtsLineItem; Last: PtsLineItem; Flags: TtsLineFlags; meta: packed record Width: Integer; // absolut width of this line Height: Integer; // absolute height of this line Spacing: Integer; // spacing between lines Ascent: Integer; // text ascent SpaceCount: Integer; // number of words in this line end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TtsBlockRenderer = class(TtsRenderRefGenerator) private fCharCache: TtsCharCache; protected property CharCache: TtsCharCache read fCharCache; procedure BeginRender; virtual; abstract; procedure EndRender; virtual; abstract; function GetDrawPos: TtsPosition; virtual; abstract; procedure SetDrawPos(const aValue: TtsPosition); virtual; abstract; procedure MoveDrawPos(const aOffset: TtsPosition); virtual; abstract; procedure SetColor(const aValue: TtsColor4f); virtual; abstract; procedure Render(const aRenderRef: TtsRenderRef; const aForcedWidth: Integer = 0); virtual; abstract; public constructor Create(const aContext: TtsContext; const aFormat: TtsFormat); destructor Destroy; override; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// TtsTextBlock = class(TtsRefManager) private fRenderer: TtsBlockRenderer; fTop: Integer; fLeft: Integer; fWidth: Integer; fHeight: Integer; fFlags: TtsBlockFlags; fVertAlign: TtsVertAlignment; fHorzAlign: TtsHorzAlignment; fClipping: TtsClipping; fCurrentChars: TtsChars; fCurrentColor: TtsColor4f; fCurrentFont: TtsFont; fFirstLine: PtsBlockLine; fLastLine: PtsBlockLine; function GetRect: TtsRect; function PushLineItem(const aItem: PtsLineItem): Boolean; procedure PushSpacing(const aWidth: Integer); procedure PushNewLine; procedure FreeLineItem(var aItem: PtsLineItem); procedure FreeLineItems(var aItem: PtsLineItem); procedure FreeLines(var aItem: PtsBlockLine); function SplitText(aText: PWideChar): PtsLineItem; function SplitIntoLines(aItem: PtsLineItem): Boolean; procedure TrimSpaces(const aLine: PtsBlockLine); procedure UpdateLineMeta(const aLine: PtsBlockLine); public procedure ChangeFont(const aFont: TtsFont); procedure ChangeColor(const aColor: TtsColor4f); public property Rect: TtsRect read GetRect; property Width: Integer read fWidth; property Height: Integer read fHeight; property Flags: TtsBlockFlags read fFlags; property Top: Integer read fTop write fTop; property Left: Integer read fLeft write fLeft; property VertAlign: TtsVertAlignment read fVertAlign write fVertAlign; property HorzAlign: TtsHorzAlignment read fHorzAlign write fHorzAlign; property Clipping: TtsClipping read fClipping write fClipping; property CurrentColor: TtsColor4f read fCurrentColor write ChangeColor; property CurrentFont: TtsFont read fCurrentFont write ChangeFont; function GetActualBlockHeight: Integer; procedure TextOutA(const aText: PAnsiChar); procedure TextOutW(const aText: PWideChar); function GetTextWidthA(const aText: PAnsiChar): Integer; function GetTextWidthW(const aText: PWideChar): Integer; procedure Render; constructor Create(const aRenderer: TtsBlockRenderer; const aLeft, aTop, aWidth, aHeight: Integer; const aFlags: TtsBlockFlags); destructor Destroy; override; end; implementation uses math, utsChar; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //TtsBlockRenderer////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// constructor TtsBlockRenderer.Create(const aContext: TtsContext; const aFormat: TtsFormat); begin inherited Create(aContext, aFormat); fCharCache := TtsCharCache.Create(self); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// destructor TtsBlockRenderer.Destroy; begin FreeAndNil(fCharCache); inherited Destroy; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// //TtsTextBlock////////////////////////////////////////////////////////////////////////////////////////////////////////// //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function TtsTextBlock.GetRect: TtsRect; begin result.Left := fLeft; result.Top := fTop; result.Right := fLeft + fWidth; result.Bottom := fTop + fHeight; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function TtsTextBlock.PushLineItem(const aItem: PtsLineItem): Boolean; begin result := false; if not Assigned(fLastLine) then PushNewLine; if not Assigned(fLastLine^.First) and (aItem^.ItemType in [tsItemTypeSpace, tsItemTypeSpacing]) then exit; // di not add line space or line spacing if line is empty if Assigned(fLastLine^.Last) then begin aItem^.Prev := fLastLine^.Last; aItem^.Next := nil; fLastLine^.Last^.Next := aItem; fLastLine^.Last := aItem; end; if not Assigned(fLastLine^.First) then begin fLastLine^.First := aItem; fLastLine^.Last := aItem; end; case aItem^.ItemType of tsItemTypeSpace, tsItemTypeText: fLastLine^.meta.Width := fLastLine^.meta.Width + aItem^.TextWidth; tsItemTypeSpacing: fLastLine^.meta.Width := fLastLine^.meta.Width + aItem^.Spacing; tsItemTypeTab: fLastLine^.meta.Width := fLastLine^.meta.Width + aItem^.TabWidth; end; result := true; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.PushSpacing(const aWidth: Integer); var p: PtsLineItem; begin if (aWidth <= 0) then exit; new(p); FillChar(p^, SizeOf(p^), #0); p^.ItemType := tsItemTypeSpacing; p^.Spacing := aWidth; PushLineItem(p); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.PushNewLine; var p: PtsBlockLine; begin TrimSpaces(fLastLine); new(p); FillChar(p^, SizeOf(p^), #0); UpdateLineMeta(p); if Assigned(fLastLine) then begin fLastLine^.Next := p; fLastLine := p; end; if not Assigned(fFirstLine) then begin fFirstLine := p; fLastLine := p; end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.FreeLineItem(var aItem: PtsLineItem); begin if Assigned(aItem^.Prev) then aItem^.Prev^.Next := aItem^.Next; if Assigned(aItem^.Next) then aItem^.Next^.Prev := aItem^.Prev; case aItem^.ItemType of tsItemTypeText, tsItemTypeSpace: tsStrDispose(aItem^.Text); end; Dispose(aItem); aItem := nil; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.FreeLineItems(var aItem: PtsLineItem); var p: PtsLineItem; begin while Assigned(aItem) do begin p := aItem; aItem := aItem^.Next; FreeLineItem(p); end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.FreeLines(var aItem: PtsBlockLine); var p: PtsBlockLine; begin while Assigned(aItem) do begin p := aItem; aItem := aItem^.Next; FreeLineItems(p^.First); p^.Last := nil; Dispose(p); end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function TtsTextBlock.SplitText(aText: PWideChar): PtsLineItem; var TextBegin: PWideChar; TextLength: Integer; State: TtsLineItemType; LastItem: PtsLineItem; procedure AddItem(const aItem: PtsLineItem); begin if Assigned(result) then begin LastItem^.Next := aItem; aItem^.Prev := LastItem; aItem^.Next := nil; LastItem := aItem; end; if not Assigned(result) then begin result := aItem; LastItem := aItem; end; end; procedure ExtractWord; var p: PtsLineItem; Text: PWideChar; begin if (State = tsItemTypeUnknown) then exit; new(p); FillChar(p^, SizeOf(p^), #0); p^.ItemType := State; case State of tsItemTypeText, tsItemTypeSpace: begin p^.Text := tsStrAlloc(TextLength); Text := p^.Text; while (TextBegin <> aText) do begin Text^ := TextBegin^; inc(Text, 1); inc(TextBegin, 1); end; AddItem(p); end; tsItemTypeLineBreak, tsItemTypeTab: begin AddItem(p); end; else Dispose(p); end; TextBegin := aText; TextLength := 0; end; begin result := nil; LastItem := nil; TextBegin := aText; TextLength := 0; State := tsItemTypeUnknown; if not Assigned(aText) then exit; while (aText^ <> #0) do begin case aText^ of // line breaks #$000D, #$000A: begin if (State <> tsItemTypeLineBreak) then begin ExtractWord; State := tsItemTypeLineBreak; end else if (TextBegin^ <> #13) or (aText^ <> #10) or (TextBegin + 1 < aText) then ExtractWord; end; // spaces #$0020: begin if (State <> tsItemTypeSpace) then ExtractWord; State := tsItemTypeSpace; end; // tabulator #$0009: begin ExtractWord; State := tsItemTypeTab; end; else if (State <> tsItemTypeText) then ExtractWord; State := tsItemTypeText; end; inc(aText, 1); inc(TextLength, 1); end; if (TextBegin <> aText) then ExtractWord; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function TtsTextBlock.SplitIntoLines(aItem: PtsLineItem): Boolean; var p: PtsLineItem; tab: Integer; begin result := false; if not Assigned(fCurrentFont) then exit; result := true; while Assigned(aItem) do begin p := aItem; aItem := aItem^.Next; p^.Next := nil; p^.Prev := nil; if not Assigned(fLastLine) then PushNewLine; case p^.ItemType of tsItemTypeText, tsItemTypeSpace: begin // increment word counter if (p^.ItemType = tsItemTypeSpace) then begin if not (tsLastItemIsSpace in fLastLine^.Flags) then inc(fLastLine^.meta.SpaceCount, 1); Include(fLastLine^.Flags, tsLastItemIsSpace); end else Exclude(fLastLine^.Flags, tsLastItemIsSpace); // update and check line width p^.TextWidth := GetTextWidthW(p^.Text); if (tsBlockFlagWordWrap in fFlags) and (fLastLine^.meta.Width + p^.TextWidth > fWidth) then begin if (fLastLine^.meta.Width = 0) then begin if not PushLineItem(p) then // if is first word, than add anyway FreeLineItem(p); p := nil; end; include(fLastLine^.Flags, tsAutoLineBreak); PushNewLine; end; // add item if Assigned(p) then begin if not PushLineItem(p) then FreeLineItem(p); PushSpacing(fCurrentFont.CharSpacing); end; end; tsItemTypeLineBreak: begin if not PushLineItem(p) then FreeLineItem(p); PushNewLine; end; tsItemTypeTab: begin tab := fCurrentFont.TabWidth * fCurrentFont.Metric.Size; p^.TabWidth := (1 + fLastLine^.meta.Width div tab) * tab - fLastLine^.meta.Width; if not PushLineItem(p) then FreeLineItem(p); end; else raise EtsException.Create('unexpected line item'); end; end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.TrimSpaces(const aLine: PtsBlockLine); procedure Trim(var aItem: PtsLineItem; const aMoveNext: Boolean); var tmp, p: PtsLineItem; IsFirst: Boolean; begin IsFirst := true; p := aItem; while Assigned(p) do begin tmp := p; if aMoveNext then p := p^.Next else p := p^.Prev; case tmp^.ItemType of tsItemTypeText: begin //done break; end; tsItemTypeSpace, tsItemTypeSpacing: begin // update line meta if (tmp^.ItemType = tsItemTypeSpace) then begin aLine^.meta.Width := aLine^.meta.Width - tmp^.TextWidth; dec(aLine^.meta.SpaceCount, 1); end else aLine^.meta.Width := aLine^.meta.Width - tmp^.Spacing; FreeLineItem(tmp); if IsFirst then aItem := p; end; else IsFirst := false; end; end; end; begin if not Assigned(aLine) then exit; Trim(aLine^.First, true); Trim(aLine^.Last, false); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.UpdateLineMeta(const aLine: PtsBlockLine); var metric: TtsTextMetric; begin if not Assigned(fCurrentFont) or not Assigned(aLine) then exit; fCurrentFont.GetTextMetric(metric); if (tsMetaValid in aLine^.Flags) then begin aLine^.meta.Height := max( aLine^.meta.Height, metric.LineHeight); aLine^.meta.Spacing := max( aLine^.meta.Spacing, metric.LineSpacing); aLine^.meta.Ascent := max( aLine^.meta.Ascent, metric.Ascent); end else begin Include(aLine^.Flags, tsMetaValid); aLine^.meta.Height := metric.LineHeight; aLine^.meta.Spacing := metric.LineSpacing; aLine^.meta.Ascent := metric.Ascent; end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.ChangeFont(const aFont: TtsFont); var p: PtsLineItem; begin if not Assigned(aFont) then exit; New(p); FillChar(p^, SizeOf(p^), #0); fCurrentFont := aFont; fCurrentChars := fRenderer.fCharCache.Chars[fCurrentFont]; p^.ItemType := tsItemTypeFont; p^.Font := fCurrentFont; PushLineItem(p); UpdateLineMeta(fLastLine); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.ChangeColor(const aColor: TtsColor4f); var p: PtsLineItem; begin New(p); FillChar(p^, SizeOf(p^), #0); fCurrentColor := aColor; p^.ItemType := tsItemTypeColor; p^.Color := fCurrentColor; PushLineItem(p); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function TtsTextBlock.GetActualBlockHeight: Integer; var line: PtsBlockLine; begin result := 0; line := fFirstLine; while Assigned(line) do begin result := result + line^.meta.Height; line := line^.Next; end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.TextOutA(const aText: PAnsiChar); var tmp: PWideChar; begin tmp := fRenderer.Context.AnsiToWide(aText); try TextOutW(tmp); finally tsStrDispose(tmp); end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.TextOutW(const aText: PWideChar); var p: PtsLineItem; begin p := SplitText(aText); if not SplitIntoLines(p) then FreeLineItems(p); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function TtsTextBlock.GetTextWidthA(const aText: PAnsiChar): Integer; begin result := 0; if Assigned(fCurrentChars) then result := fCurrentChars.GetTextWidthA(aText); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// function TtsTextBlock.GetTextWidthW(const aText: PWideChar): Integer; begin result := 0; if Assigned(fCurrentChars) then result := fCurrentChars.GetTextWidthW(aText); end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// procedure TtsTextBlock.Render; var c: PWideChar; pos: TtsPosition; x, y, tmp, tab: Integer; ExtraSpaceTotal, ExtraSpaceActual: Single; r: TtsRect; line: PtsBlockLine; item: PtsLineItem; font: TtsFont; chars: TtsChars; char: TtsChar; metric: TtsTextMetric; draw: Boolean; procedure DrawItem; begin case item^.ItemType of tsItemTypeFont: begin font := item^.Font; font.GetTextMetric(metric); chars := fRenderer.fCharCache.Chars[font]; end; tsItemTypeColor: begin fRenderer.SetColor(item^.Color); end; tsItemTypeText: begin if draw and Assigned(font) then begin c := item^.Text; while (c^ <> #0) do begin if Assigned(chars) then begin char := chars.AddChar(c^); if Assigned(char) then begin fRenderer.MoveDrawPos(tsPosition(0, -metric.BaseLineOffset)); fRenderer.Render(char.RenderRef); fRenderer.MoveDrawPos(tsPosition(char.GlyphMetric.Advance + font.CharSpacing, metric.BaseLineOffset)); end; end; inc(c); end; end; end; tsItemTypeSpace: begin if draw and Assigned(font) then begin ExtraSpaceActual := ExtraSpaceActual + ExtraSpaceTotal; c := item^.Text; while (c^ <> #0) do begin if Assigned(chars) then begin char := chars.AddChar(c^); if Assigned(char) then begin if (font.Metric.Style * [tsStyleUnderline, tsStyleStrikeout] <> []) then begin fRenderer.MoveDrawPos(tsPosition(0, -metric.BaseLineOffset)); fRenderer.Render(char.RenderRef); fRenderer.MoveDrawPos(tsPosition(char.GlyphMetric.Advance + font.CharSpacing, metric.BaseLineOffset)); end else begin fRenderer.MoveDrawPos(tsPosition(char.GlyphMetric.Advance + font.CharSpacing, 0)); end; end; end; inc(c); end; tmp := Trunc(ExtraSpaceActual); ExtraSpaceActual := ExtraSpaceActual - tmp; if (font.Metric.Style * [tsStyleUnderline, tsStyleStrikeout] <> []) then begin if Assigned(chars) then begin char := chars.AddChar(#0); if Assigned(char) then fRenderer.Render(char.RenderRef, tmp); // TODO draw lines; maybe with a temporary created fake char or something like an empty char? end; end; fRenderer.MoveDrawPos(tsPosition(tmp, 0)); end; end; tsItemTypeLineBreak: begin // because this should be the last item in a line, we have nothing to do here end; tsItemTypeTab: begin // get current x pos and round it to TabWidth pos := fRenderer.GetDrawPos; tab := font.TabWidth * font.Metric.Size; if (tab = 0) then tab := 1; pos.x := Left + (1 + (pos.x - Left) div tab) * tab; fRenderer.SetDrawPos(pos); end; tsItemTypeSpacing: begin fRenderer.MoveDrawPos(tsPosition(item^.Spacing, 0)); end; end; end; procedure DrawLine; begin // check vertical clipping case Clipping of tsClipCharBorder, tsClipWordBorder: draw := (y + line^.meta.Height >= r.Top) and (y <= r.Bottom); tsClipCharComplete, tsClipWordComplete: draw := (y >= r.Top) and (y + line^.meta.Height <= r.Bottom); else draw := true; end; // check horizontal alignment x := r.Left; ExtraSpaceTotal := 0; ExtraSpaceActual := 0; case HorzAlign of tsHorzAlignCenter: x := r.Left + (Width div 2) - (line^.meta.Width div 2); tsHorzAlignRight: x := r.Right - line^.meta.Width; tsHorzAlignJustify: if (tsAutoLineBreak in line^.Flags) and (line^.meta.SpaceCount > 0) then ExtraSpaceTotal := (Width - line^.meta.Width) / line^.meta.SpaceCount; end; if draw then fRenderer.SetDrawPos(tsPosition(x, y + line^.meta.Ascent)); inc(y, line^.meta.Height + line^.meta.Spacing); item := line^.First; while Assigned(item) do begin DrawItem; item := item^.Next; end; end; begin fRenderer.BeginRender; try // init variables y := Top; r := Rect; font := nil; line := fFirstLine; // check vertical alignment case VertAlign of tsVertAlignCenter: y := y + (Height div 2 - GetActualBlockHeight div 2); tsVertAlignBottom: y := y + (Height - GetActualBlockHeight); end; while Assigned(line) do begin DrawLine; line := line^.Next; end; finally fRenderer.EndRender; end; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// constructor TtsTextBlock.Create(const aRenderer: TtsBlockRenderer; const aLeft, aTop, aWidth, aHeight: Integer; const aFlags: TtsBlockFlags); begin inherited Create(aRenderer); fRenderer := aRenderer; fTop := aTop; fLeft := aLeft; fWidth := aWidth; fHeight := aHeight; fFlags := aFlags; end; //////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// destructor TtsTextBlock.Destroy; begin FreeLines(fFirstLine); fLastLine := nil; inherited Destroy; end; end.