/* * Copyright 2001-2013, Haiku, Inc. All rights reserved. * Distributed under the terms of the MIT License. * * Authors: * Stephan Aßmus, superstippi@gmx.de * Stefano Ceccherini, stefano.ceccherini@gmail.com * Marc Flerackers, mflerackers@androme.be * Hiroshi Lockheimer (BTextView is based on his STEEngine) * Oliver Tappe, zooey@hirschkaefer.de * Andrew Lindesay, apl@lindesay.co.nz */ #include "ParagraphLayout.h" #include #include #include #include #include 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 std::vector& glyphInfos, int offset) { int count = static_cast(glyphInfos.size()); if (offset == count - 1) return true; if (offset < 0 || offset > count) return false; uint32 charCode = glyphInfos[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 = glyphInfos[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(), fParagraphStyle(), fWidth(0.0f), fLayoutValid(false), fGlyphInfos(), fLineInfos() { } ParagraphLayout::ParagraphLayout(const Paragraph& paragraph) : fTextSpans(), fParagraphStyle(paragraph.Style()), fWidth(0.0f), fLayoutValid(false), fGlyphInfos(), fLineInfos() { _AppendTextSpans(paragraph); _Init(); } ParagraphLayout::ParagraphLayout(const ParagraphLayout& other) : fTextSpans(other.fTextSpans), fParagraphStyle(other.fParagraphStyle), fWidth(other.fWidth), fLayoutValid(false), fGlyphInfos(other.fGlyphInfos), fLineInfos() { } ParagraphLayout::~ParagraphLayout() { } void ParagraphLayout::SetParagraph(const Paragraph& paragraph) { fTextSpans.clear(); _AppendTextSpans(paragraph); fParagraphStyle = paragraph.Style(); _Init(); fLayoutValid = false; } void ParagraphLayout::SetWidth(float width) { if (fWidth != width) { fWidth = width; fLayoutValid = false; } } float ParagraphLayout::Height() { _ValidateLayout(); float height = 0.0f; if (!fLineInfos.empty()) { const LineInfo& lastLine = fLineInfos[fLineInfos.size() - 1]; height = lastLine.y + lastLine.height; } return height; } void ParagraphLayout::Draw(BView* view, const BPoint& offset) { _ValidateLayout(); int lineCount = static_cast(fLineInfos.size()); for (int i = 0; i < lineCount; i++) { const LineInfo& line = fLineInfos[i]; _DrawLine(view, offset, line); } const Bullet& bullet = fParagraphStyle.Bullet(); if (bullet.Spacing() > 0.0f && bullet.String().Length() > 0) { // Draw bullet at offset view->SetHighUIColor(B_PANEL_TEXT_COLOR); BPoint bulletPos(offset); bulletPos.x += fParagraphStyle.FirstLineInset() + fParagraphStyle.LineInset(); bulletPos.y += fLineInfos[0].maxAscent; view->DrawString(bullet.String(), bulletPos); } } int32 ParagraphLayout::CountGlyphs() const { return static_cast(fGlyphInfos.size()); } int32 ParagraphLayout::CountLines() { _ValidateLayout(); return static_cast(fLineInfos.size()); } int32 ParagraphLayout::LineIndexForOffset(int32 textOffset) { _ValidateLayout(); if (fGlyphInfos.empty()) return 0; if (textOffset >= static_cast(fGlyphInfos.size())) { const GlyphInfo& glyph = fGlyphInfos[fGlyphInfos.size() - 1]; return glyph.lineIndex; } if (textOffset < 0) textOffset = 0; const GlyphInfo& glyph = fGlyphInfos[textOffset]; return glyph.lineIndex; } int32 ParagraphLayout::FirstOffsetOnLine(int32 lineIndex) { _ValidateLayout(); if (lineIndex < 0) lineIndex = 0; int32 countLineInfos = static_cast(fLineInfos.size()); if (lineIndex >= countLineInfos) lineIndex = countLineInfos - 1; return fLineInfos[lineIndex].textOffset; } int32 ParagraphLayout::LastOffsetOnLine(int32 lineIndex) { _ValidateLayout(); if (lineIndex < 0) lineIndex = 0; if (lineIndex >= static_cast(fLineInfos.size()) - 1) return CountGlyphs() - 1; return fLineInfos[lineIndex + 1].textOffset - 1; } void ParagraphLayout::GetLineBounds(int32 lineIndex, float& x1, float& y1, float& x2, float& y2) { _ValidateLayout(); if (fGlyphInfos.empty()) { _GetEmptyLayoutBounds(x1, y1, x2, y2); return; } if (lineIndex < 0) lineIndex = 0; int32 countLineInfos = static_cast(fLineInfos.size()); if (lineIndex >= countLineInfos) lineIndex = countLineInfos - 1; const LineInfo& lineInfo = fLineInfos[lineIndex]; int32 firstGlyphIndex = lineInfo.textOffset; int32 lastGlyphIndex; if (lineIndex < countLineInfos - 1) lastGlyphIndex = fLineInfos[lineIndex + 1].textOffset - 1; else lastGlyphIndex = static_cast(fGlyphInfos.size()) - 1; const GlyphInfo& firstInfo = fGlyphInfos[firstGlyphIndex]; const GlyphInfo& lastInfo = fGlyphInfos[lastGlyphIndex]; x1 = firstInfo.x; y1 = lineInfo.y; x2 = lastInfo.x + lastInfo.width; y2 = lineInfo.y + lineInfo.height; } void ParagraphLayout::GetTextBounds(int32 textOffset, float& x1, float& y1, float& x2, float& y2) { _ValidateLayout(); if (fGlyphInfos.empty()) { _GetEmptyLayoutBounds(x1, y1, x2, y2); return; } if (textOffset >= static_cast(fGlyphInfos.size())) { const GlyphInfo& glyph = fGlyphInfos[fGlyphInfos.size() - 1]; const LineInfo& line = fLineInfos[glyph.lineIndex]; x1 = glyph.x + glyph.width; x2 = x1; y1 = line.y; y2 = y1 + line.height; return; } if (textOffset < 0) textOffset = 0; const GlyphInfo& glyph = fGlyphInfos[textOffset]; const LineInfo& line = fLineInfos[glyph.lineIndex]; x1 = glyph.x; x2 = x1 + glyph.width; y1 = line.y; y2 = y1 + line.height; } int32 ParagraphLayout::TextOffsetAt(float x, float y, bool& rightOfCenter) { _ValidateLayout(); rightOfCenter = false; int32 lineCount = static_cast(fLineInfos.size()); if (fGlyphInfos.empty() || lineCount == 0 || fLineInfos[0].y > y) { // Above first line or empty text return 0; } int32 lineIndex = 0; LineInfo lastLineInfo = fLineInfos[fLineInfos.size() - 1]; if (floorf(lastLineInfo.y + lastLineInfo.height + 0.5) > y) { // TODO: Optimize, can binary search line here: for (; lineIndex < lineCount; lineIndex++) { const LineInfo& line = fLineInfos[lineIndex]; float lineBottom = floorf(line.y + line.height + 0.5); if (lineBottom > y) break; } } else { lineIndex = lineCount - 1; } // Found line const LineInfo& line = fLineInfos[lineIndex]; int32 textOffset = line.textOffset; int32 end; if (lineIndex < lineCount - 1) end = fLineInfos[lineIndex + 1].textOffset - 1; else end = fGlyphInfos.size() - 1; // TODO: Optimize, can binary search offset here: for (; textOffset <= end; textOffset++) { const GlyphInfo& glyph = fGlyphInfos[textOffset]; float x1 = glyph.x; if (x1 > x) return textOffset; // x2 is the location at the right bounding box of the glyph float x2 = x1 + glyph.width; // x3 is the location of the next glyph, which may be different from // x2 in case the line is justified. float x3; if (textOffset < end - 1) x3 = fGlyphInfos[textOffset + 1].x; else x3 = x2; if (x3 > x) { rightOfCenter = x > (x1 + x2) / 2.0f; return textOffset; } } // Account for trailing line break at end of line, the // returned offset should be before that. rightOfCenter = fGlyphInfos[end].charCode != '\n'; return end; } // #pragma mark - private void ParagraphLayout::_Init() { fGlyphInfos.clear(); std::vector::const_iterator it; for (it = fTextSpans.begin(); it != fTextSpans.end(); it++) { const TextSpan& span = *it; if (!_AppendGlyphInfos(span)) { fprintf(stderr, "%p->ParagraphLayout::_Init() - Out of memory\n", this); return; } } } void ParagraphLayout::_ValidateLayout() { if (!fLayoutValid) { _Layout(); fLayoutValid = true; } } void ParagraphLayout::_Layout() { fLineInfos.clear(); const Bullet& bullet = fParagraphStyle.Bullet(); float x = fParagraphStyle.LineInset() + fParagraphStyle.FirstLineInset() + bullet.Spacing(); float y = 0.0f; int lineIndex = 0; int lineStart = 0; int glyphCount = static_cast(fGlyphInfos.size()); for (int i = 0; i < glyphCount; i++) { GlyphInfo glyph = fGlyphInfos[i]; uint32 charClassification = get_char_classification(glyph.charCode); float advanceX = glyph.width; float advanceY = 0.0f; bool nextLine = false; bool lineBreak = false; // if (glyph.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; // } if (glyph.charCode == '\n') { nextLine = true; lineBreak = true; glyph.x = x; fGlyphInfos[i] = glyph; } else if (fWidth > 0.0f && x + advanceX > fWidth) { fGlyphInfos[i] = glyph; 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(fGlyphInfos, lineEnd)) { 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 glyph = fGlyphInfos[i]; advanceX = glyph.width; } 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 = fParagraphStyle.LineInset() + bullet.Spacing(); y += lineHeight + fParagraphStyle.LineSpacing(); if (lineBreak) lineStart = i + 1; else lineStart = i; lineIndex++; } if (!lineBreak && i < glyphCount) { glyph.x = x; fGlyphInfos[i] = glyph; } x += advanceX; y += advanceY; } // The last line may not have been appended and initialized yet. if (lineStart <= glyphCount - 1 || glyphCount == 0) { float lineHeight; _FinalizeLine(lineStart, glyphCount - 1, lineIndex, y, lineHeight); } _ApplyAlignment(); } void ParagraphLayout::_ApplyAlignment() { Alignment alignment = fParagraphStyle.Alignment(); bool justify = fParagraphStyle.Justify(); if (alignment == ALIGN_LEFT && !justify) return; int glyphCount = static_cast(fGlyphInfos.size()); if (glyphCount == 0) return; int lineIndex = -1; float spaceLeft = 0.0f; float charSpace = 0.0f; float whiteSpace = 0.0f; bool seenChar = false; // Iterate all glyphs backwards. On the last character of the next line, // the position of the character determines the available space to be // distributed (spaceLeft). for (int i = glyphCount - 1; i >= 0; i--) { GlyphInfo glyph = fGlyphInfos[i]; if (glyph.lineIndex != lineIndex) { bool lineBreak = glyph.charCode == '\n' || i == glyphCount - 1; lineIndex = glyph.lineIndex; // The position of the last character determines the available // space. spaceLeft = fWidth - glyph.x; // If the character is visible, the width of the character needs to // be subtracted from the available space, otherwise it would be // pushed outside the line. uint32 charClassification = get_char_classification(glyph.charCode); if (charClassification != CHAR_CLASS_WHITESPACE) spaceLeft -= glyph.width; charSpace = 0.0f; whiteSpace = 0.0f; seenChar = false; if (lineBreak || !justify) { if (alignment == ALIGN_CENTER) spaceLeft /= 2.0f; else if (alignment == ALIGN_LEFT) spaceLeft = 0.0f; } else { // Figure out how much chars and white space chars are on the // line. Don't count trailing white space. int charCount = 0; int spaceCount = 0; for (int j = i; j >= 0; j--) { const GlyphInfo& previousGlyph = fGlyphInfos[j]; if (previousGlyph.lineIndex != lineIndex) { j++; break; } uint32 classification = get_char_classification( previousGlyph.charCode); if (classification == CHAR_CLASS_WHITESPACE) { if (charCount > 0) spaceCount++; else if (j < i) spaceLeft += glyph.width; } else { charCount++; } } // The first char is not shifted when justifying, so it doesn't // contribute. if (charCount > 0) charCount--; // Check if it looks better if both whitespace and chars get // some space distributed, in case there are only 1 or two // space chars on the line. float spaceLeftForSpace = spaceLeft; float spaceLeftForChars = spaceLeft; if (spaceCount > 0) { float spaceCharRatio = (float) spaceCount / charCount; if (spaceCount < 3 && spaceCharRatio < 0.4f) { spaceLeftForSpace = spaceLeft * 2.0f * spaceCharRatio; spaceLeftForChars = spaceLeft - spaceLeftForSpace; } else spaceLeftForChars = 0.0f; } if (spaceCount > 0) whiteSpace = spaceLeftForSpace / spaceCount; if (charCount > 0) charSpace = spaceLeftForChars / charCount; LineInfo line = fLineInfos[lineIndex]; line.extraGlyphSpacing = charSpace; line.extraWhiteSpacing = whiteSpace; fLineInfos[lineIndex] = line; } } // Each character is pushed towards the right by the space that is // still available. When justification is performed, the shift is // gradually decreased. This works since the iteration is backwards // and the characters on the right are pushed farthest. glyph.x += spaceLeft; unsigned classification = get_char_classification(glyph.charCode); if (i < glyphCount - 1) { GlyphInfo nextGlyph = fGlyphInfos[i + 1]; if (nextGlyph.lineIndex == lineIndex) { uint32 nextClassification = get_char_classification(nextGlyph.charCode); if (nextClassification == CHAR_CLASS_WHITESPACE && classification != CHAR_CLASS_WHITESPACE) { // When a space character is right of a regular character, // add the additional space to the space instead of the // character float shift = (nextGlyph.x - glyph.x) - glyph.width; nextGlyph.x -= shift; fGlyphInfos[i + 1] = nextGlyph; } } } fGlyphInfos[i] = glyph; // The shift (spaceLeft) is reduced depending on the character // classification. if (classification == CHAR_CLASS_WHITESPACE) { if (seenChar) spaceLeft -= whiteSpace; } else { seenChar = true; spaceLeft -= charSpace; } } } bool ParagraphLayout::_AppendGlyphInfos(const TextSpan& span) { int charCount = span.CountChars(); 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 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++) { if (!_AppendGlyphInfo(UTF8ToCharCode(&c), escapementArray[i] * size, span.Style())) { return false; } } return true; } bool ParagraphLayout::_AppendGlyphInfo(uint32 charCode, float width, const CharacterStyle& style) { if (style.Width() >= 0.0f) { // Use the metrics provided by the CharacterStyle and override // the font provided metrics passed in "width" width = style.Width(); } width += style.GlyphSpacing(); try { fGlyphInfos.push_back(GlyphInfo(charCode, 0.0f, width, 0)); } catch (std::bad_alloc& ba) { fprintf(stderr, "bad_alloc occurred adding glyph info to a " "paragraph\n"); return false; } return true; } bool ParagraphLayout::_FinalizeLine(int lineStart, int lineEnd, int lineIndex, float y, float& lineHeight) { LineInfo line(lineStart, y, 0.0f, 0.0f, 0.0f); int spanIndex = -1; int spanStart = 0; int spanEnd = 0; for (int i = lineStart; i <= lineEnd; i++) { // Mark line index in glyph GlyphInfo glyph = fGlyphInfos[i]; glyph.lineIndex = lineIndex; fGlyphInfos[i] = glyph; // See if the next sub-span needs to be added to the LineInfo bool addSpan = false; while (i >= spanEnd) { spanIndex++; const TextSpan& span = fTextSpans[spanIndex]; spanStart = spanEnd; spanEnd += span.CountChars(); addSpan = true; } if (addSpan) { const TextSpan& span = fTextSpans[spanIndex]; TextSpan subSpan = span.SubSpan(i - spanStart, (lineEnd - spanStart + 1) - (i - spanStart)); line.layoutedSpans.push_back(subSpan); _IncludeStyleInLine(line, span.Style()); } } if (fGlyphInfos.empty() && !fTextSpans.empty()) { // When the layout contains no glyphs, but there is at least one // TextSpan in the paragraph, use the font info from that span // to calculate the height of the first LineInfo. const TextSpan& span = fTextSpans[0]; line.layoutedSpans.push_back(span); _IncludeStyleInLine(line, span.Style()); } lineHeight = line.height; try { fLineInfos.push_back(line); } catch (std::bad_alloc& ba) { fprintf(stderr, "bad_alloc occurred adding line to line infos\n"); return false; } return true; } void ParagraphLayout::_IncludeStyleInLine(LineInfo& line, const CharacterStyle& style) { float ascent = style.Ascent(); if (ascent > line.maxAscent) line.maxAscent = ascent; float descent = style.Descent(); if (descent > line.maxDescent) line.maxDescent = descent; float height = ascent + descent; if (style.Font().Size() > height) height = style.Font().Size(); if (height > line.height) line.height = height; } void ParagraphLayout::_DrawLine(BView* view, const BPoint& offset, const LineInfo& line) const { int textOffset = line.textOffset; int spanCount = static_cast(line.layoutedSpans.size()); for (int i = 0; i < spanCount; i++) { const TextSpan& span = line.layoutedSpans[i]; _DrawSpan(view, offset, span, textOffset); textOffset += span.CountChars(); } } void ParagraphLayout::_DrawSpan(BView* view, BPoint offset, const TextSpan& span, int32 textOffset) const { const BString& text = span.Text(); if (text.Length() == 0) return; const GlyphInfo& glyph = fGlyphInfos[textOffset]; const LineInfo& line = fLineInfos[glyph.lineIndex]; offset.x += glyph.x; offset.y += line.y + line.maxAscent; const CharacterStyle& style = span.Style(); view->SetFont(&style.Font()); if (style.WhichForegroundColor() != B_NO_COLOR) view->SetHighUIColor(style.WhichForegroundColor()); else view->SetHighColor(style.ForegroundColor()); // TODO: Implement other style properties escapement_delta delta; delta.nonspace = line.extraGlyphSpacing; delta.space = line.extraWhiteSpacing; view->DrawString(span.Text(), offset, &delta); } void ParagraphLayout::_GetEmptyLayoutBounds(float& x1, float& y1, float& x2, float& y2) const { if (fLineInfos.empty()) { x1 = 0.0f; y1 = 0.0f; x2 = 0.0f; y2 = 0.0f; return; } // If the paragraph had at least a single empty TextSpan, the layout // can compute some meaningful bounds. const Bullet& bullet = fParagraphStyle.Bullet(); x1 = fParagraphStyle.LineInset() + fParagraphStyle.FirstLineInset() + bullet.Spacing(); x2 = x1; const LineInfo& lineInfo = fLineInfos[0]; y1 = lineInfo.y; y2 = lineInfo.y + lineInfo.height; } void ParagraphLayout::_AppendTextSpans(const Paragraph& paragraph) { int32 countTextSpans = paragraph.CountTextSpans(); for (int32 i = 0; i< countTextSpans; i++) fTextSpans.push_back(paragraph.TextSpanAtIndex(i)); }