From affa4e7c46e0dcd5f3ac1b47b5678356850ae538 Mon Sep 17 00:00:00 2001 From: Jason Simmons Date: Wed, 6 Dec 2017 14:46:03 -0800 Subject: [PATCH] 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 --- lib/ui/text/paragraph_impl_txt.cc | 14 +- third_party/txt/src/txt/paragraph.cc | 378 ++++++++++++------- third_party/txt/src/txt/paragraph.h | 84 +++-- third_party/txt/tests/paragraph_unittests.cc | 163 ++++---- 4 files changed, 396 insertions(+), 243 deletions(-) diff --git a/lib/ui/text/paragraph_impl_txt.cc b/lib/ui/text/paragraph_impl_txt.cc index 939e53b7a..565b63f54 100644 --- a/lib/ui/text/paragraph_impl_txt.cc +++ b/lib/ui/text/paragraph_impl_txt.cc @@ -64,11 +64,11 @@ void ParagraphImplTxt::paint(Canvas* canvas, double x, double y) { std::vector ParagraphImplTxt::getRectsForRange(unsigned start, unsigned end) { std::vector result; - std::vector rects = m_paragraph->GetRectsForRange(start, end); - for (size_t i = 0; i < rects.size(); ++i) { - result.push_back(TextBox( - rects[i], static_cast( - m_paragraph->GetParagraphStyle().text_direction))); + std::vector boxes = + m_paragraph->GetRectsForRange(start, end); + for (const txt::Paragraph::TextBox& box : boxes) { + result.emplace_back(box.rect, + static_cast(box.direction)); } return result; } @@ -76,14 +76,14 @@ std::vector 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(pos.affinity))); return result; } Dart_Handle ParagraphImplTxt::getWordBoundary(unsigned offset) { - txt::Paragraph::Range point = m_paragraph->GetWordBoundary(offset); + txt::Paragraph::Range 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)); diff --git a/third_party/txt/src/txt/paragraph.cc b/third_party/txt/src/txt/paragraph.cc index 9c1c281d6..45bef6c06 100644 --- a/third_party/txt/src/txt/paragraph.cc +++ b/third_party/txt/src/txt/paragraph.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -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& text, size_t start, size_t end, - std::vector* words) { + std::vector>* 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& 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&& 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&& p, + Range cu, + Range 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 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 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(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(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 words; + std::vector> 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 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 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 glyph_positions; + std::vector line_glyph_positions; double run_x_offset = GetLineXOffset(line_number); double justify_x_offset = 0; std::vector 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 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 glyph_blobs; + std::vector> 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 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::quiet_NaN(); - for (const Range& glyph_blob : glyph_blobs) { + // Build a Skia text blob from each group of glyphs. + for (const Range& 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 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 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::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 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(run.start, run.end), + Range(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 Paragraph::GetRectsForRange(size_t start, - size_t end) const { - std::vector rects; - - if (end <= start || start == end) - return rects; +std::vector Paragraph::GetRectsForRange(size_t start, + size_t end) const { + std::vector boxes; - 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 (end <= pos + glyph_lines_[line].total_code_units) { - rects.push_back(GetRectForLineRange(line, start - pos, end - pos)); - return rects; - } - - rects.push_back(GetRectForLineRange(line, start - pos, - glyph_lines_[line].total_code_units)); + if (run.code_units.end <= start) + continue; - while (true) { - pos += glyph_lines_[line].total_code_units; - line++; - if (line == glyph_lines_.size()) - break; + SkScalar top = + (run.line_number > 0) ? line_heights_[run.line_number - 1] : 0; + SkScalar bottom = line_heights_[run.line_number]; - 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(gp.x_pos.start)); + right = std::max(right, static_cast(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& 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; + ? 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; } + } + + if (gp == nullptr) { + const GlyphPosition& last_glyph = line_glyph_position.back(); + return PositionWithAffinity(last_glyph.code_units.end, UPSTREAM); + } - if (dx < boundary) + // 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; + } + } - offset += line_glyph_position[x_index].code_units; + 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 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(0, 0); + return Range( minikin::getPrevWordBreakForCache(text_.data(), offset + 1, text_.size()), minikin::getNextWordBreakForCache(text_.data(), offset, text_.size())); } diff --git a/third_party/txt/src/txt/paragraph.h b/third_party/txt/src/txt/paragraph.h index deb955dc0..706b2751a 100644 --- a/third_party/txt/src/txt/paragraph.h +++ b/third_party/txt/src/txt/paragraph.h @@ -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 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& 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 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 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 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 text_; StyledRuns runs_; + std::vector bidi_runs_; ParagraphStyle paragraph_style_; std::shared_ptr 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; - - GlyphPosition(double s, double a, size_t cu) - : start(s), advance(a), code_units(cu) {} + Range code_units; + Range x_pos; - 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 positions; const size_t total_code_units; GlyphLine(std::vector&& 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 positions; + Range code_units; + Range x_pos; + size_t line_number; + TextDirection direction; + + CodeUnitRun(std::vector&& p, + Range cu, + Range x, + size_t line, + TextDirection dir); }; // Holds the laid out x positions of each glyph. std::vector glyph_lines_; + // Holds the positions of each range of code units in the text. + // Sorted in code unit index order. + std::vector 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); diff --git a/third_party/txt/tests/paragraph_unittests.cc b/third_party/txt/tests/paragraph_unittests.cc index 494012da9..5beb9cbbf 100644 --- a/third_party/txt/tests/paragraph_unittests.cc +++ b/third_party/txt/tests/paragraph_unittests.cc @@ -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 rects = paragraph->GetRectsForRange(0, 0); - for (size_t i = 0; i < rects.size(); ++i) { - GetCanvas()->drawRect(rects[i], paint); + std::vector 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 rects = paragraph.GetRectsForRange(pos, pos + 1); - return !rects.empty() ? rects.front() : SkRect::MakeEmpty(); + std::vector 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(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)); 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(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(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(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)); 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(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(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(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(icu_text.length() - 5, icu_text.length())); rect = GetCoordinatesForGlyphPosition(*paragraph, icu_text.length()); GetCanvas()->drawLine(rect.fLeft, rect.fTop, rect.fLeft, rect.fBottom, paint); -- GitLab