libtxt: handle lines containing both LTR and RTL text runs (#4416)

* Call the ICU bidi API to divide the text into LTR/RTL runs and merge
  them with the styled runs defined by the application
* Maintain a list of glyph positions sorted by the order of the corresponding
  code units in the text, as well as a list of glyphs sorted by x/y coordinates
  in the layout
This commit is contained in:
Jason Simmons 2017-12-06 14:46:03 -08:00 committed by GitHub
parent 7e4df308a7
commit affa4e7c46
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 399 additions and 246 deletions

View File

@ -64,11 +64,11 @@ void ParagraphImplTxt::paint(Canvas* canvas, double x, double y) {
std::vector<TextBox> ParagraphImplTxt::getRectsForRange(unsigned start,
unsigned end) {
std::vector<TextBox> result;
std::vector<SkRect> rects = m_paragraph->GetRectsForRange(start, end);
for (size_t i = 0; i < rects.size(); ++i) {
result.push_back(TextBox(
rects[i], static_cast<TextDirection>(
m_paragraph->GetParagraphStyle().text_direction)));
std::vector<txt::Paragraph::TextBox> boxes =
m_paragraph->GetRectsForRange(start, end);
for (const txt::Paragraph::TextBox& box : boxes) {
result.emplace_back(box.rect,
static_cast<blink::TextDirection>(box.direction));
}
return result;
}
@ -76,14 +76,14 @@ std::vector<TextBox> ParagraphImplTxt::getRectsForRange(unsigned start,
Dart_Handle ParagraphImplTxt::getPositionForOffset(double dx, double dy) {
Dart_Handle result = Dart_NewList(2);
txt::Paragraph::PositionWithAffinity pos =
m_paragraph->GetGlyphPositionAtCoordinate(dx, dy, true);
m_paragraph->GetGlyphPositionAtCoordinate(dx, dy);
Dart_ListSetAt(result, 0, ToDart(pos.position));
Dart_ListSetAt(result, 1, ToDart(static_cast<int>(pos.affinity)));
return result;
}
Dart_Handle ParagraphImplTxt::getWordBoundary(unsigned offset) {
txt::Paragraph::Range point = m_paragraph->GetWordBoundary(offset);
txt::Paragraph::Range<size_t> point = m_paragraph->GetWordBoundary(offset);
Dart_Handle result = Dart_NewList(2);
Dart_ListSetAt(result, 0, ToDart(point.start));
Dart_ListSetAt(result, 1, ToDart(point.end));

View File

@ -19,6 +19,7 @@
#include <hb.h>
#include <algorithm>
#include <limits>
#include <map>
#include <numeric>
#include <utility>
#include <vector>
@ -31,6 +32,7 @@
#include "minikin/LayoutUtils.h"
#include "minikin/LineBreaker.h"
#include "minikin/MinikinFont.h"
#include "third_party/icu/source/common/unicode/ubidi.h"
#include "third_party/skia/include/core/SkCanvas.h"
#include "third_party/skia/include/core/SkPaint.h"
#include "third_party/skia/include/core/SkTextBlob.h"
@ -120,7 +122,7 @@ void GetPaint(const TextStyle& style, SkPaint* paint) {
void FindWords(const std::vector<uint16_t>& text,
size_t start,
size_t end,
std::vector<Paragraph::Range>* words) {
std::vector<Paragraph::Range<size_t>>* words) {
bool in_word = false;
size_t word_start;
for (size_t i = start; i < end; ++i) {
@ -141,24 +143,26 @@ void FindWords(const std::vector<uint16_t>& text,
static const float kDoubleDecorationSpacing = 3.0f;
Paragraph::GlyphPosition::GlyphPosition(double x_start,
double x_advance,
size_t code_unit_index,
size_t code_unit_width)
: code_units(code_unit_index, code_unit_index + code_unit_width),
x_pos(x_start, x_start + x_advance) {}
Paragraph::GlyphLine::GlyphLine(std::vector<GlyphPosition>&& p, size_t tcu)
: positions(std::move(p)), total_code_units(tcu) {}
const Paragraph::GlyphPosition& Paragraph::GlyphLine::GetGlyphPosition(
size_t pos) const {
FXL_DCHECK(pos < total_code_units);
if (positions.size() == total_code_units)
return positions[pos];
size_t unit_count = 0;
for (const Paragraph::GlyphPosition& gp : positions) {
if (pos < unit_count + gp.code_units)
return gp;
unit_count += gp.code_units;
}
return positions.back();
}
Paragraph::CodeUnitRun::CodeUnitRun(std::vector<GlyphPosition>&& p,
Range<size_t> cu,
Range<double> x,
size_t line,
TextDirection dir)
: positions(std::move(p)),
code_units(cu),
x_pos(x),
line_number(line),
direction(dir) {}
Paragraph::Paragraph() {
breaker_.setLocale(icu::Locale(), nullptr);
@ -251,6 +255,84 @@ bool Paragraph::ComputeLineBreaks() {
return true;
}
bool Paragraph::ComputeBidiRuns() {
bidi_runs_.clear();
auto ubidi_closer = [](UBiDi* b) { ubidi_close(b); };
std::unique_ptr<UBiDi, decltype(ubidi_closer)> bidi(ubidi_open(),
ubidi_closer);
if (!bidi)
return false;
UBiDiLevel paraLevel = (paragraph_style_.text_direction == TextDirection::rtl)
? UBIDI_DEFAULT_RTL
: UBIDI_DEFAULT_LTR;
UErrorCode status = U_ZERO_ERROR;
ubidi_setPara(bidi.get(), text_.data(), text_.size(), paraLevel, nullptr,
&status);
if (!U_SUCCESS(status))
return false;
int32_t bidi_run_count = ubidi_countRuns(bidi.get(), &status);
if (!U_SUCCESS(status))
return false;
// Build a map of styled runs indexed by start position.
std::map<size_t, StyledRuns::Run> styled_run_map;
for (size_t i = 0; i < runs_.size(); ++i) {
StyledRuns::Run run = runs_.GetRun(i);
styled_run_map.emplace(std::make_pair(run.start, run));
}
for (int32_t bidi_run_index = 0; bidi_run_index < bidi_run_count;
++bidi_run_index) {
int32_t bidi_run_start, bidi_run_length;
UBiDiDirection direction = ubidi_getVisualRun(
bidi.get(), bidi_run_index, &bidi_run_start, &bidi_run_length);
if (!U_SUCCESS(status))
return false;
// Exclude the leading bidi control character if present.
UChar32 first_char;
U16_GET(text_.data(), 0, bidi_run_start, static_cast<int>(text_.size()),
first_char);
if (u_hasBinaryProperty(first_char, UCHAR_BIDI_CONTROL)) {
bidi_run_start++;
bidi_run_length--;
}
if (bidi_run_length == 0)
continue;
// Exclude the trailing bidi control character if present.
UChar32 last_char;
U16_GET(text_.data(), 0, bidi_run_start + bidi_run_length - 1,
static_cast<int>(text_.size()), last_char);
if (u_hasBinaryProperty(last_char, UCHAR_BIDI_CONTROL)) {
bidi_run_length--;
}
if (bidi_run_length == 0)
continue;
size_t bidi_run_end = bidi_run_start + bidi_run_length;
TextDirection text_direction =
direction == UBIDI_RTL ? TextDirection::rtl : TextDirection::ltr;
// Break this bidi run into chunks based on text style.
size_t chunk_start = bidi_run_start;
while (chunk_start < bidi_run_end) {
auto styled_run_iter = styled_run_map.upper_bound(chunk_start);
styled_run_iter--;
const StyledRuns::Run& styled_run = styled_run_iter->second;
size_t chunk_end = std::min(bidi_run_end, styled_run.end);
bidi_runs_.emplace_back(chunk_start, chunk_end, text_direction,
styled_run.style);
chunk_start = chunk_end;
}
}
return true;
}
void Paragraph::Layout(double width, bool force) {
// Do not allow calling layout multiple times without changing anything.
if (!needs_layout_ && width == width_ && !force) {
@ -263,6 +345,7 @@ void Paragraph::Layout(double width, bool force) {
if (!ComputeLineBreaks()) {
return;
}
ComputeBidiRuns();
SkPaint paint;
paint.setAntiAlias(true);
@ -273,10 +356,10 @@ void Paragraph::Layout(double width, bool force) {
records_.clear();
line_heights_.clear();
glyph_lines_.clear();
code_unit_runs_.clear();
minikin::Layout layout;
SkTextBlobBuilder builder;
size_t run_index = 0;
double y_offset = 0;
double prev_max_descent = 0;
double max_word_width = 0;
@ -288,7 +371,7 @@ void Paragraph::Layout(double width, bool force) {
const LineRange& line_range = line_ranges_[line_number];
// Break the line into words if justification should be applied.
std::vector<Range> words;
std::vector<Range<size_t>> words;
double word_gap_width = 0;
size_t word_index = 0;
bool justify_line = (paragraph_style_.text_align == TextAlign::justify &&
@ -303,23 +386,21 @@ void Paragraph::Layout(double width, bool force) {
}
// Find the runs comprising this line.
std::vector<StyledRuns::Run> line_runs;
while (run_index < runs_.size()) {
StyledRuns::Run run = runs_.GetRun(run_index);
if (run.start >= line_range.end)
break;
line_runs.push_back(run);
if (run.end > line_range.end)
break;
run_index++;
std::vector<BidiRun> line_runs;
for (const BidiRun& bidi_run : bidi_runs_) {
if (bidi_run.start < line_range.end && bidi_run.end > line_range.start) {
line_runs.emplace_back(std::max(bidi_run.start, line_range.start),
std::min(bidi_run.end, line_range.end),
bidi_run.direction, bidi_run.style);
}
}
std::vector<GlyphPosition> glyph_positions;
std::vector<GlyphPosition> line_glyph_positions;
double run_x_offset = GetLineXOffset(line_number);
double justify_x_offset = 0;
std::vector<PaintRecord> paint_records;
for (const StyledRuns::Run& run : line_runs) {
for (const BidiRun& run : line_runs) {
minikin::FontStyle font;
minikin::MinikinPaint minikin_paint;
GetFontAndMinikinPaint(run.style, &font, &minikin_paint);
@ -330,10 +411,9 @@ void Paragraph::Layout(double width, bool force) {
run.style.font_family);
// Lay out this run.
size_t line_run_start = std::max(run.start, line_range.start);
size_t line_run_end = std::min(run.end, line_range.end);
uint16_t* text_ptr = text_.data() + line_run_start;
size_t text_count = line_run_end - line_run_start;
uint16_t* text_ptr = text_.data();
size_t text_start = run.start;
size_t text_count = run.end - run.start;
int bidiFlags = (paragraph_style_.text_direction == TextDirection::rtl)
? minikin::kBidi_RTL
: minikin::kBidi_LTR;
@ -352,8 +432,8 @@ void Paragraph::Layout(double width, bool force) {
std::vector<float> text_advances(text_count);
float text_width = layout.measureText(
text_ptr, 0, text_count, text_count, bidiFlags, font, minikin_paint,
minikin_font_collection, text_advances.data());
text_ptr, text_start, text_count, text_.size(), bidiFlags, font,
minikin_paint, minikin_font_collection, text_advances.data());
// Truncate characters from the text until the ellipsis fits.
size_t truncate_count = 0;
@ -366,11 +446,12 @@ void Paragraph::Layout(double width, bool force) {
ellipsized_text.reserve(text_count - truncate_count +
ellipsis.length());
ellipsized_text.insert(ellipsized_text.begin(),
text_.begin() + line_run_start,
text_.begin() + line_run_end - truncate_count);
text_.begin() + run.start,
text_.begin() + run.end - truncate_count);
ellipsized_text.insert(ellipsized_text.end(), ellipsis.begin(),
ellipsis.end());
text_ptr = ellipsized_text.data();
text_start = 0;
text_count = ellipsized_text.size();
// If there is no line limit, then skip all lines after the ellipsized
@ -381,8 +462,8 @@ void Paragraph::Layout(double width, bool force) {
}
}
layout.doLayout(text_ptr, 0, text_count, text_count, bidiFlags, font,
minikin_paint, minikin_font_collection);
layout.doLayout(text_ptr, text_start, text_count, text_.size(), bidiFlags,
font, minikin_paint, minikin_font_collection);
if (layout.nGlyphs() == 0) {
// This run is empty, so insert a placeholder paint record that captures
@ -396,17 +477,25 @@ void Paragraph::Layout(double width, bool force) {
}
// Break the layout into blobs that share the same SkPaint parameters.
std::vector<Range> glyph_blobs;
std::vector<Range<size_t>> glyph_blobs;
for (size_t blob_start = 0; blob_start < layout.nGlyphs();) {
size_t blob_len = GetBlobLength(layout, blob_start);
glyph_blobs.emplace_back(blob_start, blob_start + blob_len);
blob_start += blob_len;
}
size_t code_unit_index = 0;
std::vector<GlyphPosition> glyph_positions;
size_t code_unit_index;
if (run.is_rtl()) {
code_unit_index = text_count;
U16_BACK_1(text_ptr + text_start, 0, code_unit_index);
} else {
code_unit_index = 0;
}
double word_start_position = std::numeric_limits<double>::quiet_NaN();
for (const Range& glyph_blob : glyph_blobs) {
// Build a Skia text blob from each group of glyphs.
for (const Range<size_t>& glyph_blob : glyph_blobs) {
paint.setTypeface(GetTypefaceForGlyph(layout, glyph_blob.start));
const SkTextBlobBuilder::RunBuffer& blob_buffer =
builder.allocRunPos(paint, glyph_blob.end - glyph_blob.start);
@ -422,7 +511,7 @@ void Paragraph::Layout(double width, bool force) {
blob_buffer.pos[pos_index + 1] = layout.getY(glyph_index);
if (word_index < words.size() &&
words[word_index].start == line_run_start + code_unit_index) {
words[word_index].start == run.start + code_unit_index) {
word_start_position = run_x_offset + glyph_x_offset;
}
@ -431,44 +520,57 @@ void Paragraph::Layout(double width, bool force) {
// The glyph may be a ligature. Determine how many input characters
// are joined into this glyph. Note that each character may be
// encoded as multiple UTF-16 code units.
size_t prev_code_unit_index = code_unit_index;
U16_FWD_1(text_ptr, code_unit_index, text_count);
std::vector<size_t> subglyph_code_unit_counts{code_unit_index -
prev_code_unit_index};
while (code_unit_index < text_count) {
if (layout.getCharAdvance(code_unit_index) != 0)
break;
prev_code_unit_index = code_unit_index;
U16_FWD_1(text_ptr, code_unit_index, text_count);
std::vector<size_t> subglyph_code_unit_counts;
size_t next_code_unit_index = code_unit_index;
if (run.is_rtl()) {
U16_BACK_1(text_ptr + text_start, 0, next_code_unit_index);
subglyph_code_unit_counts.push_back(code_unit_index -
prev_code_unit_index);
next_code_unit_index);
} else {
U16_FWD_1(text_ptr + text_start, next_code_unit_index, text_count);
subglyph_code_unit_counts.push_back(next_code_unit_index -
code_unit_index);
while (next_code_unit_index < text_count) {
if (layout.getCharAdvance(next_code_unit_index) != 0)
break;
size_t cur_code_unit_index = next_code_unit_index;
U16_FWD_1(text_ptr + text_start, next_code_unit_index,
text_count);
subglyph_code_unit_counts.push_back(next_code_unit_index -
cur_code_unit_index);
}
}
float subglyph_advance =
glyph_advance / subglyph_code_unit_counts.size();
glyph_positions.emplace_back(run_x_offset + glyph_x_offset,
subglyph_advance,
subglyph_code_unit_counts[0]);
glyph_positions.emplace_back(
run_x_offset + glyph_x_offset, subglyph_advance,
run.start + code_unit_index, subglyph_code_unit_counts[0]);
// Compute positions for the additional characters in the ligature.
for (size_t i = 1; i < subglyph_code_unit_counts.size(); ++i) {
glyph_positions.emplace_back(
glyph_positions.back().start + subglyph_advance,
subglyph_advance, subglyph_code_unit_counts[i]);
glyph_positions.back().x_pos.end, subglyph_advance,
glyph_positions.back().code_units.start +
subglyph_code_unit_counts[i - 1],
subglyph_code_unit_counts[i]);
}
if (word_index < words.size() &&
words[word_index].end == line_run_start + code_unit_index) {
words[word_index].end == run.start + next_code_unit_index) {
if (justify_line)
justify_x_offset += word_gap_width;
word_index++;
if (!isnan(word_start_position)) {
double word_width =
glyph_positions.back().glyph_end() - word_start_position;
glyph_positions.back().x_pos.end - word_start_position;
max_word_width = std::max(word_width, max_word_width);
word_start_position = std::numeric_limits<double>::quiet_NaN();
}
}
code_unit_index = next_code_unit_index;
}
SkPaint::FontMetrics metrics;
@ -478,6 +580,22 @@ void Paragraph::Layout(double width, bool force) {
layout.getAdvance());
}
line_glyph_positions.insert(line_glyph_positions.end(),
glyph_positions.begin(),
glyph_positions.end());
// Add a record of glyph positions sorted by code unit index.
std::vector<GlyphPosition> code_unit_positions(glyph_positions);
std::sort(code_unit_positions.begin(), code_unit_positions.end(),
[](const GlyphPosition& a, const GlyphPosition& b) {
return a.code_units.start < b.code_units.start;
});
code_unit_runs_.emplace_back(
std::move(code_unit_positions), Range<size_t>(run.start, run.end),
Range<double>(glyph_positions.front().x_pos.start,
glyph_positions.back().x_pos.end),
line_number, run.direction);
run_x_offset += layout.getAdvance();
}
@ -519,7 +637,7 @@ void Paragraph::Layout(double width, bool force) {
size_t next_line_start = (line_number < line_ranges_.size() - 1)
? line_ranges_[line_number + 1].start
: text_.size();
glyph_lines_.emplace_back(std::move(glyph_positions),
glyph_lines_.emplace_back(std::move(line_glyph_positions),
next_line_start - line_range.start);
}
@ -528,6 +646,11 @@ void Paragraph::Layout(double width, bool force) {
max_intrinsic_width_ += line_width;
}
min_intrinsic_width_ = std::min(max_word_width, max_intrinsic_width_);
std::sort(code_unit_runs_.begin(), code_unit_runs_.end(),
[](const CodeUnitRun& a, const CodeUnitRun& b) {
return a.code_units.start < b.code_units.start;
});
}
double Paragraph::GetLineXOffset(size_t line) {
@ -766,108 +889,105 @@ void Paragraph::PaintDecorations(SkCanvas* canvas,
}
}
std::vector<SkRect> Paragraph::GetRectsForRange(size_t start,
size_t end) const {
std::vector<SkRect> rects;
std::vector<Paragraph::TextBox> Paragraph::GetRectsForRange(size_t start,
size_t end) const {
std::vector<TextBox> boxes;
if (end <= start || start == end)
return rects;
size_t pos = 0;
size_t line;
for (line = 0; line < glyph_lines_.size(); ++line) {
if (start < pos + glyph_lines_[line].total_code_units)
for (const CodeUnitRun& run : code_unit_runs_) {
if (run.code_units.start >= end)
break;
pos += glyph_lines_[line].total_code_units;
}
if (line == glyph_lines_.size())
return rects;
if (run.code_units.end <= start)
continue;
if (end <= pos + glyph_lines_[line].total_code_units) {
rects.push_back(GetRectForLineRange(line, start - pos, end - pos));
return rects;
}
SkScalar top =
(run.line_number > 0) ? line_heights_[run.line_number - 1] : 0;
SkScalar bottom = line_heights_[run.line_number];
rects.push_back(GetRectForLineRange(line, start - pos,
glyph_lines_[line].total_code_units));
while (true) {
pos += glyph_lines_[line].total_code_units;
line++;
if (line == glyph_lines_.size())
break;
if (end <= pos + glyph_lines_[line].total_code_units) {
rects.push_back(GetRectForLineRange(line, 0, end - pos));
break;
SkScalar left, right;
if (run.code_units.start >= start && run.code_units.end <= end) {
left = run.x_pos.start;
right = run.x_pos.end;
} else {
rects.push_back(
GetRectForLineRange(line, 0, glyph_lines_[line].total_code_units));
left = SK_ScalarMax;
right = SK_ScalarMin;
for (const GlyphPosition& gp : run.positions) {
if (gp.code_units.start >= start && gp.code_units.end <= end) {
left = std::min(left, static_cast<SkScalar>(gp.x_pos.start));
right = std::max(right, static_cast<SkScalar>(gp.x_pos.end));
}
}
if (left == SK_ScalarMax || right == SK_ScalarMin)
continue;
}
boxes.emplace_back(SkRect::MakeLTRB(left, top, right, bottom),
run.direction);
}
return rects;
}
SkRect Paragraph::GetRectForLineRange(size_t line,
size_t start,
size_t end) const {
FXL_DCHECK(line < glyph_lines_.size());
const GlyphLine& glyph_line = glyph_lines_[line];
if (glyph_line.positions.empty())
return SkRect::MakeEmpty();
FXL_DCHECK(start < glyph_line.total_code_units);
SkScalar left = glyph_line.GetGlyphPosition(start).start;
end = std::min(end, glyph_line.total_code_units);
SkScalar right = glyph_line.GetGlyphPosition(end - 1).glyph_end();
SkScalar top = (line > 0) ? line_heights_[line - 1] : 0;
SkScalar bottom = line_heights_[line];
return SkRect::MakeLTRB(left, top, right, bottom);
return boxes;
}
Paragraph::PositionWithAffinity Paragraph::GetGlyphPositionAtCoordinate(
double dx,
double dy,
bool using_glyph_center_as_boundary) const {
if (line_heights_.empty())
double dy) const {
if (line_heights_.empty() || dy < 0)
return PositionWithAffinity(0, DOWNSTREAM);
size_t offset = 0;
size_t y_index;
for (y_index = 0; y_index < line_heights_.size() - 1; ++y_index) {
if (dy < line_heights_[y_index])
break;
offset += glyph_lines_[y_index].total_code_units;
}
const std::vector<GlyphPosition>& line_glyph_position =
glyph_lines_[y_index].positions;
if (line_glyph_position.empty()) {
int line_start_index = std::accumulate(
glyph_lines_.begin(), glyph_lines_.begin() + y_index, 0,
[](const int a, const GlyphLine& b) { return a + b.total_code_units; });
return PositionWithAffinity(line_start_index, DOWNSTREAM);
}
size_t x_index;
const GlyphPosition* gp = nullptr;
for (x_index = 0; x_index < line_glyph_position.size(); ++x_index) {
double glyph_end = (x_index < line_glyph_position.size() - 1)
? line_glyph_position[x_index + 1].start
: line_glyph_position[x_index].glyph_end();
double boundary;
if (using_glyph_center_as_boundary) {
boundary = (line_glyph_position[x_index].start + glyph_end) / 2.0f;
} else {
boundary = glyph_end;
}
if (dx < boundary)
? line_glyph_position[x_index + 1].x_pos.start
: line_glyph_position[x_index].x_pos.end;
if (dx < glyph_end) {
gp = &line_glyph_position[x_index];
break;
offset += line_glyph_position[x_index].code_units;
}
}
if (gp == nullptr) {
const GlyphPosition& last_glyph = line_glyph_position.back();
return PositionWithAffinity(last_glyph.code_units.end, UPSTREAM);
}
// Find the direction of the run that contains this glyph.
TextDirection direction = TextDirection::ltr;
for (const CodeUnitRun& run : code_unit_runs_) {
if (gp->code_units.start >= run.code_units.start &&
gp->code_units.end <= run.code_units.end) {
direction = run.direction;
break;
}
}
double glyph_center = (gp->x_pos.start + gp->x_pos.end) / 2;
if ((direction == TextDirection::ltr && dx < glyph_center) ||
(direction == TextDirection::rtl && dx >= glyph_center)) {
return PositionWithAffinity(gp->code_units.start, DOWNSTREAM);
} else {
return PositionWithAffinity(gp->code_units.end, UPSTREAM);
}
return PositionWithAffinity(offset, x_index > 0 ? UPSTREAM : DOWNSTREAM);
}
Paragraph::Range Paragraph::GetWordBoundary(size_t offset) const {
Paragraph::Range<size_t> Paragraph::GetWordBoundary(size_t offset) const {
// TODO(garyq): Consider punctuation as separate words.
if (text_.size() == 0)
return Range(0, 0);
return Range(
return Range<size_t>(0, 0);
return Range<size_t>(
minikin::getPrevWordBreakForCache(text_.data(), offset + 1, text_.size()),
minikin::getNextWordBreakForCache(text_.data(), offset, text_.size()));
}

View File

@ -29,9 +29,7 @@
#include "paragraph_style.h"
#include "styled_runs.h"
#include "third_party/gtest/include/gtest/gtest_prod.h"
#include "third_party/skia/include/core/SkPoint.h"
#include "third_party/skia/include/core/SkRect.h"
#include "third_party/skia/include/core/SkTextBlob.h"
#include "utils/WindowsUtils.h"
class SkCanvas;
@ -64,14 +62,32 @@ class Paragraph {
PositionWithAffinity(size_t p, Affinity a) : position(p), affinity(a) {}
};
struct TextBox {
const SkRect rect;
const TextDirection direction;
TextBox(SkRect r, TextDirection d) : rect(r), direction(d) {}
};
template <typename T>
struct Range {
Range(size_t s, size_t e) : start(s), end(e) {}
size_t start, end;
bool operator==(const Range& other) const {
Range(T s, T e) : start(s), end(e) {}
T start, end;
bool operator==(const Range<T>& other) const {
return start == other.start && end == other.end;
}
};
struct BidiRun {
BidiRun(size_t s, size_t e, TextDirection d, const TextStyle& st)
: start(s), end(e), direction(d), style(st) {}
size_t start, end;
TextDirection direction;
const TextStyle& style;
bool is_rtl() const { return direction == TextDirection::rtl; }
};
// Minikin Layout doLayout() and LineBreaker addStyleRun() has an
// O(N^2) (according to benchmarks) time complexity where N is the total
// number of characters. However, this is not significant for reasonably sized
@ -121,28 +137,15 @@ class Paragraph {
// Returns a vector of bounding boxes that enclose all text between start and
// end glyph indexes, including start and excluding end.
std::vector<SkRect> GetRectsForRange(size_t start, size_t end) const;
// Returns a bounding box that encloses the given starting and ending
// positions within a line.
SkRect GetRectForLineRange(size_t line, size_t start, size_t end) const;
std::vector<TextBox> GetRectsForRange(size_t start, size_t end) const;
// Returns the index of the glyph that corresponds to the provided coordinate,
// with the top left corner as the origin, and +y direction as down.
//
// When using_glyph_center_as_boundary == true, coords to the + direction of
// the center x-position of the glyph will be considered as the next glyph. A
// typical use-case for this is when the cursor is meant to be on either side
// of any given character. This allows the transition border to be middle of
// each character.
PositionWithAffinity GetGlyphPositionAtCoordinate(
double dx,
double dy,
bool using_glyph_center_as_boundary = false) const;
PositionWithAffinity GetGlyphPositionAtCoordinate(double dx, double dy) const;
// Finds the first and last glyphs that define a word containing the glyph at
// index offset.
Range GetWordBoundary(size_t offset) const;
Range<size_t> GetWordBoundary(size_t offset) const;
// Returns the number of lines the paragraph takes up. If the text exceeds the
// amount width and maxlines provides, Layout() truncates the extra text from
@ -185,6 +188,7 @@ class Paragraph {
// Starting data to layout.
std::vector<uint16_t> text_;
StyledRuns runs_;
std::vector<BidiRun> bidi_runs_;
ParagraphStyle paragraph_style_;
std::shared_ptr<FontCollection> font_collection_;
@ -205,30 +209,45 @@ class Paragraph {
bool did_exceed_max_lines_;
struct GlyphPosition {
const double start;
const double advance;
const size_t code_units;
Range<size_t> code_units;
Range<double> x_pos;
GlyphPosition(double s, double a, size_t cu)
: start(s), advance(a), code_units(cu) {}
double glyph_end() const { return start + advance; }
GlyphPosition(double x_start,
double x_advance,
size_t code_unit_index,
size_t code_unit_width);
};
struct GlyphLine {
// Glyph positions sorted by x coordinate.
const std::vector<GlyphPosition> positions;
const size_t total_code_units;
GlyphLine(std::vector<GlyphPosition>&& p, size_t tcu);
};
// Return the GlyphPosition containing the given code unit index within
// the line.
const GlyphPosition& GetGlyphPosition(size_t pos) const;
struct CodeUnitRun {
// Glyph positions sorted by code unit index.
std::vector<GlyphPosition> positions;
Range<size_t> code_units;
Range<double> x_pos;
size_t line_number;
TextDirection direction;
CodeUnitRun(std::vector<GlyphPosition>&& p,
Range<size_t> cu,
Range<double> x,
size_t line,
TextDirection dir);
};
// Holds the laid out x positions of each glyph.
std::vector<GlyphLine> glyph_lines_;
// Holds the positions of each range of code units in the text.
// Sorted in code unit index order.
std::vector<CodeUnitRun> code_unit_runs_;
// The max width of the paragraph as provided in the most recent Layout()
// call.
double width_ = -1.0f;
@ -260,6 +279,9 @@ class Paragraph {
// Break the text into lines.
bool ComputeLineBreaks();
// Break the text into runs based on LTR/RTL text direction.
bool ComputeBidiRuns();
// Calculate the starting X offset of a line based on the line's width and
// alignment.
double GetLineXOffset(size_t line);

View File

@ -891,10 +891,10 @@ TEST_F(ParagraphTest, GetGlyphPositionAtCoordinateParagraph) {
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(0, 0).position, 0ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(3, 3).position, 0ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(35, 1).position, 1ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(300, 2).position, 10ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(301, 2.2).position, 10ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(302, 2.6).position, 10ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(301, 2.1).position, 10ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(300, 2).position, 11ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(301, 2.2).position, 11ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(302, 2.6).position, 11ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(301, 2.1).position, 11ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(100000, 20).position,
18ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(450, 20).position, 16ull);
@ -912,7 +912,7 @@ TEST_F(ParagraphTest, GetGlyphPositionAtCoordinateParagraph) {
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(35, 90).position, 19ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(10000, 10000).position,
77ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(85, 10000).position, 74ull);
ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(85, 10000).position, 75ull);
}
TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(GetRectsForRangeParagraph)) {
@ -955,87 +955,89 @@ TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(GetRectsForRangeParagraph)) {
// NOTE: The base truth values may still need adjustment as the specifics
// are adjusted.
paint.setColor(SK_ColorRED);
std::vector<SkRect> rects = paragraph->GetRectsForRange(0, 0);
for (size_t i = 0; i < rects.size(); ++i) {
GetCanvas()->drawRect(rects[i], paint);
std::vector<txt::Paragraph::TextBox> boxes =
paragraph->GetRectsForRange(0, 0);
for (size_t i = 0; i < boxes.size(); ++i) {
GetCanvas()->drawRect(boxes[i].rect, paint);
}
EXPECT_EQ(rects.size(), 0ull);
EXPECT_EQ(boxes.size(), 0ull);
rects = paragraph->GetRectsForRange(0, 1);
for (size_t i = 0; i < rects.size(); ++i) {
GetCanvas()->drawRect(rects[i], paint);
boxes = paragraph->GetRectsForRange(0, 1);
for (size_t i = 0; i < boxes.size(); ++i) {
GetCanvas()->drawRect(boxes[i].rect, paint);
}
EXPECT_EQ(rects.size(), 1ull);
EXPECT_FLOAT_EQ(rects[0].left(), 0);
EXPECT_FLOAT_EQ(rects[0].top(), 0);
EXPECT_FLOAT_EQ(rects[0].right(), 28.417969);
EXPECT_FLOAT_EQ(rects[0].bottom(), 59);
EXPECT_EQ(boxes.size(), 1ull);
EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0);
EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0);
EXPECT_FLOAT_EQ(boxes[0].rect.right(), 28.417969);
EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 59);
paint.setColor(SK_ColorBLUE);
rects = paragraph->GetRectsForRange(2, 8);
for (size_t i = 0; i < rects.size(); ++i) {
GetCanvas()->drawRect(rects[i], paint);
boxes = paragraph->GetRectsForRange(2, 8);
for (size_t i = 0; i < boxes.size(); ++i) {
GetCanvas()->drawRect(boxes[i].rect, paint);
}
EXPECT_EQ(rects.size(), 1ull);
EXPECT_FLOAT_EQ(rects[0].left(), 56.835938);
EXPECT_FLOAT_EQ(rects[0].top(), 0);
EXPECT_FLOAT_EQ(rects[0].right(), 177.44922);
EXPECT_FLOAT_EQ(rects[0].bottom(), 59);
EXPECT_EQ(boxes.size(), 1ull);
EXPECT_FLOAT_EQ(boxes[0].rect.left(), 56.835938);
EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0);
EXPECT_FLOAT_EQ(boxes[0].rect.right(), 177.44922);
EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 59);
paint.setColor(SK_ColorGREEN);
rects = paragraph->GetRectsForRange(8, 21);
for (size_t i = 0; i < rects.size(); ++i) {
GetCanvas()->drawRect(rects[i], paint);
boxes = paragraph->GetRectsForRange(8, 21);
for (size_t i = 0; i < boxes.size(); ++i) {
GetCanvas()->drawRect(boxes[i].rect, paint);
}
EXPECT_EQ(rects.size(), 1ull);
EXPECT_FLOAT_EQ(rects[0].left(), 177);
EXPECT_FLOAT_EQ(rects[0].top(), 0);
EXPECT_FLOAT_EQ(rects[0].right(), 506.08984);
EXPECT_FLOAT_EQ(rects[0].bottom(), 59);
EXPECT_EQ(boxes.size(), 1ull);
EXPECT_FLOAT_EQ(boxes[0].rect.left(), 177);
EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0);
EXPECT_FLOAT_EQ(boxes[0].rect.right(), 506.08984);
EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 59);
paint.setColor(SK_ColorRED);
rects = paragraph->GetRectsForRange(30, 100);
for (size_t i = 0; i < rects.size(); ++i) {
GetCanvas()->drawRect(rects[i], paint);
boxes = paragraph->GetRectsForRange(30, 100);
for (size_t i = 0; i < boxes.size(); ++i) {
GetCanvas()->drawRect(boxes[i].rect, paint);
}
EXPECT_EQ(rects.size(), 4ull);
EXPECT_FLOAT_EQ(rects[0].left(), 210.83594);
EXPECT_FLOAT_EQ(rects[0].top(), 59);
EXPECT_FLOAT_EQ(rects[0].right(), 463.44922);
EXPECT_FLOAT_EQ(rects[0].bottom(), 118);
EXPECT_EQ(boxes.size(), 4ull);
EXPECT_FLOAT_EQ(boxes[0].rect.left(), 210.83594);
EXPECT_FLOAT_EQ(boxes[0].rect.top(), 59);
EXPECT_FLOAT_EQ(boxes[0].rect.right(), 463.44922);
EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 118);
// TODO(garyq): The following set of vals are definetly wrong and
// end of paragraph handling needs to be fixed in a later patch.
EXPECT_FLOAT_EQ(rects[3].left(), 0);
EXPECT_FLOAT_EQ(rects[3].top(), 236);
EXPECT_FLOAT_EQ(rects[3].right(), 142.08984);
EXPECT_FLOAT_EQ(rects[3].bottom(), 295);
EXPECT_FLOAT_EQ(boxes[3].rect.left(), 0);
EXPECT_FLOAT_EQ(boxes[3].rect.top(), 236);
EXPECT_FLOAT_EQ(boxes[3].rect.right(), 142.08984);
EXPECT_FLOAT_EQ(boxes[3].rect.bottom(), 295);
paint.setColor(SK_ColorBLUE);
rects = paragraph->GetRectsForRange(19, 22);
for (size_t i = 0; i < rects.size(); ++i) {
GetCanvas()->drawRect(rects[i], paint);
boxes = paragraph->GetRectsForRange(19, 22);
for (size_t i = 0; i < boxes.size(); ++i) {
GetCanvas()->drawRect(boxes[i].rect, paint);
}
EXPECT_EQ(rects.size(), 1ull);
EXPECT_FLOAT_EQ(rects[0].left(), 449.25391);
EXPECT_FLOAT_EQ(rects[0].top(), 0);
EXPECT_FLOAT_EQ(rects[0].right(), 519.44922);
EXPECT_FLOAT_EQ(rects[0].bottom(), 59);
EXPECT_EQ(boxes.size(), 1ull);
EXPECT_FLOAT_EQ(boxes[0].rect.left(), 449.25391);
EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0);
EXPECT_FLOAT_EQ(boxes[0].rect.right(), 519.44922);
EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 59);
paint.setColor(SK_ColorRED);
rects = paragraph->GetRectsForRange(21, 21);
for (size_t i = 0; i < rects.size(); ++i) {
GetCanvas()->drawRect(rects[i], paint);
boxes = paragraph->GetRectsForRange(21, 21);
for (size_t i = 0; i < boxes.size(); ++i) {
GetCanvas()->drawRect(boxes[i].rect, paint);
}
EXPECT_EQ(rects.size(), 0ull);
EXPECT_EQ(boxes.size(), 0ull);
ASSERT_TRUE(Snapshot());
}
SkRect GetCoordinatesForGlyphPosition(const txt::Paragraph& paragraph,
size_t pos) {
std::vector<SkRect> rects = paragraph.GetRectsForRange(pos, pos + 1);
return !rects.empty() ? rects.front() : SkRect::MakeEmpty();
std::vector<txt::Paragraph::TextBox> boxes =
paragraph.GetRectsForRange(pos, pos + 1);
return !boxes.empty() ? boxes.front().rect : SkRect::MakeEmpty();
}
TEST_F(ParagraphTest, GetWordBoundaryParagraph) {
@ -1077,35 +1079,42 @@ TEST_F(ParagraphTest, GetWordBoundaryParagraph) {
SkRect rect = GetCoordinatesForGlyphPosition(*paragraph, 0);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(0), txt::Paragraph::Range(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(1), txt::Paragraph::Range(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(2), txt::Paragraph::Range(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(3), txt::Paragraph::Range(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(4), txt::Paragraph::Range(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(0), txt::Paragraph::Range<size_t>(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(1), txt::Paragraph::Range<size_t>(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(2), txt::Paragraph::Range<size_t>(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(3), txt::Paragraph::Range<size_t>(0, 5));
EXPECT_EQ(paragraph->GetWordBoundary(4), txt::Paragraph::Range<size_t>(0, 5));
rect = GetCoordinatesForGlyphPosition(*paragraph, 5);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(5), txt::Paragraph::Range(5, 6));
EXPECT_EQ(paragraph->GetWordBoundary(5), txt::Paragraph::Range<size_t>(5, 6));
rect = GetCoordinatesForGlyphPosition(*paragraph, 6);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(6), txt::Paragraph::Range(6, 7));
EXPECT_EQ(paragraph->GetWordBoundary(6), txt::Paragraph::Range<size_t>(6, 7));
rect = GetCoordinatesForGlyphPosition(*paragraph, 7);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(7), txt::Paragraph::Range(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(8), txt::Paragraph::Range(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(9), txt::Paragraph::Range(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(10), txt::Paragraph::Range(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(11), txt::Paragraph::Range(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(7),
txt::Paragraph::Range<size_t>(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(8),
txt::Paragraph::Range<size_t>(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(9),
txt::Paragraph::Range<size_t>(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(10),
txt::Paragraph::Range<size_t>(7, 12));
EXPECT_EQ(paragraph->GetWordBoundary(11),
txt::Paragraph::Range<size_t>(7, 12));
rect = GetCoordinatesForGlyphPosition(*paragraph, 12);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(12), txt::Paragraph::Range(12, 13));
EXPECT_EQ(paragraph->GetWordBoundary(12),
txt::Paragraph::Range<size_t>(12, 13));
rect = GetCoordinatesForGlyphPosition(*paragraph, 13);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(13), txt::Paragraph::Range(13, 18));
EXPECT_EQ(paragraph->GetWordBoundary(13),
txt::Paragraph::Range<size_t>(13, 18));
rect = GetCoordinatesForGlyphPosition(*paragraph, 18);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
@ -1121,15 +1130,17 @@ TEST_F(ParagraphTest, GetWordBoundaryParagraph) {
rect = GetCoordinatesForGlyphPosition(*paragraph, 30);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(30), txt::Paragraph::Range(30, 31));
EXPECT_EQ(paragraph->GetWordBoundary(30),
txt::Paragraph::Range<size_t>(30, 31));
rect = GetCoordinatesForGlyphPosition(*paragraph, 31);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
rect = GetCoordinatesForGlyphPosition(*paragraph, icu_text.length() - 5);
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);
EXPECT_EQ(paragraph->GetWordBoundary(icu_text.length() - 1),
txt::Paragraph::Range(icu_text.length() - 5, icu_text.length()));
EXPECT_EQ(
paragraph->GetWordBoundary(icu_text.length() - 1),
txt::Paragraph::Range<size_t>(icu_text.length() - 5, icu_text.length()));
rect = GetCoordinatesForGlyphPosition(*paragraph, icu_text.length());
GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint);