unit painters; { TODO line spacing. } {$M+} interface uses types, buffers { TTextBufferItem }, textfitters { TTextLayoutRectangle }, encodings, classes, streams, sysutils; type TAreaOverrunException = class(Exception) end; TContinuousCharacterPainterTotalSizeChanged = procedure(Sender : TObject; newSize : TSize) of object; TCellIndex = type Integer; // TODO use State pattern to model the dummy/non-dummy difference. TSelectionRange = array[0..1] of TPoint; TUnicodeRun = TUnicodeCharacter; // array of codepoints for a run (multiple characters). IContinuousCharacterPainter = interface procedure EmitLineFeed(); function ProcessItem(item : TTextBufferItem) : Boolean; function ProcessBlock(aBeginning : PChar; aSize : Cardinal) : Cardinal; // not neccessarily 0-terminated. function GetCurrentWidth : Cardinal; procedure SetCurrentWidth(aValue : Cardinal); function GetCurrentHeight : Cardinal; procedure SetCurrentHeight(aValue : Cardinal); function GetRectangle : TTextLayoutRectangle; procedure SetRectangle(aValue : TTextLayoutRectangle); function GetNextRectangles : ITextLayoutRectanglesIterator; procedure SetNextRectangles(aValue : ITextLayoutRectanglesIterator); function GetPositionX : Cardinal; procedure SetPositionX(aValue : Cardinal); function GetPositionY : Cardinal; procedure SetPositionY(aValue : Cardinal); function GetRowHeight : Cardinal; procedure SetRowHeight(aValue : Cardinal); function GetRowSpacing : Cardinal; procedure SetRowSpacing(aValue : Cardinal); function GetBreakPositionX : Cardinal; procedure SetBreakPositionX(aValue : Cardinal); function GetBreakPositionY : Cardinal; procedure SetBreakPositionY(aValue : Cardinal); procedure BeginRound; procedure Flush; // call this after FinishRound, if in doubt. procedure FinishRound; // ClrEOS. function GetTotalSizeChanged : TContinuousCharacterPainterTotalSizeChanged; procedure SetTotalSizeChanged(aValue : TContinuousCharacterPainterTotalSizeChanged); property Width : Cardinal read GetCurrentWidth write SetCurrentWidth; property Height : Cardinal read GetCurrentHeight write SetCurrentHeight; property NextRectangles : ITextLayoutRectanglesIterator read GetNextRectangles write SetNextRectangles; property PositionX : Cardinal read GetPositionX write SetPositionX; { relative to the current rectangle's origin. } property PositionY : Cardinal read GetPositionY write SetPositionY; { relative to the current rectangle's origin. } // "break" position: where the character painter will stop as if the window was overrun, in order to be able to determine where a user mouse click went. property BreakPositionX : Cardinal read GetBreakPositionX write SetBreakPositionX; property BreakPositionY : Cardinal read GetBreakPositionY write SetBreakPositionY; property RowHeight : Cardinal read GetRowHeight write SetRowHeight; { current row. } property RowSpacing : Cardinal read GetRowSpacing write SetRowSpacing; property TotalSizeChanged : TContinuousCharacterPainterTotalSizeChanged read GetTotalSizeChanged write SetTotalSizeChanged; function StoreCaretPosition : Cardinal; procedure SetCaretPosition(aValue : Cardinal); // { TODO also remember (or recalculate) the bounding boxes of the characters as they have been the last time they were drawn. } //function GetCellBoundingBox(aOffset : TCellIndex) : TRect; //function GetCellByBoundingBox(aBoundingBox : TRect) : TCellIndex; //important function GetCellNearPoint(aPoint : TPoint) : TCellIndex; // TODO how to do TCellIndex -> buffer position conversion? // FIXME property Rectangle : TTextLayoutRectangle read GetRectangle write SetRectangle; //property RectangleOriginX : Cardinal read GetRectangleOriginX write SetRectangleOriginX; //property RectangleOriginY : Cardinal read GetRectangleOriginY write SetRectangleOriginY; //property RectangleWidth : Cardinal read GetRectangleWidth write SetRectangleWidth; //property RectangleHeight : Cardinal read GetRectangleHeight write SetRectangleHeight; procedure BeginSelectingAt(aX, aY : Cardinal); procedure EndSelectingAt(aX, aY : Cardinal); function FindOffsetNearVisualPosition(aX, aY : Cardinal; out oCharacterRect : TRect) : Cardinal; function FindRectangleContaining(aX, aY : Cardinal) : TTextLayoutRectangle; function GetDummyPainter() : IContinuousCharacterPainter; function FinishCharacter() : Boolean; // marks the currently queued character as finished and prints it. returns: True if it worked (enough space on screen etc). function GetCompletedInputCount : Cardinal; property CompletedInputCount : Cardinal read GetCompletedInputCount; // since BeginRound end; // TUnicodeCharacter = Cardinal; // see "encodings". TDummyContinuousCharacterPainter = class(TInterfacedObject, IContinuousCharacterPainter, IInterface) private fRectangle : TTextLayoutRectangle; fRectangleHeight : Cardinal; // use this in preference to fRectangle.Height. fRectangleWidth : Cardinal; // use this in preference to fRectangle.Width. This is a width dependent on the PositionY. fNextRectangles : ITextLayoutRectanglesIterator; fCurrentWidth : Cardinal; fCurrentHeight : Cardinal; fPositionX : Cardinal; fPositionY : Cardinal; fBreakPositionX : Cardinal; fBreakPositionY : Cardinal; fRowHeight : Cardinal; // height of the CURRENT row (until #"EmitLineFeed" is called). Useful for finding out where the next line can start. fRowSpacing : Cardinal; // setting for all rows, usually. fCharacterDecoder : ICharacterDecoder; fRingBufferStream : TRingBufferStream; fTotalSizeChanged : TContinuousCharacterPainterTotalSizeChanged; fCharacterCodepoints : TUnicodeCharacter; // codepoints of the current character. fCharacterCodepointInputCount : Cardinal; // how many bytes from the input were buffered? fCompletedInputCount : Cardinal; // how many bytes from the input were actually printed on screen? function GetCompletedInputCount : Cardinal; inline; protected fSelection : TSelectionRange; protected function GetCurrentWidth : Cardinal; procedure SetCurrentWidth(aValue : Cardinal); function GetCurrentHeight : Cardinal; procedure SetCurrentHeight(aValue : Cardinal); function GetRectangle : TTextLayoutRectangle; procedure SetRectangle(aValue : TTextLayoutRectangle); virtual; function GetNextRectangles : ITextLayoutRectanglesIterator; procedure SetNextRectangles(aValue : ITextLayoutRectanglesIterator); function GetPositionX : Cardinal; procedure SetPositionX(aValue : Cardinal); function GetPositionY : Cardinal; procedure SetPositionY(aValue : Cardinal); function GetRowHeight : Cardinal; procedure SetRowHeight(aValue : Cardinal); function GetRowSpacing : Cardinal; procedure SetRowSpacing(aValue : Cardinal); function GetBreakPositionX : Cardinal; procedure SetBreakPositionX(aValue : Cardinal); function GetBreakPositionY : Cardinal; procedure SetBreakPositionY(aValue : Cardinal); procedure GotoNextRectangle; procedure Flush; virtual; procedure FinishRound; virtual; function StoreCaretPosition : Cardinal; virtual; procedure SetCaretPosition(aValue : Cardinal); virtual; function GetTotalSizeChanged : TContinuousCharacterPainterTotalSizeChanged; procedure SetTotalSizeChanged(aValue : TContinuousCharacterPainterTotalSizeChanged); virtual; procedure EmitTotalSizeChanged(aSize : TSize); protected function BreakRectangleHeight() : Cardinal; function BreakRectangleWidth() : Cardinal; inline; public destructor Destroy; override; published constructor Create; // (aDeviceName : string { the terminal to use; TODO: use UNIX terminal fd }); procedure EmitLineFeed(); virtual; function ProcessItem(item : TTextBufferItem) : Boolean; function ProcessBlock(aBeginning : PChar; aSize : Cardinal) : Cardinal; // not neccessarily 0-terminated. procedure BeginRound; virtual; // TODO remove? function FindRectangleContaining(aX, aY : Cardinal) : TTextLayoutRectangle; protected // these support multi-character unicode thingies. procedure ProcessCharacter(); inline; function MeasureCharacter(const aCharacter : TUnicodeCharacter) : TSize; virtual; // this only works taking into account all the other state in this object. You have been warned. Do not cache. procedure RenderCharacter(const aCharacter : TUnicodeCharacter; aCount : Cardinal; aBeginning : Cardinal = 0); virtual; // no checks. published property CompletedInputCount : Cardinal read fCompletedInputCount; // since BeginRound property Width : Cardinal read fCurrentWidth write SetCurrentWidth; property Height : Cardinal read fCurrentHeight write SetCurrentHeight; property NextRectangles : ITextLayoutRectanglesIterator read GetNextRectangles write SetNextRectangles; property PositionX : Cardinal read fPositionX write SetPositionX; property PositionY : Cardinal read fPositionY write SetPositionY; property BreakPositionX : Cardinal read fBreakPositionX write SetBreakPositionX; property BreakPositionY : Cardinal read fBreakPositionY write SetBreakPositionY; property RowHeight : Cardinal read fRowHeight write SetRowHeight; { current row. } property RowSpacing : Cardinal read fRowSpacing write SetRowSpacing; property TotalSizeChanged : TContinuousCharacterPainterTotalSizeChanged read fTotalSizeChanged write SetTotalSizeChanged; procedure BeginSelectingAt(aX, aY : Cardinal); virtual; procedure EndSelectingAt(aX, aY : Cardinal); virtual; function FindOffsetNearVisualPosition(aX, aY : Cardinal; out oCharacterRect : TRect) : Cardinal; function GetDummyPainter() : IContinuousCharacterPainter; virtual; function FinishCharacter() : Boolean; // marks the currently queued character as finished and prints it. public property Rectangle : TTextLayoutRectangle read fRectangle write SetRectangle; end; const CardinalMaximum { : Cardinal } = $FFFFFFFF; // FIXME use some system constant. implementation uses lexer_interfaces, debug; { TDummyContinuousCharacterPainter. } constructor TDummyContinuousCharacterPainter.Create; // (aDeviceName : string { the terminal to use; TODO: use UNIX terminal fd }); begin fPositionX := 0; fPositionY := 0; fRingBufferStream := TRingBufferStream.Create; fCharacterDecoder := TUTF8ToUnicode.Create(fRingBufferStream); // TODO configurable? fRectangle.Init(); {fRectangle : TTextLayoutRectangle; fNextRectangles : ITextLayoutRectanglesIterator;} fCurrentWidth := 0; fCurrentHeight := 0; fBreakPositionX := CardinalMaximum; fBreakPositionY := CardinalMaximum; end; destructor TDummyContinuousCharacterPainter.Destroy; begin FreeAndNil(fRingBufferStream); inherited Destroy; end; function TDummyContinuousCharacterPainter.GetCurrentWidth : Cardinal; begin Result := fCurrentWidth; end; procedure TDummyContinuousCharacterPainter.SetCurrentWidth(aValue : Cardinal); begin fCurrentWidth := aValue; end; function TDummyContinuousCharacterPainter.GetCurrentHeight : Cardinal; begin Result := fCurrentHeight; end; procedure TDummyContinuousCharacterPainter.SetCurrentHeight(aValue : Cardinal); begin fCurrentHeight := aValue; end; function TDummyContinuousCharacterPainter.GetRectangle : TTextLayoutRectangle; begin Result := fRectangle; end; procedure TDummyContinuousCharacterPainter.SetRectangle(aValue : TTextLayoutRectangle); begin fRectangle := aValue; fRectangleHeight := BreakRectangleHeight(); fRectangleWidth := BreakRectangleWidth(); end; function TDummyContinuousCharacterPainter.GetNextRectangles : ITextLayoutRectanglesIterator; begin Result := fNextRectangles; end; procedure TDummyContinuousCharacterPainter.SetNextRectangles(aValue : ITextLayoutRectanglesIterator); begin fNextRectangles := aValue; end; function TDummyContinuousCharacterPainter.GetRowSpacing : Cardinal; begin Result := fRowSpacing; end; procedure TDummyContinuousCharacterPainter.SetRowSpacing(aValue : Cardinal); begin fRowSpacing := aValue; end; function TDummyContinuousCharacterPainter.GetRowHeight : Cardinal; begin Result := fRowHeight; end; procedure TDummyContinuousCharacterPainter.SetRowHeight(aValue : Cardinal); begin fRowHeight := aValue; end; function TDummyContinuousCharacterPainter.GetBreakPositionX : Cardinal; begin Result := fBreakPositionX; end; procedure TDummyContinuousCharacterPainter.SetBreakPositionX(aValue : Cardinal); begin fBreakPositionX := aValue; fRectangleHeight := BreakRectangleHeight(); fRectangleWidth := BreakRectangleWidth(); end; function TDummyContinuousCharacterPainter.GetBreakPositionY : Cardinal; begin Result := fBreakPositionY; end; procedure TDummyContinuousCharacterPainter.SetBreakPositionY(aValue : Cardinal); begin fBreakPositionY := aValue; fRectangleHeight := BreakRectangleHeight(); fRectangleWidth := BreakRectangleWidth(); end; function TDummyContinuousCharacterPainter.GetPositionX : Cardinal; begin Result := fPositionX; end; procedure TDummyContinuousCharacterPainter.SetPositionX(aValue : Cardinal); begin fPositionX := aValue; end; function TDummyContinuousCharacterPainter.GetPositionY : Cardinal; begin Result := fPositionY; end; procedure TDummyContinuousCharacterPainter.SetPositionY(aValue : Cardinal); begin fPositionY := aValue; fRectangleWidth := BreakRectangleWidth(); end; procedure TDummyContinuousCharacterPainter.GotoNextRectangle; begin raise TAreaOverrunException.Create('area overrun'); Exit; // FIXME. try if Assigned(fNextRectangles) then Rectangle := fNextRectangles.Next else raise TAreaOverrunException.Create('area overrun'); except raise TAreaOverrunException.Create('area overrun'); end; // raise TAreaOverrunException.Create('area overrun'); end; procedure TDummyContinuousCharacterPainter.EmitLineFeed(); begin // TODO take orientation into account (fRectangle.#"DirectionX", #"DirectionY"). PositionX := 0; if (fPositionY + fRowHeight) <= fRectangle.Height then begin PositionY := PositionY + fRowHeight + fRowSpacing; // TODO take orientation into account. // TODO line spacing. end else begin { doesn't fit, Y-direction-wise. } { try to go to next rectangle } end; // TODO auto-growable rectangles (optional). //if fPositionY > fCurrentHeight then // fCurrentHeight := fPositionY; fRowHeight := 1; // TODO just precalculate row height of each row. if fPositionY > fBreakPositionY then raise TAreaOverrunException.Create('area overrun'); end; { given fRectangle and fBreakPosition*, caps the height to the break position, if applicable. } function TDummyContinuousCharacterPainter.BreakRectangleHeight() : Cardinal; inline; begin Result := fRectangle.Height; if Result = 0 then Exit; Exit; if (fBreakPositionX >= fRectangle.OriginX) and (fBreakPositionY >= fRectangle.OriginY) then begin if fBreakPositionY > fRectangle.OriginY then begin Result := fBreakPositionY - fRectangle.OriginY; if Result > fRectangle.Height then Result := fRectangle.Height; end else Result := 0; end; end; function TDummyContinuousCharacterPainter.BreakRectangleWidth() : Cardinal; inline; begin Result := fRectangle.Width; if Result = 0 then Exit; Exit; if (fPositionY >= fBreakPositionY) then begin if fBreakPositionX > fRectangle.OriginX then begin Result := fBreakPositionX - fRectangle.OriginX; if Result > fRectangle.Width then Result := fRectangle.Width; end else Result := 0; end; end; // returns: whether it finally printed some character (not the one you passed!) function TDummyContinuousCharacterPainter.ProcessItem(item : TTextBufferItem) : Boolean; var fCodepoint : TUnicodeCodepoint; begin Result := False; //Dump('ProcessItem'); {if fRectangle = nil then begin Dump('overrun'); raise TAreaOverrunException.Create('area overrun'); end;} // TODO take orientation into account (fRectangle.#"DirectionX", #"DirectionY"). { TODO non-Unicode encodings?... } try fRingBufferStream.WriteByte(Ord(item)); //Dump(Format('TDummyContinuousCharacterPainter.ProcessItem: wrote %d to ring buffer', [Ord(item)])); except on E : TRingBufferFullException do begin //Dump('Ring buffer was full'); // ??? fRingBufferStream.Drain; end; end; if (fCharacterDecoder as ILexer).ConsumeOne <> TokenNone then begin fCodepoint := fCharacterDecoder.Codepoint; end else begin Dump('incomplete codepoint so far'); Exit; end; // here, a new Unicode codepoint was recognized. // this is here not because of some implementation difficulty but in order to avoid using up all available RAM with a malformed file. if Length(fCharacterCodepoints) >= 32 then begin Writeln(Length(fCharacterCodepoints)); fCodepoint := fCharacterCodepoints[0]; Writeln(fCodepoint); fCodepoint := fCharacterCodepoints[1]; Writeln(fCodepoint); fCodepoint := fCharacterCodepoints[2]; Writeln(fCodepoint); raise EOverflow.Create('too many codepoints for this character.'); end; // if the new codepoint is NOT a diacritical mark, flush out the previous character, if any, with its diacritical marks first. if IsBeginningOfNewCharacter(fCodepoint) then begin if not FinishCharacter() then raise TAreaOverrunException.Create('area overrun'); Result := True; end; // buffer the new codepoint. SetLength(fCharacterCodepoints, Length(fCharacterCodepoints) + 1); fCharacterCodepoints[High(fCharacterCodepoints)] := fCodepoint; Inc(fCharacterCodepointInputCount); end; function TDummyContinuousCharacterPainter.GetCompletedInputCount : Cardinal; inline; begin Result := fCompletedInputCount; end; function TDummyContinuousCharacterPainter.FinishCharacter() : Boolean; begin Result := True; if Length(fCharacterCodepoints) = 0 then Exit; try ProcessCharacter(); Inc(fCompletedInputCount, fCharacterCodepointInputCount); except on E : TAreaOverrunException do Result := False; end; SetLength(fCharacterCodepoints, 0); fCharacterCodepointInputCount := 0; end; { processes the character in #fCharacterCodepoints } procedure TDummyContinuousCharacterPainter.ProcessCharacter(); inline; var itemSize : TSize; fitsP : Boolean; fitsHorizontallyP : Boolean; begin if Length(fCharacterCodepoints) = 0 then Exit; //if fCodepoint = Ord(buffers.cLineBreak) then begin // in typical UNIX tradition, we allow in-bounds control characters, eek. // #"MeasureCharacter" is supposed to return a 0-width for this. //Self.EmitLineFeed(); //Exit; //end; // FIXME here, be careful to handle diacritical marks. These come AFTER the character to be modified (yeah, sick...) and there can be any number of them. itemSize := Self.MeasureCharacter(fCharacterCodepoints); fitsP := (itemSize.cx = 0) and (itemSize.cy = 0); fitsHorizontallyP := fitsP; if (not fitsP) or (not fitsHorizontallyP) then repeat fitsHorizontallyP := False; if (fPositionX + itemSize.cx) <= fRectangleWidth then begin { fits, X-direction-wise. } fitsHorizontallyP := True; end else begin { doesn't fit, X-direction-wise. } if fPositionX > 0 then begin Self.EmitLineFeed(); if (fPositionX + itemSize.cx) <= fRectangleWidth then begin { fits, X-direction-wise. } fitsHorizontallyP := True; end; end else begin Self.GotoNextRectangle; end; end; if (fPositionY + fRowHeight) <= fRectangleHeight then begin fitsP := True; end else begin { doesn't fit, Y-direction-wise. } { try to go to next rectangle } Self.GotoNextRectangle; // raises exception. end; until fitsP and fitsHorizontallyP; // or exception. // FIXME support other directions. if (fBreakPositionY < fPositionY) or ((fBreakPositionY < fPositionY + itemSize.cy) and (fBreakPositionX < fPositionX + itemSize.cx)) then //if {(fBreakPositionX >= fPositionX) and }(fBreakPositionX < fPositionX + itemSize.cx) and // {(fBreakPositionY >= fPositionY) and }(fBreakPositionY < fPositionY + itemSize.cy) then raise TAreaOverrunException.Create('area overrun'); Self.RenderCharacter(fCharacterCodepoints, Length(fCharacterCodepoints), 0); if fCharacterCodepoints[0] = Ord(buffers.cLineBreak) then begin // second part. Self.EmitLineFeed(); end; // adjust fRowHeight, if neccessary. if itemSize.cy > fRowHeight then fRowHeight := itemSize.cy; Inc(fPositionX, itemSize.cx); if fPositionX > fCurrentWidth then fCurrentWidth := fPositionX; //fCurrentPositionY fCurrentHeight end; function TDummyContinuousCharacterPainter.ProcessBlock(aBeginning : PChar; aSize : Cardinal) : Cardinal; // not neccessarily 0-terminated. var i : Cardinal; begin Result := 0; try if aSize > 0 then for i := 1 to aSize do begin Self.ProcessItem(aBeginning^); Result := i; Inc(aBeginning); end; except on exception : TAreaOverrunException do Exit; // Result := Result; end; end; function TDummyContinuousCharacterPainter.MeasureCharacter(const aCharacter : TUnicodeCharacter) : TSize; // this only works taking into account all the other state in this object. You have been warned. Do not cache. begin // TODO support other directions. // TODO support non-1 character widths. Result.cx := 1; Result.cy := 1; end; { render a character at (PositionX, PositionY). It is assured that there is enough room by the caller. Just draw the thing. } procedure TDummyContinuousCharacterPainter.RenderCharacter(const aCharacter : TUnicodeCharacter; aCount : Cardinal; aBeginning : Cardinal = 0); // no checks. begin end; procedure TDummyContinuousCharacterPainter.BeginRound; begin fCurrentWidth := 0; fCurrentHeight := 0; fPositionX := 0; fPositionY := 0; fRowHeight := 0; fRingBufferStream.Drain; // TODO nicer place. SetLength(fCharacterCodepoints, 0); // since I absolutely detest error messages popping up for unrelated items, I don't err here. fCharacterCodepointInputCount := 0; fCompletedInputCount := 0; end; procedure TDummyContinuousCharacterPainter.Flush; begin end; procedure TDummyContinuousCharacterPainter.FinishRound; begin FinishCharacter(); end; function TDummyContinuousCharacterPainter.StoreCaretPosition : Cardinal; begin Result := 0; end; procedure TDummyContinuousCharacterPainter.SetCaretPosition(aValue : Cardinal); begin end; function TDummyContinuousCharacterPainter.GetTotalSizeChanged : TContinuousCharacterPainterTotalSizeChanged; begin Result := fTotalSizeChanged; end; procedure TDummyContinuousCharacterPainter.SetTotalSizeChanged(aValue : TContinuousCharacterPainterTotalSizeChanged); begin fTotalSizeChanged := aValue; end; procedure TDummyContinuousCharacterPainter.EmitTotalSizeChanged(aSize : TSize); begin if Assigned(fTotalSizeChanged) then fTotalSizeChanged(Self, aSize); end; procedure TDummyContinuousCharacterPainter.BeginSelectingAt(aX, aY : Cardinal); begin with fSelection[0] do begin X := aX; Y := aY; end; end; procedure TDummyContinuousCharacterPainter.EndSelectingAt(aX, aY : Cardinal); begin with fSelection[1] do begin X := aX; Y := aY; end; end; function TDummyContinuousCharacterPainter.FindOffsetNearVisualPosition(aX, aY : Cardinal; out oCharacterRect : TRect) : Cardinal; begin Result := CardinalMaximum; end; function TDummyContinuousCharacterPainter.GetDummyPainter() : IContinuousCharacterPainter; begin Result := Self; end; function TDummyContinuousCharacterPainter.FindRectangleContaining(aX, aY : Cardinal) : TTextLayoutRectangle; begin Result := GetRectangle(); // FIXME more. end; end.