HaikuDepot: Re-implemented TextLayout as ParagraphLayout

* Completely unfininished and untested, won't even compile,
   but is not included in the build. The idea being to wrap TextSpan
   objects and split them onto Lines each containing their own
   layouted TextSpan chunks. TextLayout will later be repurposed
   to off-load the layout of individual text paragraphs to
   ParagraphLayouts.
This commit is contained in:
Stephan Aßmus
2013-09-02 12:15:18 +02:00
parent 947ba26bd0
commit 15a557e69c
2 changed files with 731 additions and 0 deletions
@@ -0,0 +1,596 @@
/*
* Copyright 2001-2013, Haiku, Inc. All rights reserved.
* Distributed under the terms of the MIT License.
*
* Authors:
* Stephan Aßmus, [email protected]
* Stefano Ceccherini, [email protected]
* Marc Flerackers, [email protected]
* Hiroshi Lockheimer (BTextView is based on his STEEngine)
* Oliver Tappe, [email protected]
*/
#include "ParagraphLayout.h"
#include <new>
#include <stdio.h>
#include <AutoDeleter.h>
#include <utf8_functions.h>
#include <View.h>
#include "GlyphInfo.h"
#include "List.h"
#include "TextStyle.h"
enum {
CHAR_CLASS_DEFAULT,
CHAR_CLASS_WHITESPACE,
CHAR_CLASS_GRAPHICAL,
CHAR_CLASS_QUOTE,
CHAR_CLASS_PUNCTUATION,
CHAR_CLASS_PARENS_OPEN,
CHAR_CLASS_PARENS_CLOSE,
CHAR_CLASS_END_OF_TEXT
};
inline uint32
get_char_classification(uint32 charCode)
{
// TODO: Should check against a list of characters containing also
// word breakers from other languages.
switch (charCode) {
case '\0':
return CHAR_CLASS_END_OF_TEXT;
case ' ':
case '\t':
case '\n':
return CHAR_CLASS_WHITESPACE;
case '=':
case '+':
case '@':
case '#':
case '$':
case '%':
case '^':
case '&':
case '*':
case '\\':
case '|':
case '<':
case '>':
case '/':
case '~':
return CHAR_CLASS_GRAPHICAL;
case '\'':
case '"':
return CHAR_CLASS_QUOTE;
case ',':
case '.':
case '?':
case '!':
case ';':
case ':':
case '-':
return CHAR_CLASS_PUNCTUATION;
case '(':
case '[':
case '{':
return CHAR_CLASS_PARENS_OPEN;
case ')':
case ']':
case '}':
return CHAR_CLASS_PARENS_CLOSE;
default:
return CHAR_CLASS_DEFAULT;
}
}
inline bool
can_end_line(const GlyphInfo* buffer, int offset, int count)
{
if (offset == count - 1)
return true;
if (offset < 0 || offset > count)
return false;
uint32 charCode = buffer[offset].charCode;
uint32 classification = get_char_classification(charCode);
// wrapping is always allowed at end of text and at newlines
if (classification == CHAR_CLASS_END_OF_TEXT || charCode == '\n')
return true;
uint32 nextCharCode = buffer[offset + 1].charCode;
uint32 nextClassification = get_char_classification(nextCharCode);
// never separate a punctuation char from its preceding word
if (classification == CHAR_CLASS_DEFAULT
&& nextClassification == CHAR_CLASS_PUNCTUATION) {
return false;
}
if ((classification == CHAR_CLASS_WHITESPACE
&& nextClassification != CHAR_CLASS_WHITESPACE)
|| (classification != CHAR_CLASS_WHITESPACE
&& nextClassification == CHAR_CLASS_WHITESPACE)) {
return true;
}
// allow wrapping after whitespace, unless more whitespace (except for
// newline) follows
if (classification == CHAR_CLASS_WHITESPACE
&& (nextClassification != CHAR_CLASS_WHITESPACE
|| nextCharCode == '\n')) {
return true;
}
// allow wrapping after punctuation chars, unless more punctuation, closing
// parenthesis or quotes follow
if (classification == CHAR_CLASS_PUNCTUATION
&& nextClassification != CHAR_CLASS_PUNCTUATION
&& nextClassification != CHAR_CLASS_PARENS_CLOSE
&& nextClassification != CHAR_CLASS_QUOTE) {
return true;
}
// allow wrapping after quotes, graphical chars and closing parenthesis only
// if whitespace follows (not perfect, but seems to do the right thing most
// of the time)
if ((classification == CHAR_CLASS_QUOTE
|| classification == CHAR_CLASS_GRAPHICAL
|| classification == CHAR_CLASS_PARENS_CLOSE)
&& nextClassification == CHAR_CLASS_WHITESPACE) {
return true;
}
return false;
}
// #pragma mark - ParagraphLayout
ParagraphLayout::ParagraphLayout()
:
fTextSpans(),
fWidth(0.0f),
fLayoutValid(false),
fGlyphInfoBuffer(NULL),
fGlpyhInfoBufferSize(0),
fGlyphInfoCount(0),
fLayoutLines()
{
}
ParagraphLayout::ParagraphLayout(const Paragraph& paragraph)
:
fTextSpans(paragraph.TextSpans()),
fWidth(other.fWidth),
fLayoutValid(false),
fGlyphInfoBuffer(NULL),
fGlpyhInfoBufferSize(0),
fGlyphInfoCount(0),
fLayoutLines()
{
_Init();
}
ParagraphLayout::ParagraphLayout(const ParagraphLayout& other)
:
fTextSpans(other.fTextSpans),
fWidth(other.fWidth),
fLayoutValid(false),
fGlyphInfoBuffer(NULL),
fGlpyhInfoBufferSize(0),
fGlyphInfoCount(0),
fLayoutLines()
{
_Init();
}
ParagraphLayout::~ParagraphLayout()
{
delete[] fGlyphInfoBuffer;
}
void
ParagraphLayout::SetWidth(float width)
{
if (fWidth != width) {
fWidth = width;
fLayoutValid = false;
}
}
float
ParagraphLayout::Height()
{
_ValidateLayout();
float height = 0.0f;
if (fLayoutLines.CountItems() > 0) {
const LayoutLine& lastLine = fLayoutLines.LastItem();
height = lastLine.y + lastLine.height;
}
return height;
}
void
ParagraphLayout::Draw(BView* view, const BPoint& offset)
{
_ValidateLayout();
int lineCount = fLayoutLines.CountItems();
for (int i = 0; i < lineCount; i++) {
const LayoutLine& line = fLayoutLines.ItemAtFast(i);
_DrawLine(view, line);
}
}
// #pragma mark - private
void
ParagraphLayout::_ValidateLayout()
{
if (!fLayoutValid)
_Layout();
}
void
ParagraphLayout::_Layout()
{
fLayoutLines.Clear();
// int charCount = fText.CountChars();
// if (charCount == 0)
// return;
//
// // Allocate arrays
// float* escapementArray = new (std::nothrow) float[charCount];
// if (escapementArray == NULL)
// return;
// ArrayDeleter<float> escapementDeleter(escapementArray);
//
// // Fetch glyph spacing information
// fDefaultFont.GetEscapements(fText.String(), charCount, escapementArray);
//
// // Convert to glyph buffer
// fGlyphInfoCount = charCount;
// fGlyphInfoBuffer = new (std::nothrow) GlyphInfo[fGlyphInfoCount];
//
// // Init glyph buffer and convert escapement scale
// float size = fDefaultFont.Size();
// const char* c = fText.String();
// for (int i = 0; i < fGlyphInfoCount; i++) {
// fGlyphInfoBuffer[i].charCode = UTF8ToCharCode(&c);
// escapementArray[i] *= size;
// }
//
//
// float x = fFirstLineInset;
// float y = 0.0f;
// int lineIndex = 0;
// int lineStart = 0;
//
// for (int i = 0; i < fGlyphInfoCount; i++) {
// uint32 charClassification = get_char_classification(
// fGlyphInfoBuffer[i].charCode);
//
// float advanceX = 0.0f;
// float advanceY = 0.0f;
// _GetGlyphAdvance(i, advanceX, advanceY, escapementArray);
//
// bool nextLine = false;
// bool lineBreak = false;
//
// if (fGlyphInfoBuffer[i].charCode == '\t') {
// // Figure out tab width, it's the width between the last two tab
// // stops.
// float tabWidth = 0.0f;
// if (fTabCount > 0)
// tabWidth = fTabBuffer[fTabCount - 1];
// if (fTabCount > 1)
// tabWidth -= fTabBuffer[fTabCount - 2];
//
// // Try to find a tab stop that is farther than the current x
// // offset
// double tabOffset = 0.0;
// for (unsigned tabIndex = 0; tabIndex < fTabCount; tabIndex++) {
// tabOffset = fTabBuffer[tabIndex];
// if (tabOffset > x)
// break;
// }
//
// // If no tab stop has been found, make the tab stop a multiple of
// // the tab width
// if (tabOffset <= x && tabWidth > 0.0)
// tabOffset = ((int) (x / tabWidth) + 1) * tabWidth;
//
// if (tabOffset - x > 0.0)
// advanceX = tabOffset - x;
// }
//
// fGlyphInfoBuffer[i].advanceX = advanceX;
//
// if (fGlyphInfoBuffer[i].charCode == '\n') {
// nextLine = true;
// lineBreak = true;
// fGlyphInfoBuffer[i].x = x;
// fGlyphInfoBuffer[i].y = y;
// } else if (fWidth > 0.0f && x + advanceX > fWidth) {
// if (charClassification == CHAR_CLASS_WHITESPACE) {
// advanceX = 0.0f;
// } else if (i > lineStart) {
// nextLine = true;
// // The current glyph extends outside the width, we need to wrap
// // to the next line. See what previous offset can be the end
// // of the line.
// int lineEnd = i - 1;
// while (lineEnd > lineStart
// && !can_end_line(fGlyphInfoBuffer, lineEnd,
// fGlyphInfoCount)) {
// lineEnd--;
// }
//
// if (lineEnd > lineStart) {
// // Found a place to perform a line break.
// i = lineEnd + 1;
// // Adjust the glyph info to point at the changed buffer
// // position
// _GetGlyphAdvance(i, advanceX, advanceY, escapementArray);
// } else {
// // Just break where we are.
// }
// }
// }
//
// if (nextLine) {
// // * Initialize the max ascent/descent of all preceding glyph infos
// // on the current/last line
// // * Adjust the baseline offset according to the max ascent
// // * Fill in the line index.
// unsigned lineEnd;
// if (lineBreak)
// lineEnd = i;
// else
// lineEnd = i - 1;
//
// float lineHeight = 0.0;
// _FinalizeLine(lineStart, lineEnd, lineIndex, y,
// lineHeight);
//
// // Start position of the next line
// x = fLineInset;
// y += lineHeight + fLineSpacing;
//
// if (lineBreak)
// lineStart = i + 1;
// else
// lineStart = i;
//
// lineIndex++;
// }
//
// if (!lineBreak && i < fGlyphInfoCount) {
// fGlyphInfoBuffer[i].x = x;
// fGlyphInfoBuffer[i].y = y;
// }
//
// x += advanceX;
// y += advanceY;
// }
//
// // The last line may not have been appended and initialized yet.
// if (lineStart <= fGlyphInfoCount - 1) {
// float lineHeight;
// _FinalizeLine(lineStart, fGlyphInfoCount - 1, lineIndex, y, lineHeight);
// }
}
void
ParagraphLayout::_Init()
{
delete[] fGlyphInfoBuffer;
fGlyphInfoBuffer = NULL;
fGlpyhInfoBufferSize = 0;
fGlyphInfoCount = 0;
int spanCount = fTextSpans.CountItems();
for (int i = 0; i < spanCount; i++) {
const TextSpan& span = fTextSpans.ItemAtFast(i);
if (!_AppendGlyphInfos(span)) {
fprintf(stderr, "%p->ParagraphLayout::_Init() - Out of memory\n",
this);
return;
}
}
}
bool
ParagraphLayout::_AppendGlyphInfos(const TextSpan& span)
{
int charCount = span.CharCount();
if (charCount == 0)
return true;
const BString& text = span.Text();
const BFont& font = span.Style().font;
// Allocate arrays
float* escapementArray = new (std::nothrow) float[charCount];
if (escapementArray == NULL)
return false;
ArrayDeleter<float> escapementDeleter(escapementArray);
// Fetch glyph spacing information
font.GetEscapements(text, charCount, escapementArray);
// Append to glyph buffer and convert escapement scale
float size = font.Size();
const char* c = text.String();
for (int i = 0; i < charCount; i++) {
_AppendGlyphInfo(UTF8ToCharCode(&c), );
fGlyphInfoBuffer[i].charCode = ;
escapementArray[i] *= size;
}
}
bool
ParagraphLayout::_AppendGlyphInfo(uint32 charCode, const TextStyleRef& style)
{
// Enlarge buffer if necessary
if (fGlyphInfoCount == fGlyphInfoBufferSize) {
int size = fGlyphInfoBufferSize + 64;
GlyphInfo* buffer = (GlyphInfo*) realloc(fGlyphInfoBuffer,
size * sizeof(GlyphInfo));
if (buffer == NULL)
return false;
fGlyphInfoBufferSize = size;
fGlyphInfoBuffer = buffer;
}
// Store given information
fGlyphInfoBuffer[fGlyphInfoCount].charCode = charCode;
fGlyphInfoBuffer[fGlyphInfoCount].glyph = glyph;
fGlyphInfoBuffer[fGlyphInfoCount].x = 0;
fGlyphInfoBuffer[fGlyphInfoCount].y = 0;
fGlyphInfoBuffer[fGlyphInfoCount].advanceX = 0;
fGlyphInfoBuffer[fGlyphInfoCount].maxAscend = 0;
fGlyphInfoBuffer[fGlyphInfoCount].maxDescend = 0;
fGlyphInfoBuffer[fGlyphInfoCount].lineIndex = 0;
fGlyphInfoBuffer[fGlyphInfoCount].styleRun = styleRun;
fGlyphInfoCount++;
return true;
}
void
ParagraphLayout::_GetGlyphAdvance(int offset, float& advanceX, float& advanceY,
float escapementArray[]) const
{
float additionalGlyphSpacing = 0.0f;
if (fGlyphInfoBuffer[offset].style.Get() != NULL)
additionalGlyphSpacing = fGlyphInfoBuffer[offset].style->glyphSpacing;
else
additionalGlyphSpacing = fGlyphSpacing;
if (fGlyphInfoBuffer[offset].style.Get() != NULL
&& fGlyphInfoBuffer[offset].style->width > 0.0f) {
// Use the metrics provided by the TextStyle
advanceX = fGlyphInfoBuffer[offset].style->width
+ additionalGlyphSpacing;
} else {
advanceX = escapementArray[offset] + additionalGlyphSpacing;
advanceY = 0.0f;
}
}
void
ParagraphLayout::_FinalizeLine(int lineStart, int lineEnd, int lineIndex, float y,
float& lineHeight)
{
lineHeight = 0.0f;
float maxAscent = 0.0f;
float maxDescent = 0.0f;
for (int i = lineStart; i <= lineEnd; i++) {
if (fGlyphInfoBuffer[i].style.Get() != NULL) {
if (fGlyphInfoBuffer[i].style->font.Size() > lineHeight)
lineHeight = fGlyphInfoBuffer[i].style->font.Size();
if (fGlyphInfoBuffer[i].style->ascent > maxAscent)
maxAscent = fGlyphInfoBuffer[i].style->ascent;
if (fGlyphInfoBuffer[i].style->descent > maxDescent)
maxDescent = fGlyphInfoBuffer[i].style->descent;
} else {
if (fDefaultFont.Size() > lineHeight)
lineHeight = fDefaultFont.Size();
if (fAscent > maxAscent)
maxAscent = fAscent;
if (fDescent > maxDescent)
maxDescent = fDescent;
}
}
fLayoutLines.Add(LayoutLine(lineStart, y, lineHeight, maxAscent, maxDescent));
for (int i = lineStart; i <= lineEnd; i++) {
fGlyphInfoBuffer[i].maxAscent = maxAscent;
fGlyphInfoBuffer[i].maxDescent = maxDescent;
fGlyphInfoBuffer[i].lineIndex = lineIndex;
fGlyphInfoBuffer[i].y += maxAscent;
}
}
void
ParagraphLayout::_DrawLine(BView* view, const LayoutLine& line) const
{
int textOffset = line.textOffset;
int spanCount = line.layoutedSpans.CountItems();
for (int i = 0; i < spanCount) {
const TextSpan& span = line.layoutedSpans.ItemAtFast(i);
_DrawSpan(view, span, textOffset);
textOffset += span.CharCount();
}
}
void
ParagraphLayout::_DrawSpan(BView* view, const TextSpan& span,
int textOffset) const
{
float x = fGlyphInfoBuffer[textOffset].x;
float y = fGlyphInfoBuffer[textOffset].y;
const TextStyle& style = span.Style();
view->SetFont(&style.font);
view->SetHighColor(style.fgColor);
// TODO: Implement other style properties
// TODO: Implement correct glyph spacing
view->DrawString(span.Text(), BPoint(x, y));
}
@@ -0,0 +1,135 @@
/*
* Copyright 2013, Stephan Aßmus <[email protected]>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#ifndef PARAGRAPH_LAYOUT_H
#define PARAGRAPH_LAYOUT_H
#include <Font.h>
#include <Referenceable.h>
#include <String.h>
#include "GlyphInfo.h"
#include "Paragraph.h"
class BView;
class LayoutLine {
public:
LayoutLine()
:
textOffset(0),
y(0.0f),
height(0.0f),
maxAscent(0.0f),
maxDescent(0.0f)
{
}
LayoutLine(const LayoutLine& other)
:
textOffset(other.textOffset),
y(other.y),
height(other.height),
maxAscent(other.maxAscent),
maxDescent(other.maxDescent),
layoutedSpans(other.layoutedSpans)
{
}
LayoutLine& operator=(const LayoutLine& other)
{
textOffset = other.textOffset;
y = other.y;
height = other.height;
maxAscent = other.maxAscent;
maxDescent = other.maxDescent;
layoutedSpans = other.layoutedSpans;
return *this;
}
bool operator==(const LayoutLine& other) const
{
return textOffset == other.textOffset
&& y == other.y
&& height == other.height
&& maxAscent == other.maxAscent
&& maxDescent == other.maxDescent
&& layoutedSpans == otner.layoutedSpans;
}
bool operator!=(const LayoutLine& other) const
{
return !(*this == other);
}
public:
int textOffset;
float y;
float height;
float maxAscent;
float maxDescent;
TextSpanList layoutedSpans;
};
typedef List<LayoutLine, false> LayoutLineList;
class ParagraphLayout : public BReferenceable {
public:
ParagraphLayout();
ParagraphLayout(const Paragraph& paragraph);
ParagraphLayout(const ParagraphLayout& other);
virtual ~ParagraphLayout();
void SetParagraph(const ::Paragraph& paragraph);
const ::Paragraph& Paragraph() const
{ return fParagraph; }
void SetWidth(float width);
float Width() const
{ return fWidth; }
float Height();
void Draw(BView* view, const BPoint& offset);
private:
void _ValidateLayout();
void _Layout();
void _Init();
bool _AppendGlyphInfos(const TextSpan& span);
bool _AppendGlyphInfo(uint32 charCode,
const TextStyleRef& span);
void _GetGlyphAdvance(int offset,
float& advanceX, float& advanceY,
float escapementArray[]) const;
void _FinalizeLine(int lineStart, int lineEnd,
int lineIndex, float y, float& lineHeight);
void _DrawLine(BView* view,
const LayoutLine& line) const;
void _DrawSpan(BView* view, const TextSpan& span,
int textOffset) const;
private:
TextSpanList fTextSpans;
float fWidth;
bool fLayoutValid;
GlyphInfo* fGlyphInfoBuffer;
int fGlpyhInfoBufferSize;
int fGlyphInfoCount;
LayoutLineList fLayoutLines;
};
#endif // PARAGRAPH_LAYOUT_H