Implement initial version of text justification, new tests, and fix bugs

with right and center alignment.

Change-Id: I3b6a547e1d81c966b82108c02d60aa7181cb0b87
This commit is contained in:
Gary Qian 2017-06-23 12:54:46 -07:00
parent c553c87792
commit 19bb52f360
7 changed files with 264 additions and 43 deletions

View File

@ -56,14 +56,14 @@ class PaintRecord {
const TextStyle& style() const { return style_; }
double line() const { return line_; }
size_t line() const { return line_; }
private:
TextStyle style_;
SkPoint offset_;
sk_sp<SkTextBlob> text_;
SkPaint::FontMetrics metrics_;
int line_;
size_t line_;
FTL_DISALLOW_COPY_AND_ASSIGN(PaintRecord);
};

View File

@ -24,6 +24,7 @@
#include <minikin/Layout.h>
#include "lib/ftl/logging.h"
#include "lib/txt/libs/minikin/LayoutUtils.h"
#include "lib/txt/src/font_collection.h"
#include "lib/txt/src/font_skia.h"
#include "minikin/LineBreaker.h"
@ -96,7 +97,7 @@ void GetFontAndMinikinPaint(const TextStyle& style,
*font = minikin::FontStyle(GetWeight(style), GetItalic(style));
paint->size = style.font_size;
paint->letterSpacing = style.letter_spacing;
paint->wordSpacing = style.word_spacing; // Likely not working yet.
paint->wordSpacing = style.word_spacing;
// TODO(abarth): word_spacing.
}
@ -148,8 +149,9 @@ void Paragraph::Layout(double width,
width_ = width;
breaker_.setLineWidths(0.0f, 0, width);
breaker_.setLineWidths(0.0f, 0, width_);
AddRunsToLineBreaker(rootdir);
breaker_.setJustified(paragraph_style_.text_align == TextAlign::justify);
size_t breaks_count = breaker_.computeBreaks();
const int* breaks = breaker_.getBreaks();
@ -171,12 +173,12 @@ void Paragraph::Layout(double width,
SkScalar y = y_offset;
size_t break_index = 0;
double letter_spacing_offset = 0.0f;
double word_spacing_offset = 0.0f;
double max_line_spacing = 0.0f;
double max_descent = 0.0f;
double prev_max_descent = 0.0f;
double line_width = 0.0f;
std::vector<SkScalar> x_queue;
size_t character_index = 0;
auto flush = [this, &x_queue, &y]() -> void {
for (size_t i = 0; i < x_queue.size(); ++i) {
@ -209,28 +211,44 @@ void Paragraph::Layout(double width,
int bidiFlags = 0;
layout.doLayout(text_.data(), layout_start, layout_end - layout_start,
text_.size(), bidiFlags, font, minikin_paint, collection);
const size_t glyph_count = layout.nGlyphs();
size_t blob_start = 0;
// Each word/blob.
// Each blob.
std::vector<const SkTextBlobBuilder::RunBuffer*> buffers;
std::vector<size_t> buffer_sizes;
int word_count = 0;
while (blob_start < glyph_count) {
const size_t blob_length = GetBlobLength(layout, blob_start);
buffer_sizes.push_back(blob_length);
// TODO(abarth): Precompute when we can use allocRunPosH.
paint.setTypeface(GetTypefaceForGlyph(layout, blob_start));
auto buffer = builder.allocRunPos(paint, blob_length);
buffers.push_back(&builder.allocRunPos(paint, blob_length));
letter_spacing_offset += run.style.letter_spacing;
// Each Glyph/Letter.
bool whitespace_ended = true;
for (size_t blob_index = 0; blob_index < blob_length; ++blob_index) {
const size_t glyph_index = blob_start + blob_index;
buffer.glyphs[blob_index] = layout.getGlyphId(glyph_index);
buffers.back()->glyphs[blob_index] = layout.getGlyphId(glyph_index);
// Check if the current Glyph is a whitespace and handle multiple
// whitespaces in a row.
if (minikin::isWordSpace(text_[character_index])) {
// Only increment word_count if it is the first in a series of
// whitespaces.
if (whitespace_ended)
++word_count;
whitespace_ended = false;
} else {
whitespace_ended = true;
}
++character_index;
const size_t pos_index = 2 * blob_index;
buffer.pos[pos_index] = layout.getX(glyph_index) +
letter_spacing_offset + word_spacing_offset;
buffer.pos[pos_index + 1] = layout.getY(glyph_index);
buffers.back()->pos[pos_index] =
layout.getX(glyph_index) + letter_spacing_offset;
buffers.back()->pos[pos_index + 1] = layout.getY(glyph_index);
letter_spacing_offset += run.style.letter_spacing;
}
@ -240,22 +258,21 @@ void Paragraph::Layout(double width,
// removed depending on the specifications for letter spacing.
// letter_spacing_offset -= run.style.letter_spacing;
word_spacing_offset += run.style.word_spacing;
max_intrinsic_width_ +=
layout.getX(blob_start - 1) + letter_spacing_offset;
}
// Subtract word offset to avoid big gap at end of run. This my be
// removed depending on the specificatins for word spacing.
word_spacing_offset -= run.style.word_spacing;
// TODO(abarth): We could keep the same SkTextBlobBuilder as long as the
// color stayed the same.
// TODO(garyq): Ensure that the typeface does not change throughout a
// run.
SkPaint::FontMetrics metrics;
paint.getFontMetrics(&metrics);
// Apply additional word spacing if the text is justified.
if (paragraph_style_.text_align == TextAlign::justify &&
buffer_sizes.size() > 0) {
JustifyLine(buffers, buffer_sizes, word_count, character_index);
}
records_.push_back(
PaintRecord{run.style, builder.make(), metrics, lines_});
line_width +=
@ -293,7 +310,7 @@ void Paragraph::Layout(double width,
max_descent = 0.0f;
x = 0.0f;
letter_spacing_offset = 0.0f;
word_spacing_offset = 0.0f;
word_count = 0;
line_width = 0.0f;
// TODO(abarth): Use the line height, which is something like the max
// font_size for runs in this line times the paragraph's line height.
@ -314,6 +331,45 @@ void Paragraph::Layout(double width,
height_ = y + max_descent;
}
// Amends the buffers to incorporate justification.
void Paragraph::JustifyLine(
std::vector<const SkTextBlobBuilder::RunBuffer*>& buffers,
std::vector<size_t>& buffer_sizes,
int word_count,
size_t character_index) {
// TODO(garyq): Add letter_spacing_offset back in. It is Temporarily
// removed.
double justify_spacing =
(width_ - breaker_.getWidths()[lines_]) / (word_count - 1);
word_count = 0;
// Set up index to properly access text_ because minikin::isWordSpace()
// takes uint_16 instead of GlyphIDs.
size_t line_character_index = character_index;
for (size_t i = 0; i < buffers.size(); ++i)
line_character_index -= buffer_sizes[i];
bool whitespace_ended = true;
for (size_t i = 0; i < buffers.size(); ++i) {
for (size_t glyph_index = 0; glyph_index < buffer_sizes[i]; ++glyph_index) {
// Check if the current Glyph is a whitespace and handle multiple
// whitespaces in a row.
if (minikin::isWordSpace(text_[line_character_index])) {
// Only increment word_count and add justification spacing to
// whitespace if it is the first in a series of whitespaces.
if (whitespace_ended) {
++word_count;
buffers[i]->pos[glyph_index * 2] += justify_spacing * word_count;
}
whitespace_ended = false;
} else {
// Add justification spacing for all non-whitespace glyphs.
buffers[i]->pos[glyph_index * 2] += justify_spacing * word_count;
whitespace_ended = true;
}
++line_character_index;
}
}
}
const ParagraphStyle& Paragraph::GetParagraphStyle() const {
return paragraph_style_;
}
@ -346,8 +402,8 @@ void Paragraph::SetParagraphStyle(const ParagraphStyle& style) {
}
void Paragraph::Paint(SkCanvas* canvas, double x, double y) {
SkPaint paint;
for (const auto& record : records_) {
SkPaint paint;
paint.setColor(record.style().color);
SkPoint offset = record.offset();
// TODO(garyq): Fix alignment for paragraphs with multiple styles per line.
@ -355,15 +411,15 @@ void Paragraph::Paint(SkCanvas* canvas, double x, double y) {
case TextAlign::left:
break;
case TextAlign::right: {
offset.offset(width_ - line_widths_[record.line()], 0);
offset.offset(width_ - breaker_.getWidths()[record.line()], 0);
break;
}
case TextAlign::center: {
offset.offset((width_ - line_widths_[record.line()]) / 2, 0);
offset.offset((width_ - breaker_.getWidths()[record.line()]) / 2, 0);
break;
}
case TextAlign::justify: {
// TODO(garyq): implement justify.
// Justify is performed in the Layout().
break;
}
}
@ -371,12 +427,6 @@ void Paragraph::Paint(SkCanvas* canvas, double x, double y) {
PaintDecorations(canvas, x + offset.x(), y + offset.y(), record.style(),
record.metrics(), record.text());
}
paint.setStyle(SkPaint::kFill_Style);
paint.setAntiAlias(true);
paint.setStrokeWidth(4);
paint.setColor(0xffFE938C);
canvas->drawCircle(x, y, 3, paint);
}
void Paragraph::PaintDecorations(SkCanvas* canvas,

View File

@ -68,7 +68,10 @@ class Paragraph {
FRIEND_TEST(RenderTest, RainbowParagraph);
FRIEND_TEST(RenderTest, DefaultStyleParagraph);
FRIEND_TEST(RenderTest, BoldParagraph);
FRIEND_TEST(RenderTest, LinebreakParagraph);
FRIEND_TEST(RenderTest, LeftAlignParagraph);
FRIEND_TEST(RenderTest, RightAlignParagraph);
FRIEND_TEST(RenderTest, CenterAlignParagraph);
FRIEND_TEST(RenderTest, JustifyAlignParagraph);
FRIEND_TEST(RenderTest, ItalicsParagraph);
std::vector<uint16_t> text_;
@ -80,7 +83,7 @@ class Paragraph {
// TODO(garyq): Height of the paragraph after Layout().
SkScalar height_ = 0.0f;
double width_ = 0.0f;
int lines_ = 1;
size_t lines_ = 0;
double max_intrinsic_width_ = 0.0f;
double min_intrinsic_width_ = 0.0f;
double alphabetic_baseline_ = FLT_MAX;
@ -93,6 +96,11 @@ class Paragraph {
void AddRunsToLineBreaker(const std::string& rootdir = "");
void JustifyLine(std::vector<const SkTextBlobBuilder::RunBuffer*>& buffers,
std::vector<size_t>& buffer_sizes,
int word_count,
size_t character_index);
void PaintDecorations(SkCanvas* canvas,
double x,
double y,

View File

@ -32,7 +32,7 @@ class ParagraphStyle {
FontStyle font_style = FontStyle::normal;
std::string font_family = "";
double font_size = 14;
int max_lines = 1;
size_t max_lines = 1;
double line_height = 1.0;
std::string ellipsis;
};

View File

@ -60,7 +60,10 @@ class StyledRuns {
FRIEND_TEST(RenderTest, RainbowParagraph);
FRIEND_TEST(RenderTest, DefaultStyleParagraph);
FRIEND_TEST(RenderTest, BoldParagraph);
FRIEND_TEST(RenderTest, LinebreakParagraph);
FRIEND_TEST(RenderTest, LeftAlignParagraph);
FRIEND_TEST(RenderTest, RightAlignParagraph);
FRIEND_TEST(RenderTest, CenterAlignParagraph);
FRIEND_TEST(RenderTest, JustifyAlignParagraph);
FRIEND_TEST(RenderTest, ItalicsParagraph);
struct IndexedRun {

View File

@ -252,28 +252,132 @@ TEST_F(RenderTest, BoldParagraph) {
ASSERT_TRUE(Snapshot());
}
TEST_F(RenderTest, LinebreakParagraph) {
TEST_F(RenderTest, LeftAlignParagraph) {
const char* text =
"This is a very long sentence to test if the text will properly wrap "
"around and go to the next line. Sometimes, short sentence. Longer "
"sentences are okay too because they are nessecary. Very short."
"sentences are okay too because they are nessecary. Very short. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum.";
auto icu_text = icu::UnicodeString::fromUTF8(text);
std::u16string u16_text(icu_text.getBuffer(),
icu_text.getBuffer() + icu_text.length());
txt::ParagraphStyle paragraph_style;
paragraph_style.max_lines = 14;
paragraph_style.text_align = TextAlign::left;
txt::ParagraphBuilder builder(paragraph_style);
txt::TextStyle text_style;
text_style.font_size = 26;
text_style.letter_spacing = 1;
text_style.word_spacing = 5;
text_style.color = SK_ColorBLACK;
text_style.height = 1.15;
text_style.decoration = txt::TextDecoration(0x1);
text_style.decoration_color = SK_ColorBLACK;
builder.PushStyle(text_style);
builder.AddText(u16_text);
builder.Pop();
auto paragraph = builder.Build();
paragraph->Layout(GetTestCanvasWidth() - 100, txt::GetFontDir());
paragraph->Paint(GetCanvas(), 0, 0);
ASSERT_EQ(paragraph->text_.size(), std::string{text}.length());
for (size_t i = 0; i < u16_text.length(); i++) {
ASSERT_EQ(paragraph->text_[i], u16_text[i]);
}
ASSERT_EQ(paragraph->runs_.runs_.size(), 1ull);
ASSERT_EQ(paragraph->runs_.styles_.size(), 1ull);
ASSERT_TRUE(paragraph->runs_.styles_[0].equals(text_style));
ASSERT_EQ(paragraph->records_[0].style().color, text_style.color);
ASSERT_TRUE(Snapshot());
}
TEST_F(RenderTest, RightAlignParagraph) {
const char* text =
"This is a very long sentence to test if the text will properly wrap "
"around and go to the next line. Sometimes, short sentence. Longer "
"sentences are okay too because they are nessecary. Very short."
"sentences are okay too because they are nessecary. Very short. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum."
"mollit anim id est laborum. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum."
"mollit anim id est laborum.";
auto icu_text = icu::UnicodeString::fromUTF8(text);
std::u16string u16_text(icu_text.getBuffer(),
icu_text.getBuffer() + icu_text.length());
txt::ParagraphStyle paragraph_style;
paragraph_style.max_lines = 14;
paragraph_style.text_align = TextAlign::right;
txt::ParagraphBuilder builder(paragraph_style);
txt::TextStyle text_style;
text_style.font_size = 26;
text_style.letter_spacing = 1;
text_style.word_spacing = 5;
text_style.color = SK_ColorBLACK;
text_style.height = 1.15;
text_style.decoration = txt::TextDecoration(0x1);
text_style.decoration_color = SK_ColorBLACK;
builder.PushStyle(text_style);
builder.AddText(u16_text);
builder.Pop();
auto paragraph = builder.Build();
paragraph->Layout(GetTestCanvasWidth() - 100, txt::GetFontDir());
paragraph->Paint(GetCanvas(), 0, 0);
ASSERT_EQ(paragraph->text_.size(), std::string{text}.length());
for (size_t i = 0; i < u16_text.length(); i++) {
ASSERT_EQ(paragraph->text_[i], u16_text[i]);
}
ASSERT_EQ(paragraph->runs_.runs_.size(), 1ull);
ASSERT_EQ(paragraph->runs_.styles_.size(), 1ull);
ASSERT_TRUE(paragraph->runs_.styles_[0].equals(text_style));
ASSERT_EQ(paragraph->records_[0].style().color, text_style.color);
ASSERT_TRUE(Snapshot());
}
TEST_F(RenderTest, CenterAlignParagraph) {
const char* text =
"This is a very long sentence to test if the text will properly wrap "
"around and go to the next line. Sometimes, short sentence. Longer "
"sentences are okay too because they are nessecary. Very short. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
@ -292,8 +396,8 @@ TEST_F(RenderTest, LinebreakParagraph) {
txt::TextStyle text_style;
text_style.font_size = 26;
// Letter spacing not yet implemented
text_style.letter_spacing = 0;
text_style.letter_spacing = 1;
text_style.word_spacing = 5;
text_style.color = SK_ColorBLACK;
text_style.height = 1.15;
text_style.decoration = txt::TextDecoration(0x1);
@ -307,8 +411,64 @@ TEST_F(RenderTest, LinebreakParagraph) {
auto paragraph = builder.Build();
paragraph->Layout(GetTestCanvasWidth() - 100, txt::GetFontDir());
paragraph->Paint(GetCanvas(), 0, 30.0);
paragraph->Paint(GetCanvas(), 0, 0);
ASSERT_EQ(paragraph->text_.size(), std::string{text}.length());
for (size_t i = 0; i < u16_text.length(); i++) {
ASSERT_EQ(paragraph->text_[i], u16_text[i]);
}
ASSERT_EQ(paragraph->runs_.runs_.size(), 1ull);
ASSERT_EQ(paragraph->runs_.styles_.size(), 1ull);
ASSERT_TRUE(paragraph->runs_.styles_[0].equals(text_style));
ASSERT_EQ(paragraph->records_[0].style().color, text_style.color);
ASSERT_TRUE(Snapshot());
}
TEST_F(RenderTest, JustifyAlignParagraph) {
const char* text =
"This is a very long sentence to test if the text will properly wrap "
"around and go to the next line. Sometimes, short sentence. Longer "
"sentences are okay too because they are nessecary. Very short. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum. "
"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod "
"tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim "
"veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea "
"commodo consequat. Duis aute irure dolor in reprehenderit in voluptate "
"velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint "
"occaecat cupidatat non proident, sunt in culpa qui officia deserunt "
"mollit anim id est laborum.";
auto icu_text = icu::UnicodeString::fromUTF8(text);
std::u16string u16_text(icu_text.getBuffer(),
icu_text.getBuffer() + icu_text.length());
txt::ParagraphStyle paragraph_style;
paragraph_style.max_lines = 14;
paragraph_style.text_align = TextAlign::justify;
txt::ParagraphBuilder builder(paragraph_style);
txt::TextStyle text_style;
text_style.font_size = 26;
text_style.letter_spacing = 1;
text_style.word_spacing = 5;
text_style.color = SK_ColorBLACK;
text_style.height = 1.15;
text_style.decoration = txt::TextDecoration(0x1);
text_style.decoration_color = SK_ColorBLACK;
builder.PushStyle(text_style);
builder.AddText(u16_text);
builder.Pop();
auto paragraph = builder.Build();
paragraph->Layout(GetTestCanvasWidth() - 100, txt::GetFontDir());
paragraph->Paint(GetCanvas(), 0, 0);
ASSERT_EQ(paragraph->text_.size(), std::string{text}.length());
for (size_t i = 0; i < u16_text.length(); i++) {
ASSERT_EQ(paragraph->text_[i], u16_text[i]);
@ -321,7 +481,7 @@ TEST_F(RenderTest, LinebreakParagraph) {
}
TEST_F(RenderTest, ItalicsParagraph) {
const char* text = "I am Italicized!";
const char* text = "I am Italicized! ";
auto icu_text = icu::UnicodeString::fromUTF8(text);
std::u16string u16_text(icu_text.getBuffer(),
icu_text.getBuffer() + icu_text.length());

View File

@ -47,7 +47,7 @@ bool RenderTest::Snapshot() {
}
size_t RenderTest::GetTestCanvasWidth() const {
return 800;
return 1000;
}
size_t RenderTest::GetTestCanvasHeight() const {