unit textfitters; interface uses classes, interfaces, buffers, buffers_export, marks; //type // clockwise. so Inc()able. //TTextLayoutDirection = (ldLeftToRight, ldTopToBottom, ldRightToLeft, ldBottomToTop); //function GetNextClockwiseTextLayoutDirection(aDirection : TTextLayoutDirection) : TTextLayoutDirection; // TODO non-rectangular layouts. type TTextLayoutRectangle = object // class OriginX : Cardinal; OriginY : Cardinal; Width : Cardinal; Height : Cardinal; // Rect : TRect; // ?? DirectionX : Integer; // -1, 0, 1 DirectionY : Integer; // -1, 0, 1 { TODO flags: growable X, growable Y } constructor Init(); end; PTextLayoutRectangle = ^TTextLayoutRectangle; ITextLayoutRectanglesIterator = interface(ISimpleIterator) function Next : TTextLayoutRectangle; end; {TTextLayoutRectanglesIterator = class(TInterfacedObject, ISimpleIterator, ITextLayoutRectanglesIterator) function Next : TTextLayoutRectangle; // x1, y1, x2, y2 : Cardinal // guaranteed to be in lexically useful ascending order (usually y < Next.y). end;} // TODO better just pass a stream to begin with. implementation uses lexer_interfaces, words; constructor TTextLayoutRectangle.Init; begin OriginX := 0; OriginY := 0; Width := 0; Height := 0; // Rect : TRect; // ?? DirectionX := 1; DirectionY := 1; end; {function GetNextClockwiseTextLayoutDirection(aDirection : TTextLayoutDirection) : TTextLayoutDirection; begin if aDirection = ldBottomToTop then Result := ldLeftToRight else Result := TTextLayoutDirection(Integer(aDirection) + 1); end;} procedure X; var wordBreakLexer : ILexer; begin wordBreakLexer.SourceStream := nil; // FIXME. while (not wordBreakLexer.EOF) do begin case wordBreakLexer.Token of words.TokenWord: ; words.TokenWhitespace: ; end; wordBreakLexer.MatchBeginning; assert(wordBreakLexer.MatchEnd = (wordBreakLexer as ISourcePosition).SourcePosition); wordBreakLexer.Consume; end; end; { #"aBeginning" is the beginning text mark to start outputting at (after adding #"aOffset" to it). #"aOffset" is the offset to add to #"aBeginning" before doing anything with it. usually advances #"aRectangles" by calling its #"Next" method (except when #"aCurrentRectangle" sufficed). } procedure LayTextOut(aBeginning : ITextMark; aOffset : Cardinal; aRectangle : TTextLayoutRectangle; aPosition : TPoint; aRectangles : ITextLayoutRectanglesIterator); begin {...} end; { lay out text at #"aBeginning", reading and using at most #"aTotalSize" bytes from there. also increases #"aBeginning" as it uses up characters. TODO maybe use the text buffer iterator interface for this? TODO sub-class painting in one rectangle. } procedure XLayTextOut(var aBeginning : PChar; var aTotalSize : Cardinal; aRectangle : TTextLayoutRectangle; aPosition : TPoint; aRectangles : ITextLayoutRectanglesIterator); begin { A view has a 1) top left beginning text mark. Starting from this text mark, the paint method will start painting word by word. "Word" is something to be determined either by searching for spaces or something to be determined by custom parsing rules to find a good word-break point. Note that the beginning text mark can point into the middle of a word or whatever. This can happen if a word is too long to be split at all. Therefore there is a word-break-point state machine. This state machine should also work backwards? Depending on text attibutes and, if small, text length etc there's an optimal number of characters to begin trying to fit into a single line. Should it not fit or leave room, binary search will find the actual number, taking into account the word boundaries. Many optimizations are by paragraph, therefore there is a text mark on the beginning of each paragraph. Many of the state machines will auto-reset on each paragraph break. Others don't. So in effect and to be mostly efficient, do things in this order: 0) maybe try adding big chunks of the buffer into a single line first. 1) find words since words are mostly independent anyway. Should a word be too long (i.e. longer than a fixed length), find sub-words for it instead, using the word-break state machine, using longer parts first (do not forget to count the dash immediately in this case!). Caching (if any) should take into account: attributes (font size, font style, font name). If attributes change, take that as a word boundary. 2) for as long as possible lay (sub-)words out in the rectangle, increasing the x (or decreasing x, or increasing y, ..., depending on text direction). Because step 1 found either words or sub-words at word-break points, we will not need to retract any of the items laid out in this manner. After that, allot one space for the dash, try to use as many of sub-word-found-by-word-breaking parts (without dash) as possible, finally add the dash. Then do perpendicular movement in order to go to the next row here. If you cannot fit the sub-word anywhere and reach the end of all available rectangles, a fallback mode is used that lays the text out one character at a time. // CharacterWidthEstimator } end; end.