| /* |
| * Copyright 2017 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| #include "paragraph.h" |
| |
| #include <hb.h> |
| #include <minikin/Layout.h> |
| |
| #include <algorithm> |
| #include <limits> |
| #include <map> |
| #include <numeric> |
| #include <utility> |
| #include <vector> |
| |
| #include "flutter/fml/logging.h" |
| #include "font_collection.h" |
| #include "font_skia.h" |
| #include "minikin/FontLanguageListCache.h" |
| #include "minikin/GraphemeBreak.h" |
| #include "minikin/HbFontCache.h" |
| #include "minikin/LayoutUtils.h" |
| #include "minikin/LineBreaker.h" |
| #include "minikin/MinikinFont.h" |
| #include "third_party/skia/include/core/SkCanvas.h" |
| #include "third_party/skia/include/core/SkFont.h" |
| #include "third_party/skia/include/core/SkFontMetrics.h" |
| #include "third_party/skia/include/core/SkMaskFilter.h" |
| #include "third_party/skia/include/core/SkPaint.h" |
| #include "third_party/skia/include/core/SkTextBlob.h" |
| #include "third_party/skia/include/core/SkTypeface.h" |
| #include "third_party/skia/include/effects/SkDashPathEffect.h" |
| #include "third_party/skia/include/effects/SkDiscretePathEffect.h" |
| #include "unicode/ubidi.h" |
| #include "unicode/utf16.h" |
| |
| namespace txt { |
| namespace { |
| |
| class GlyphTypeface { |
| public: |
| GlyphTypeface(sk_sp<SkTypeface> typeface, minikin::FontFakery fakery) |
| : typeface_(std::move(typeface)), |
| fake_bold_(fakery.isFakeBold()), |
| fake_italic_(fakery.isFakeItalic()) {} |
| |
| bool operator==(GlyphTypeface& other) { |
| return other.typeface_.get() == typeface_.get() && |
| other.fake_bold_ == fake_bold_ && other.fake_italic_ == fake_italic_; |
| } |
| |
| bool operator!=(GlyphTypeface& other) { return !(*this == other); } |
| |
| void apply(SkFont& font) { |
| font.setTypeface(typeface_); |
| font.setEmbolden(fake_bold_); |
| font.setSkewX(fake_italic_ ? -SK_Scalar1 / 4 : 0); |
| } |
| |
| private: |
| sk_sp<SkTypeface> typeface_; |
| bool fake_bold_; |
| bool fake_italic_; |
| }; |
| |
| GlyphTypeface GetGlyphTypeface(const minikin::Layout& layout, size_t index) { |
| const FontSkia* font = static_cast<const FontSkia*>(layout.getFont(index)); |
| return GlyphTypeface(font->GetSkTypeface(), layout.getFakery(index)); |
| } |
| |
| // Return ranges of text that have the same typeface in the layout. |
| std::vector<Paragraph::Range<size_t>> GetLayoutTypefaceRuns( |
| const minikin::Layout& layout) { |
| std::vector<Paragraph::Range<size_t>> result; |
| if (layout.nGlyphs() == 0) |
| return result; |
| size_t run_start = 0; |
| GlyphTypeface run_typeface = GetGlyphTypeface(layout, run_start); |
| for (size_t i = 1; i < layout.nGlyphs(); ++i) { |
| GlyphTypeface typeface = GetGlyphTypeface(layout, i); |
| if (typeface != run_typeface) { |
| result.emplace_back(run_start, i); |
| run_start = i; |
| run_typeface = typeface; |
| } |
| } |
| result.emplace_back(run_start, layout.nGlyphs()); |
| return result; |
| } |
| |
| int GetWeight(const FontWeight weight) { |
| switch (weight) { |
| case FontWeight::w100: |
| return 1; |
| case FontWeight::w200: |
| return 2; |
| case FontWeight::w300: |
| return 3; |
| case FontWeight::w400: // Normal. |
| return 4; |
| case FontWeight::w500: |
| return 5; |
| case FontWeight::w600: |
| return 6; |
| case FontWeight::w700: // Bold. |
| return 7; |
| case FontWeight::w800: |
| return 8; |
| case FontWeight::w900: |
| return 9; |
| default: |
| return -1; |
| } |
| } |
| |
| int GetWeight(const TextStyle& style) { |
| return GetWeight(style.font_weight); |
| } |
| |
| bool GetItalic(const TextStyle& style) { |
| switch (style.font_style) { |
| case FontStyle::italic: |
| return true; |
| case FontStyle::normal: |
| default: |
| return false; |
| } |
| } |
| |
| minikin::FontStyle GetMinikinFontStyle(const TextStyle& style) { |
| uint32_t language_list_id = |
| style.locale.empty() |
| ? minikin::FontLanguageListCache::kEmptyListId |
| : minikin::FontStyle::registerLanguageList(style.locale); |
| return minikin::FontStyle(language_list_id, 0, GetWeight(style), |
| GetItalic(style)); |
| } |
| |
| void GetFontAndMinikinPaint(const TextStyle& style, |
| minikin::FontStyle* font, |
| minikin::MinikinPaint* paint) { |
| *font = GetMinikinFontStyle(style); |
| paint->size = style.font_size; |
| // Divide by font size so letter spacing is pixels, not proportional to font |
| // size. |
| paint->letterSpacing = style.letter_spacing / style.font_size; |
| paint->wordSpacing = style.word_spacing; |
| paint->scaleX = 1.0f; |
| // Prevent spacing rounding in Minikin. This causes jitter when switching |
| // between same text content with different runs composing it, however, it |
| // also produces more accurate layouts. |
| paint->paintFlags |= minikin::LinearTextFlag; |
| } |
| |
| void FindWords(const std::vector<uint16_t>& text, |
| size_t start, |
| size_t end, |
| std::vector<Paragraph::Range<size_t>>* words) { |
| bool in_word = false; |
| size_t word_start; |
| for (size_t i = start; i < end; ++i) { |
| bool is_space = minikin::isWordSpace(text[i]); |
| if (!in_word && !is_space) { |
| word_start = i; |
| in_word = true; |
| } else if (in_word && is_space) { |
| words->emplace_back(word_start, i); |
| in_word = false; |
| } |
| } |
| if (in_word) |
| words->emplace_back(word_start, end); |
| } |
| |
| } // namespace |
| |
| 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) {} |
| |
| void Paragraph::GlyphPosition::Shift(double delta) { |
| x_pos.Shift(delta); |
| } |
| |
| Paragraph::GlyphLine::GlyphLine(std::vector<GlyphPosition>&& p, size_t tcu) |
| : positions(std::move(p)), total_code_units(tcu) {} |
| |
| Paragraph::CodeUnitRun::CodeUnitRun(std::vector<GlyphPosition>&& p, |
| Range<size_t> cu, |
| Range<double> x, |
| size_t line, |
| const SkFontMetrics& metrics, |
| TextDirection dir) |
| : positions(std::move(p)), |
| code_units(cu), |
| x_pos(x), |
| line_number(line), |
| font_metrics(metrics), |
| direction(dir) {} |
| |
| void Paragraph::CodeUnitRun::Shift(double delta) { |
| x_pos.Shift(delta); |
| for (GlyphPosition& position : positions) |
| position.Shift(delta); |
| } |
| |
| Paragraph::Paragraph() { |
| breaker_.setLocale(icu::Locale(), nullptr); |
| } |
| |
| Paragraph::~Paragraph() = default; |
| |
| void Paragraph::SetText(std::vector<uint16_t> text, StyledRuns runs) { |
| needs_layout_ = true; |
| if (text.size() == 0) |
| return; |
| text_ = std::move(text); |
| runs_ = std::move(runs); |
| } |
| |
| bool Paragraph::ComputeLineBreaks() { |
| line_ranges_.clear(); |
| line_widths_.clear(); |
| max_intrinsic_width_ = 0; |
| |
| std::vector<size_t> newline_positions; |
| for (size_t i = 0; i < text_.size(); ++i) { |
| ULineBreak ulb = static_cast<ULineBreak>( |
| u_getIntPropertyValue(text_[i], UCHAR_LINE_BREAK)); |
| if (ulb == U_LB_LINE_FEED || ulb == U_LB_MANDATORY_BREAK) |
| newline_positions.push_back(i); |
| } |
| newline_positions.push_back(text_.size()); |
| |
| size_t run_index = 0; |
| for (size_t newline_index = 0; newline_index < newline_positions.size(); |
| ++newline_index) { |
| size_t block_start = |
| (newline_index > 0) ? newline_positions[newline_index - 1] + 1 : 0; |
| size_t block_end = newline_positions[newline_index]; |
| size_t block_size = block_end - block_start; |
| |
| if (block_size == 0) { |
| line_ranges_.emplace_back(block_start, block_end, block_end, |
| block_end + 1, true); |
| line_widths_.push_back(0); |
| continue; |
| } |
| |
| breaker_.setLineWidths(0.0f, 0, width_); |
| breaker_.setJustified(paragraph_style_.text_align == TextAlign::justify); |
| breaker_.setStrategy(paragraph_style_.break_strategy); |
| breaker_.resize(block_size); |
| memcpy(breaker_.buffer(), text_.data() + block_start, |
| block_size * sizeof(text_[0])); |
| breaker_.setText(); |
| |
| // Add the runs that include this line to the LineBreaker. |
| double block_total_width = 0; |
| while (run_index < runs_.size()) { |
| StyledRuns::Run run = runs_.GetRun(run_index); |
| if (run.start >= block_end) |
| break; |
| if (run.end < block_start) { |
| run_index++; |
| continue; |
| } |
| |
| minikin::FontStyle font; |
| minikin::MinikinPaint paint; |
| GetFontAndMinikinPaint(run.style, &font, &paint); |
| std::shared_ptr<minikin::FontCollection> collection = |
| GetMinikinFontCollectionForStyle(run.style); |
| if (collection == nullptr) { |
| FML_LOG(INFO) << "Could not find font collection for families \"" |
| << (run.style.font_families.empty() |
| ? "" |
| : run.style.font_families[0]) |
| << "\"."; |
| return false; |
| } |
| size_t run_start = std::max(run.start, block_start) - block_start; |
| size_t run_end = std::min(run.end, block_end) - block_start; |
| bool isRtl = (paragraph_style_.text_direction == TextDirection::rtl); |
| double run_width = breaker_.addStyleRun(&paint, collection, font, |
| run_start, run_end, isRtl); |
| block_total_width += run_width; |
| |
| if (run.end > block_end) |
| break; |
| run_index++; |
| } |
| |
| max_intrinsic_width_ = std::max(max_intrinsic_width_, block_total_width); |
| |
| size_t breaks_count = breaker_.computeBreaks(); |
| const int* breaks = breaker_.getBreaks(); |
| for (size_t i = 0; i < breaks_count; ++i) { |
| size_t break_start = (i > 0) ? breaks[i - 1] : 0; |
| size_t line_start = break_start + block_start; |
| size_t line_end = breaks[i] + block_start; |
| bool hard_break = i == breaks_count - 1; |
| size_t line_end_including_newline = |
| (hard_break && line_end < text_.size()) ? line_end + 1 : line_end; |
| size_t line_end_excluding_whitespace = line_end; |
| while ( |
| line_end_excluding_whitespace > line_start && |
| minikin::isLineEndSpace(text_[line_end_excluding_whitespace - 1])) { |
| line_end_excluding_whitespace--; |
| } |
| line_ranges_.emplace_back(line_start, line_end, |
| line_end_excluding_whitespace, |
| line_end_including_newline, hard_break); |
| line_widths_.push_back(breaker_.getWidths()[i]); |
| } |
| |
| breaker_.finish(); |
| } |
| |
| return true; |
| } |
| |
| bool Paragraph::ComputeBidiRuns(std::vector<BidiRun>* result) { |
| if (text_.empty()) |
| return true; |
| |
| 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_RTL |
| : UBIDI_LTR; |
| UErrorCode status = U_ZERO_ERROR; |
| ubidi_setPara(bidi.get(), reinterpret_cast<const UChar*>(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. |
| std::vector<BidiRun> chunks; |
| 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); |
| chunks.emplace_back(chunk_start, chunk_end, text_direction, |
| styled_run.style); |
| chunk_start = chunk_end; |
| } |
| |
| if (text_direction == TextDirection::ltr) { |
| result->insert(result->end(), chunks.begin(), chunks.end()); |
| } else { |
| result->insert(result->end(), chunks.rbegin(), chunks.rend()); |
| } |
| } |
| |
| return true; |
| } |
| |
| void Paragraph::ComputeStrut(StrutMetrics* strut, SkFont& font) { |
| strut->ascent = 0; |
| strut->descent = 0; |
| strut->leading = 0; |
| strut->half_leading = 0; |
| strut->line_height = 0; |
| strut->force_strut = false; |
| |
| // Font size must be positive. |
| bool valid_strut = |
| paragraph_style_.strut_enabled && paragraph_style_.strut_font_size >= 0; |
| if (!valid_strut) { |
| return; |
| } |
| // force_strut makes all lines have exactly the strut metrics, and ignores all |
| // actual metrics. We only force the strut if the strut is non-zero and valid. |
| strut->force_strut = paragraph_style_.force_strut_height && valid_strut; |
| minikin::FontStyle minikin_font_style( |
| 0, GetWeight(paragraph_style_.strut_font_weight), |
| paragraph_style_.strut_font_style == FontStyle::italic); |
| |
| std::shared_ptr<minikin::FontCollection> collection = |
| font_collection_->GetMinikinFontCollectionForFamilies( |
| paragraph_style_.strut_font_families, ""); |
| if (!collection) { |
| return; |
| } |
| minikin::FakedFont faked_font = collection->baseFontFaked(minikin_font_style); |
| |
| if (faked_font.font != nullptr) { |
| SkString str; |
| static_cast<FontSkia*>(faked_font.font) |
| ->GetSkTypeface() |
| ->getFamilyName(&str); |
| font.setTypeface(static_cast<FontSkia*>(faked_font.font)->GetSkTypeface()); |
| font.setSize(paragraph_style_.strut_font_size); |
| SkFontMetrics strut_metrics; |
| font.getMetrics(&strut_metrics); |
| |
| strut->ascent = paragraph_style_.strut_height * -strut_metrics.fAscent; |
| strut->descent = paragraph_style_.strut_height * strut_metrics.fDescent; |
| strut->leading = |
| // Use font's leading if there is no user specified strut leading. |
| paragraph_style_.strut_leading < 0 |
| ? strut_metrics.fLeading |
| : (paragraph_style_.strut_leading * |
| (strut_metrics.fDescent - strut_metrics.fAscent)); |
| strut->half_leading = strut->leading / 2; |
| strut->line_height = strut->ascent + strut->descent + strut->leading; |
| } |
| } |
| |
| void Paragraph::Layout(double width, bool force) { |
| // Do not allow calling layout multiple times without changing anything. |
| if (!needs_layout_ && width == width_ && !force) { |
| return; |
| } |
| needs_layout_ = false; |
| |
| width_ = floor(width); |
| |
| if (!ComputeLineBreaks()) |
| return; |
| |
| std::vector<BidiRun> bidi_runs; |
| if (!ComputeBidiRuns(&bidi_runs)) |
| return; |
| |
| SkFont font; |
| font.setEdging(SkFont::Edging::kAntiAlias); |
| font.setSubpixel(true); |
| font.setHinting(kSlight_SkFontHinting); |
| |
| records_.clear(); |
| line_heights_.clear(); |
| line_baselines_.clear(); |
| glyph_lines_.clear(); |
| code_unit_runs_.clear(); |
| line_max_spacings_.clear(); |
| line_max_descent_.clear(); |
| line_max_ascent_.clear(); |
| max_right_ = FLT_MIN; |
| min_left_ = FLT_MAX; |
| |
| minikin::Layout layout; |
| SkTextBlobBuilder builder; |
| double y_offset = 0; |
| double prev_max_descent = 0; |
| double max_word_width = 0; |
| |
| // Compute strut minimums according to paragraph_style_. |
| StrutMetrics strut; |
| ComputeStrut(&strut, font); |
| |
| // Paragraph bounds tracking. |
| size_t line_limit = std::min(paragraph_style_.max_lines, line_ranges_.size()); |
| did_exceed_max_lines_ = (line_ranges_.size() > paragraph_style_.max_lines); |
| |
| for (size_t line_number = 0; line_number < line_limit; ++line_number) { |
| const LineRange& line_range = line_ranges_[line_number]; |
| |
| // Break the line into words if justification should be applied. |
| 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 && |
| line_number != line_limit - 1 && !line_range.hard_break); |
| FindWords(text_, line_range.start, line_range.end, &words); |
| if (justify_line) { |
| if (words.size() > 1) { |
| word_gap_width = |
| (width_ - line_widths_[line_number]) / (words.size() - 1); |
| } |
| } |
| |
| // Exclude trailing whitespace from justified lines so the last visible |
| // character in the line will be flush with the right margin. |
| size_t line_end_index = |
| (paragraph_style_.effective_align() == TextAlign::right || |
| paragraph_style_.effective_align() == TextAlign::center || |
| paragraph_style_.effective_align() == TextAlign::justify) |
| ? line_range.end_excluding_whitespace |
| : line_range.end; |
| |
| // Find the runs comprising this line. |
| std::vector<BidiRun> line_runs; |
| for (const BidiRun& bidi_run : bidi_runs) { |
| // A "ghost" run is a run that does not impact the layout, breaking, |
| // alignment, width, etc but is still "visible" through getRectsForRange. |
| // For example, trailing whitespace on centered text can be scrolled |
| // through with the caret but will not wrap the line. |
| // |
| // Here, we add an additional run for the whitespace, but dont |
| // let it impact metrics. After layout of the whitespace run, we do not |
| // add its width into the x-offset adjustment, effectively nullifying its |
| // impact on the layout. |
| std::unique_ptr<BidiRun> ghost_run = nullptr; |
| if (paragraph_style_.ellipsis.empty() && |
| line_range.end_excluding_whitespace < line_range.end && |
| bidi_run.start() <= line_range.end && |
| bidi_run.end() > line_end_index) { |
| ghost_run = std::make_unique<BidiRun>( |
| std::max(bidi_run.start(), line_end_index), |
| std::min(bidi_run.end(), line_range.end), bidi_run.direction(), |
| bidi_run.style(), true); |
| } |
| // Include the ghost run before normal run if RTL |
| if (bidi_run.direction() == TextDirection::rtl && ghost_run != nullptr) { |
| line_runs.push_back(*ghost_run); |
| } |
| // Emplace a normal line run. |
| if (bidi_run.start() < line_end_index && |
| bidi_run.end() > line_range.start) { |
| line_runs.emplace_back(std::max(bidi_run.start(), line_range.start), |
| std::min(bidi_run.end(), line_end_index), |
| bidi_run.direction(), bidi_run.style()); |
| } |
| // Include the ghost run after normal run if LTR |
| if (bidi_run.direction() == TextDirection::ltr && ghost_run != nullptr) { |
| line_runs.push_back(*ghost_run); |
| } |
| } |
| bool line_runs_all_rtl = |
| line_runs.size() && |
| std::accumulate( |
| line_runs.begin(), line_runs.end(), true, |
| [](const bool a, const BidiRun& b) { return a && b.is_rtl(); }); |
| if (line_runs_all_rtl) { |
| std::reverse(words.begin(), words.end()); |
| } |
| |
| std::vector<GlyphPosition> line_glyph_positions; |
| std::vector<CodeUnitRun> line_code_unit_runs; |
| double run_x_offset = 0; |
| double justify_x_offset = 0; |
| std::vector<PaintRecord> paint_records; |
| |
| for (auto line_run_it = line_runs.begin(); line_run_it != line_runs.end(); |
| ++line_run_it) { |
| const BidiRun& run = *line_run_it; |
| minikin::FontStyle minikin_font; |
| minikin::MinikinPaint minikin_paint; |
| GetFontAndMinikinPaint(run.style(), &minikin_font, &minikin_paint); |
| font.setSize(run.style().font_size); |
| |
| std::shared_ptr<minikin::FontCollection> minikin_font_collection = |
| GetMinikinFontCollectionForStyle(run.style()); |
| |
| // Lay out this run. |
| uint16_t* text_ptr = text_.data(); |
| size_t text_start = run.start(); |
| size_t text_count = run.end() - run.start(); |
| size_t text_size = text_.size(); |
| |
| // Apply ellipsizing if the run was not completely laid out and this |
| // is the last line (or lines are unlimited). |
| const std::u16string& ellipsis = paragraph_style_.ellipsis; |
| std::vector<uint16_t> ellipsized_text; |
| if (ellipsis.length() && !isinf(width_) && !line_range.hard_break && |
| line_run_it == line_runs.end() - 1 && |
| (line_number == line_limit - 1 || |
| paragraph_style_.unlimited_lines())) { |
| float ellipsis_width = layout.measureText( |
| reinterpret_cast<const uint16_t*>(ellipsis.data()), 0, |
| ellipsis.length(), ellipsis.length(), run.is_rtl(), minikin_font, |
| minikin_paint, minikin_font_collection, nullptr); |
| |
| std::vector<float> text_advances(text_count); |
| float text_width = |
| layout.measureText(text_ptr, text_start, text_count, text_.size(), |
| run.is_rtl(), minikin_font, minikin_paint, |
| minikin_font_collection, text_advances.data()); |
| |
| // Truncate characters from the text until the ellipsis fits. |
| size_t truncate_count = 0; |
| while (truncate_count < text_count && |
| run_x_offset + text_width + ellipsis_width > width_) { |
| text_width -= text_advances[text_count - truncate_count - 1]; |
| truncate_count++; |
| } |
| |
| ellipsized_text.reserve(text_count - truncate_count + |
| ellipsis.length()); |
| ellipsized_text.insert(ellipsized_text.begin(), |
| 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(); |
| text_size = text_count; |
| |
| // If there is no line limit, then skip all lines after the ellipsized |
| // line. |
| if (paragraph_style_.unlimited_lines()) { |
| line_limit = line_number + 1; |
| did_exceed_max_lines_ = true; |
| } |
| } |
| |
| layout.doLayout(text_ptr, text_start, text_count, text_size, run.is_rtl(), |
| minikin_font, minikin_paint, minikin_font_collection); |
| |
| if (layout.nGlyphs() == 0) |
| continue; |
| |
| // When laying out RTL ghost runs, shift the run_x_offset here by the |
| // advance so that the ghost run is positioned to the left of the first |
| // real run of text in the line. However, since we do not want it to |
| // impact the layout of real text, this advance is subsequently added |
| // back into the run_x_offset after the ghost run positions have been |
| // calcuated and before the next real run of text is laid out, ensuring |
| // later runs are laid out in the same position as if there were no ghost |
| // run. |
| if (run.is_ghost() && run.is_rtl()) |
| run_x_offset -= layout.getAdvance(); |
| |
| std::vector<float> layout_advances(text_count); |
| layout.getAdvances(layout_advances.data()); |
| |
| // Break the layout into blobs that share the same SkPaint parameters. |
| std::vector<Range<size_t>> glyph_blobs = GetLayoutTypefaceRuns(layout); |
| |
| double word_start_position = std::numeric_limits<double>::quiet_NaN(); |
| |
| // Build a Skia text blob from each group of glyphs. |
| for (const Range<size_t>& glyph_blob : glyph_blobs) { |
| std::vector<GlyphPosition> glyph_positions; |
| |
| GetGlyphTypeface(layout, glyph_blob.start).apply(font); |
| const SkTextBlobBuilder::RunBuffer& blob_buffer = |
| builder.allocRunPos(font, glyph_blob.end - glyph_blob.start); |
| |
| double justify_x_offset_delta = 0; |
| |
| for (size_t glyph_index = glyph_blob.start; |
| glyph_index < glyph_blob.end;) { |
| size_t cluster_start_glyph_index = glyph_index; |
| uint32_t cluster = layout.getGlyphCluster(cluster_start_glyph_index); |
| double glyph_x_offset; |
| |
| // Add all the glyphs in this cluster to the text blob. |
| do { |
| size_t blob_index = glyph_index - glyph_blob.start; |
| blob_buffer.glyphs[blob_index] = layout.getGlyphId(glyph_index); |
| |
| size_t pos_index = blob_index * 2; |
| blob_buffer.pos[pos_index] = |
| layout.getX(glyph_index) + justify_x_offset_delta; |
| blob_buffer.pos[pos_index + 1] = layout.getY(glyph_index); |
| |
| if (glyph_index == cluster_start_glyph_index) |
| glyph_x_offset = blob_buffer.pos[pos_index]; |
| |
| glyph_index++; |
| } while (glyph_index < glyph_blob.end && |
| layout.getGlyphCluster(glyph_index) == cluster); |
| |
| Range<int32_t> glyph_code_units(cluster, 0); |
| std::vector<size_t> grapheme_code_unit_counts; |
| if (run.is_rtl()) { |
| if (cluster_start_glyph_index > 0) { |
| glyph_code_units.end = |
| layout.getGlyphCluster(cluster_start_glyph_index - 1); |
| } else { |
| glyph_code_units.end = text_count; |
| } |
| grapheme_code_unit_counts.push_back(glyph_code_units.width()); |
| } else { |
| if (glyph_index < layout.nGlyphs()) { |
| glyph_code_units.end = layout.getGlyphCluster(glyph_index); |
| } else { |
| glyph_code_units.end = text_count; |
| } |
| |
| // The glyph may be a ligature. Determine how many graphemes are |
| // joined into this glyph and how many input code units map to |
| // each grapheme. |
| size_t code_unit_count = 1; |
| for (int32_t offset = glyph_code_units.start + 1; |
| offset < glyph_code_units.end; ++offset) { |
| if (minikin::GraphemeBreak::isGraphemeBreak( |
| layout_advances.data(), text_ptr, text_start, text_count, |
| offset)) { |
| grapheme_code_unit_counts.push_back(code_unit_count); |
| code_unit_count = 1; |
| } else { |
| code_unit_count++; |
| } |
| } |
| grapheme_code_unit_counts.push_back(code_unit_count); |
| } |
| float glyph_advance = layout.getCharAdvance(glyph_code_units.start); |
| float grapheme_advance = |
| glyph_advance / grapheme_code_unit_counts.size(); |
| |
| glyph_positions.emplace_back(run_x_offset + glyph_x_offset, |
| grapheme_advance, |
| run.start() + glyph_code_units.start, |
| grapheme_code_unit_counts[0]); |
| |
| // Compute positions for the additional graphemes in the ligature. |
| for (size_t i = 1; i < grapheme_code_unit_counts.size(); ++i) { |
| glyph_positions.emplace_back( |
| glyph_positions.back().x_pos.end, grapheme_advance, |
| glyph_positions.back().code_units.start + |
| grapheme_code_unit_counts[i - 1], |
| grapheme_code_unit_counts[i]); |
| } |
| |
| bool at_word_start = false; |
| bool at_word_end = false; |
| if (word_index < words.size()) { |
| at_word_start = |
| words[word_index].start == run.start() + glyph_code_units.start; |
| at_word_end = |
| words[word_index].end == run.start() + glyph_code_units.end; |
| if (line_runs_all_rtl) { |
| std::swap(at_word_start, at_word_end); |
| } |
| } |
| |
| if (at_word_start) { |
| word_start_position = run_x_offset + glyph_x_offset; |
| } |
| |
| if (at_word_end) { |
| if (justify_line) { |
| justify_x_offset_delta += word_gap_width; |
| } |
| word_index++; |
| |
| if (!isnan(word_start_position)) { |
| double word_width = |
| 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(); |
| } |
| } |
| } // for each in glyph_blob |
| |
| if (glyph_positions.empty()) |
| continue; |
| SkFontMetrics metrics; |
| font.getMetrics(&metrics); |
| Range<double> record_x_pos( |
| glyph_positions.front().x_pos.start - run_x_offset, |
| glyph_positions.back().x_pos.end - run_x_offset); |
| paint_records.emplace_back( |
| run.style(), SkPoint::Make(run_x_offset + justify_x_offset, 0), |
| builder.make(), metrics, line_number, record_x_pos.start, |
| record_x_pos.end, run.is_ghost()); |
| justify_x_offset += justify_x_offset_delta; |
| |
| 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; |
| }); |
| |
| line_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, metrics, run.direction()); |
| |
| if (!run.is_ghost()) { |
| min_left_ = std::min(min_left_, glyph_positions.front().x_pos.start); |
| max_right_ = std::max(max_right_, glyph_positions.back().x_pos.end); |
| } |
| } // for each in glyph_blobs |
| // Do not increase x offset for LTR trailing ghost runs as it should not |
| // impact the layout of visible glyphs. RTL tailing ghost runs have the |
| // advance subtracted, so we do add the advance here to reset the |
| // run_x_offset. We do keep the record though so GetRectsForRange() can |
| // find metrics for trailing spaces. |
| if (!run.is_ghost() || run.is_rtl()) { |
| run_x_offset += layout.getAdvance(); |
| } |
| } // for each in line_runs |
| |
| // Adjust the glyph positions based on the alignment of the line. |
| double line_x_offset = GetLineXOffset(run_x_offset); |
| if (line_x_offset) { |
| for (CodeUnitRun& code_unit_run : line_code_unit_runs) { |
| code_unit_run.Shift(line_x_offset); |
| } |
| for (GlyphPosition& position : line_glyph_positions) { |
| position.Shift(line_x_offset); |
| } |
| } |
| |
| 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(line_glyph_positions), |
| next_line_start - line_range.start); |
| code_unit_runs_.insert(code_unit_runs_.end(), line_code_unit_runs.begin(), |
| line_code_unit_runs.end()); |
| |
| // Calculate the amount to advance in the y direction. This is done by |
| // computing the maximum ascent and descent with respect to the strut. |
| double max_ascent = strut.ascent + strut.half_leading; |
| double max_descent = strut.descent + strut.half_leading; |
| SkScalar max_unscaled_ascent = 0; |
| auto update_line_metrics = [&](const SkFontMetrics& metrics, |
| const TextStyle& style) { |
| if (!strut.force_strut) { |
| double ascent = |
| (-metrics.fAscent + metrics.fLeading / 2) * style.height; |
| max_ascent = std::max(ascent, max_ascent); |
| |
| double descent = |
| (metrics.fDescent + metrics.fLeading / 2) * style.height; |
| max_descent = std::max(descent, max_descent); |
| } |
| |
| max_unscaled_ascent = std::max(-metrics.fAscent, max_unscaled_ascent); |
| }; |
| for (const PaintRecord& paint_record : paint_records) { |
| update_line_metrics(paint_record.metrics(), paint_record.style()); |
| } |
| |
| // If no fonts were actually rendered, then compute a baseline based on the |
| // font of the paragraph style. |
| if (paint_records.empty()) { |
| SkFontMetrics metrics; |
| TextStyle style(paragraph_style_.GetTextStyle()); |
| font.setTypeface(GetDefaultSkiaTypeface(style)); |
| font.setSize(style.font_size); |
| font.getMetrics(&metrics); |
| update_line_metrics(metrics, style); |
| } |
| |
| // Calculate the baselines. This is only done on the first line. |
| if (line_number == 0) { |
| alphabetic_baseline_ = max_ascent; |
| // TODO(garyq): Ideographic baseline is currently bottom of EM |
| // box, which is not correct. This should be obtained from metrics. |
| // Skia currently does not support various baselines. |
| ideographic_baseline_ = (max_ascent + max_descent); |
| } |
| |
| line_heights_.push_back((line_heights_.empty() ? 0 : line_heights_.back()) + |
| round(max_ascent + max_descent)); |
| line_baselines_.push_back(line_heights_.back() - max_descent); |
| y_offset += round(max_ascent + prev_max_descent); |
| prev_max_descent = max_descent; |
| |
| // The max line spacing and ascent have been multiplied by -1 to make math |
| // in GetRectsForRange more logical/readable. |
| line_max_spacings_.push_back(max_ascent); |
| line_max_descent_.push_back(max_descent); |
| line_max_ascent_.push_back(max_unscaled_ascent); |
| |
| for (PaintRecord& paint_record : paint_records) { |
| paint_record.SetOffset( |
| SkPoint::Make(paint_record.offset().x() + line_x_offset, y_offset)); |
| records_.emplace_back(std::move(paint_record)); |
| } |
| } // for each line_number |
| |
| if (paragraph_style_.max_lines == 1 || |
| (paragraph_style_.unlimited_lines() && paragraph_style_.ellipsized())) { |
| min_intrinsic_width_ = max_intrinsic_width_; |
| } else { |
| 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; |
| }); |
| |
| tight_width_ = max_right_ - min_left_; |
| } |
| |
| double Paragraph::GetLineXOffset(double line_total_advance) { |
| if (isinf(width_)) |
| return 0; |
| |
| TextAlign align = paragraph_style_.effective_align(); |
| |
| if (align == TextAlign::right) { |
| return width_ - line_total_advance; |
| } else if (align == TextAlign::center) { |
| return (width_ - line_total_advance) / 2; |
| } else { |
| return 0; |
| } |
| } |
| |
| const ParagraphStyle& Paragraph::GetParagraphStyle() const { |
| return paragraph_style_; |
| } |
| |
| double Paragraph::GetAlphabeticBaseline() const { |
| // Currently -fAscent |
| return alphabetic_baseline_; |
| } |
| |
| double Paragraph::GetIdeographicBaseline() const { |
| // TODO(garyq): Currently -fAscent + fUnderlinePosition. Verify this. |
| return ideographic_baseline_; |
| } |
| |
| double Paragraph::GetMaxIntrinsicWidth() const { |
| return max_intrinsic_width_; |
| } |
| |
| double Paragraph::GetMinIntrinsicWidth() const { |
| return min_intrinsic_width_; |
| } |
| |
| size_t Paragraph::TextSize() const { |
| return text_.size(); |
| } |
| |
| double Paragraph::GetHeight() const { |
| return line_heights_.size() ? line_heights_.back() : 0; |
| } |
| |
| double Paragraph::GetMaxWidth() const { |
| return width_; |
| } |
| |
| double Paragraph::GetTightWidth() const { |
| return tight_width_; |
| } |
| |
| void Paragraph::SetParagraphStyle(const ParagraphStyle& style) { |
| needs_layout_ = true; |
| paragraph_style_ = style; |
| } |
| |
| void Paragraph::SetFontCollection( |
| std::shared_ptr<FontCollection> font_collection) { |
| font_collection_ = std::move(font_collection); |
| } |
| |
| std::shared_ptr<minikin::FontCollection> |
| Paragraph::GetMinikinFontCollectionForStyle(const TextStyle& style) { |
| std::string locale; |
| if (!style.locale.empty()) { |
| uint32_t language_list_id = |
| minikin::FontStyle::registerLanguageList(style.locale); |
| const minikin::FontLanguages& langs = |
| minikin::FontLanguageListCache::getById(language_list_id); |
| if (langs.size()) { |
| locale = langs[0].getString(); |
| } |
| } |
| |
| return font_collection_->GetMinikinFontCollectionForFamilies( |
| style.font_families, locale); |
| } |
| |
| sk_sp<SkTypeface> Paragraph::GetDefaultSkiaTypeface(const TextStyle& style) { |
| std::shared_ptr<minikin::FontCollection> collection = |
| GetMinikinFontCollectionForStyle(style); |
| if (!collection) { |
| return nullptr; |
| } |
| minikin::FakedFont faked_font = |
| collection->baseFontFaked(GetMinikinFontStyle(style)); |
| return static_cast<FontSkia*>(faked_font.font)->GetSkTypeface(); |
| } |
| |
| // The x,y coordinates will be the very top left corner of the rendered |
| // paragraph. |
| void Paragraph::Paint(SkCanvas* canvas, double x, double y) { |
| SkPoint base_offset = SkPoint::Make(x, y); |
| SkPaint paint; |
| // Paint the background first before painting any text to prevent |
| // potential overlap. |
| for (const PaintRecord& record : records_) { |
| PaintBackground(canvas, record, base_offset); |
| } |
| for (const PaintRecord& record : records_) { |
| if (record.style().has_foreground) { |
| paint = record.style().foreground; |
| } else { |
| paint.reset(); |
| paint.setColor(record.style().color); |
| } |
| SkPoint offset = base_offset + record.offset(); |
| PaintShadow(canvas, record, offset); |
| canvas->drawTextBlob(record.text(), offset.x(), offset.y(), paint); |
| PaintDecorations(canvas, record, base_offset); |
| } |
| } |
| |
| void Paragraph::PaintDecorations(SkCanvas* canvas, |
| const PaintRecord& record, |
| SkPoint base_offset) { |
| if (record.style().decoration == TextDecoration::kNone) |
| return; |
| |
| if (record.isGhost()) |
| return; |
| |
| const SkFontMetrics& metrics = record.metrics(); |
| SkPaint paint; |
| paint.setStyle(SkPaint::kStroke_Style); |
| if (record.style().decoration_color == SK_ColorTRANSPARENT) { |
| paint.setColor(record.style().color); |
| } else { |
| paint.setColor(record.style().decoration_color); |
| } |
| paint.setAntiAlias(true); |
| |
| // This is set to 2 for the double line style |
| int decoration_count = 1; |
| |
| // Filled when drawing wavy decorations. |
| SkPath path; |
| |
| double width = record.GetRunWidth(); |
| |
| SkScalar underline_thickness; |
| if ((metrics.fFlags & |
| SkFontMetrics::FontMetricsFlags::kUnderlineThicknessIsValid_Flag) && |
| metrics.fUnderlineThickness > 0) { |
| underline_thickness = metrics.fUnderlineThickness; |
| } else { |
| // Backup value if the fUnderlineThickness metric is not available: |
| // Divide by 14pt as it is the default size. |
| underline_thickness = record.style().font_size / 14.0f; |
| } |
| paint.setStrokeWidth(underline_thickness * |
| record.style().decoration_thickness_multiplier); |
| |
| SkPoint record_offset = base_offset + record.offset(); |
| SkScalar x = record_offset.x() + record.x_start(); |
| SkScalar y = record_offset.y(); |
| |
| // Setup the decorations. |
| switch (record.style().decoration_style) { |
| case TextDecorationStyle::kSolid: { |
| break; |
| } |
| case TextDecorationStyle::kDouble: { |
| decoration_count = 2; |
| break; |
| } |
| // Note: the intervals are scaled by the thickness of the line, so it is |
| // possible to change spacing by changing the decoration_thickness |
| // property of TextStyle. |
| case TextDecorationStyle::kDotted: { |
| // Divide by 14pt as it is the default size. |
| const float scale = record.style().font_size / 14.0f; |
| const SkScalar intervals[] = {1.0f * scale, 1.5f * scale, 1.0f * scale, |
| 1.5f * scale}; |
| size_t count = sizeof(intervals) / sizeof(intervals[0]); |
| paint.setPathEffect(SkPathEffect::MakeCompose( |
| SkDashPathEffect::Make(intervals, count, 0.0f), |
| SkDiscretePathEffect::Make(0, 0))); |
| break; |
| } |
| // Note: the intervals are scaled by the thickness of the line, so it is |
| // possible to change spacing by changing the decoration_thickness |
| // property of TextStyle. |
| case TextDecorationStyle::kDashed: { |
| // Divide by 14pt as it is the default size. |
| const float scale = record.style().font_size / 14.0f; |
| const SkScalar intervals[] = {4.0f * scale, 2.0f * scale, 4.0f * scale, |
| 2.0f * scale}; |
| size_t count = sizeof(intervals) / sizeof(intervals[0]); |
| paint.setPathEffect(SkPathEffect::MakeCompose( |
| SkDashPathEffect::Make(intervals, count, 0.0f), |
| SkDiscretePathEffect::Make(0, 0))); |
| break; |
| } |
| case TextDecorationStyle::kWavy: { |
| int wave_count = 0; |
| double x_start = 0; |
| double wavelength = |
| underline_thickness * record.style().decoration_thickness_multiplier; |
| path.moveTo(x, y); |
| while (x_start + wavelength * 2 < width) { |
| path.rQuadTo(wavelength, wave_count % 2 != 0 ? wavelength : -wavelength, |
| wavelength * 2, 0); |
| x_start += wavelength * 2; |
| ++wave_count; |
| } |
| break; |
| } |
| } |
| |
| // Draw the decorations. |
| // Use a for loop for "kDouble" decoration style |
| for (int i = 0; i < decoration_count; i++) { |
| double y_offset = i * underline_thickness * kDoubleDecorationSpacing; |
| double y_offset_original = y_offset; |
| // Underline |
| if (record.style().decoration & TextDecoration::kUnderline) { |
| y_offset += |
| (metrics.fFlags & |
| SkFontMetrics::FontMetricsFlags::kUnderlinePositionIsValid_Flag) |
| ? metrics.fUnderlinePosition |
| : underline_thickness; |
| if (record.style().decoration_style != TextDecorationStyle::kWavy) { |
| canvas->drawLine(x, y + y_offset, x + width, y + y_offset, paint); |
| } else { |
| SkPath offsetPath = path; |
| offsetPath.offset(0, y_offset); |
| canvas->drawPath(offsetPath, paint); |
| } |
| y_offset = y_offset_original; |
| } |
| // Overline |
| if (record.style().decoration & TextDecoration::kOverline) { |
| // We subtract fAscent here because for double overlines, we want the |
| // second line to be above, not below the first. |
| y_offset -= metrics.fAscent; |
| if (record.style().decoration_style != TextDecorationStyle::kWavy) { |
| canvas->drawLine(x, y - y_offset, x + width, y - y_offset, paint); |
| } else { |
| SkPath offsetPath = path; |
| offsetPath.offset(0, -y_offset); |
| canvas->drawPath(offsetPath, paint); |
| } |
| y_offset = y_offset_original; |
| } |
| // Strikethrough |
| if (record.style().decoration & TextDecoration::kLineThrough) { |
| if (metrics.fFlags & |
| SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag) |
| paint.setStrokeWidth(metrics.fStrikeoutThickness * |
| record.style().decoration_thickness_multiplier); |
| // Make sure the double line is "centered" vertically. |
| y_offset += (decoration_count - 1.0) * underline_thickness * |
| kDoubleDecorationSpacing / -2.0; |
| y_offset += |
| (metrics.fFlags & |
| SkFontMetrics::FontMetricsFlags::kStrikeoutThicknessIsValid_Flag) |
| ? metrics.fStrikeoutPosition |
| // Backup value if the strikeoutposition metric is not |
| // available: |
| : metrics.fXHeight / -2.0; |
| if (record.style().decoration_style != TextDecorationStyle::kWavy) { |
| canvas->drawLine(x, y + y_offset, x + width, y + y_offset, paint); |
| } else { |
| SkPath offsetPath = path; |
| offsetPath.offset(0, y_offset); |
| canvas->drawPath(offsetPath, paint); |
| } |
| y_offset = y_offset_original; |
| } |
| } |
| } |
| |
| void Paragraph::PaintBackground(SkCanvas* canvas, |
| const PaintRecord& record, |
| SkPoint base_offset) { |
| if (!record.style().has_background) |
| return; |
| |
| const SkFontMetrics& metrics = record.metrics(); |
| SkRect rect(SkRect::MakeLTRB(record.x_start(), metrics.fAscent, |
| record.x_end(), metrics.fDescent)); |
| rect.offset(base_offset + record.offset()); |
| canvas->drawRect(rect, record.style().background); |
| } |
| |
| void Paragraph::PaintShadow(SkCanvas* canvas, |
| const PaintRecord& record, |
| SkPoint offset) { |
| if (record.style().text_shadows.size() == 0) |
| return; |
| for (TextShadow text_shadow : record.style().text_shadows) { |
| if (!text_shadow.hasShadow()) { |
| continue; |
| } |
| |
| SkPaint paint; |
| paint.setColor(text_shadow.color); |
| if (text_shadow.blur_radius != 0.0) { |
| paint.setMaskFilter(SkMaskFilter::MakeBlur( |
| kNormal_SkBlurStyle, text_shadow.blur_radius, false)); |
| } |
| canvas->drawTextBlob(record.text(), offset.x() + text_shadow.offset.x(), |
| offset.y() + text_shadow.offset.y(), paint); |
| } |
| } |
| |
| std::vector<Paragraph::TextBox> Paragraph::GetRectsForRange( |
| size_t start, |
| size_t end, |
| RectHeightStyle rect_height_style, |
| RectWidthStyle rect_width_style) const { |
| // Struct that holds calculated metrics for each line. |
| struct LineBoxMetrics { |
| std::vector<Paragraph::TextBox> boxes; |
| // Per-line metrics for max and min coordinates for left and right boxes. |
| // These metrics cannot be calculated in layout generically because of |
| // selections that do not cover the whole line. |
| SkScalar max_right = FLT_MIN; |
| SkScalar min_left = FLT_MAX; |
| }; |
| |
| std::map<size_t, LineBoxMetrics> line_metrics; |
| // Text direction of the first line so we can extend the correct side for |
| // RectWidthStyle::kMax. |
| TextDirection first_line_dir = TextDirection::ltr; |
| |
| // Lines that are actually in the requested range. |
| size_t max_line = 0; |
| size_t min_line = INT_MAX; |
| size_t glyph_length = 0; |
| |
| // Generate initial boxes and calculate metrics. |
| for (const CodeUnitRun& run : code_unit_runs_) { |
| // Check to see if we are finished. |
| if (run.code_units.start >= end) |
| break; |
| if (run.code_units.end <= start) |
| continue; |
| |
| double baseline = line_baselines_[run.line_number]; |
| SkScalar top = baseline + run.font_metrics.fAscent; |
| SkScalar bottom = baseline + run.font_metrics.fDescent; |
| |
| max_line = std::max(run.line_number, max_line); |
| min_line = std::min(run.line_number, min_line); |
| |
| // Calculate left and right. |
| 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 { |
| 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)); |
| } else if (gp.code_units.end == end) { |
| // Calculate left and right when we are at |
| // the last position of a combining character. |
| glyph_length = (gp.code_units.end - gp.code_units.start) - 1; |
| if (gp.code_units.start == |
| std::max<size_t>(0, (start - glyph_length))) { |
| 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; |
| } |
| // Keep track of the min and max horizontal coordinates over all lines. Not |
| // needed for kTight. |
| if (rect_width_style == RectWidthStyle::kMax) { |
| line_metrics[run.line_number].max_right = |
| std::max(line_metrics[run.line_number].max_right, right); |
| line_metrics[run.line_number].min_left = |
| std::min(line_metrics[run.line_number].min_left, left); |
| if (min_line == run.line_number) { |
| first_line_dir = run.direction; |
| } |
| } |
| line_metrics[run.line_number].boxes.emplace_back( |
| SkRect::MakeLTRB(left, top, right, bottom), run.direction); |
| } |
| |
| // Add empty rectangles representing any newline characters within the |
| // range. |
| for (size_t line_number = 0; line_number < line_ranges_.size(); |
| ++line_number) { |
| const LineRange& line = line_ranges_[line_number]; |
| if (line.start >= end) |
| break; |
| if (line.end_including_newline <= start) |
| continue; |
| if (line_metrics.find(line_number) == line_metrics.end()) { |
| if (line.end != line.end_including_newline && line.end >= start && |
| line.end_including_newline <= end) { |
| SkScalar x = line_widths_[line_number]; |
| // Move empty box to center if center aligned and is an empty line. |
| if (x == 0 && !isinf(width_) && |
| paragraph_style_.effective_align() == TextAlign::center) { |
| x = width_ / 2; |
| } |
| SkScalar top = (line_number > 0) ? line_heights_[line_number - 1] : 0; |
| SkScalar bottom = line_heights_[line_number]; |
| line_metrics[line_number].boxes.emplace_back( |
| SkRect::MakeLTRB(x, top, x, bottom), TextDirection::ltr); |
| } |
| } |
| } |
| |
| // "Post-process" metrics and aggregate final rects to return. |
| std::vector<Paragraph::TextBox> boxes; |
| for (const auto& kv : line_metrics) { |
| // Handle rect_width_styles. We skip the last line because not everything is |
| // selected. |
| if (rect_width_style == RectWidthStyle::kMax && kv.first != max_line) { |
| if (line_metrics[kv.first].min_left > min_left_ && |
| (kv.first != min_line || first_line_dir == TextDirection::rtl)) { |
| line_metrics[kv.first].boxes.emplace_back( |
| SkRect::MakeLTRB( |
| min_left_, |
| line_baselines_[kv.first] - line_max_ascent_[kv.first], |
| line_metrics[kv.first].min_left, |
| line_baselines_[kv.first] + line_max_descent_[kv.first]), |
| TextDirection::rtl); |
| } |
| if (line_metrics[kv.first].max_right < max_right_ && |
| (kv.first != min_line || first_line_dir == TextDirection::ltr)) { |
| line_metrics[kv.first].boxes.emplace_back( |
| SkRect::MakeLTRB( |
| line_metrics[kv.first].max_right, |
| line_baselines_[kv.first] - line_max_ascent_[kv.first], |
| max_right_, |
| line_baselines_[kv.first] + line_max_descent_[kv.first]), |
| TextDirection::ltr); |
| } |
| } |
| |
| // Handle rect_height_styles. The height metrics used are all positive to |
| // make the signage clear here. |
| if (rect_height_style == RectHeightStyle::kTight) { |
| // Ignore line max height and width and generate tight bounds. |
| boxes.insert(boxes.end(), kv.second.boxes.begin(), kv.second.boxes.end()); |
| } else if (rect_height_style == RectHeightStyle::kMax) { |
| for (const Paragraph::TextBox& box : kv.second.boxes) { |
| boxes.emplace_back( |
| SkRect::MakeLTRB( |
| box.rect.fLeft, |
| line_baselines_[kv.first] - line_max_ascent_[kv.first], |
| box.rect.fRight, |
| line_baselines_[kv.first] + line_max_descent_[kv.first]), |
| box.direction); |
| } |
| } else if (rect_height_style == |
| RectHeightStyle::kIncludeLineSpacingMiddle) { |
| SkScalar adjusted_bottom = |
| line_baselines_[kv.first] + line_max_descent_[kv.first]; |
| if (kv.first < line_ranges_.size() - 1) { |
| adjusted_bottom += (line_max_spacings_[kv.first + 1] - |
| line_max_ascent_[kv.first + 1]) / |
| 2; |
| } |
| SkScalar adjusted_top = |
| line_baselines_[kv.first] - line_max_ascent_[kv.first]; |
| if (kv.first != 0) { |
| adjusted_top -= |
| (line_max_spacings_[kv.first] - line_max_ascent_[kv.first]) / 2; |
| } |
| for (const Paragraph::TextBox& box : kv.second.boxes) { |
| boxes.emplace_back(SkRect::MakeLTRB(box.rect.fLeft, adjusted_top, |
| box.rect.fRight, adjusted_bottom), |
| box.direction); |
| } |
| } else if (rect_height_style == RectHeightStyle::kIncludeLineSpacingTop) { |
| for (const Paragraph::TextBox& box : kv.second.boxes) { |
| SkScalar adjusted_top = |
| kv.first == 0 |
| ? line_baselines_[kv.first] - line_max_ascent_[kv.first] |
| : line_baselines_[kv.first] - line_max_spacings_[kv.first]; |
| boxes.emplace_back( |
| SkRect::MakeLTRB( |
| box.rect.fLeft, adjusted_top, box.rect.fRight, |
| line_baselines_[kv.first] + line_max_descent_[kv.first]), |
| box.direction); |
| } |
| } else { // kIncludeLineSpacingBottom |
| for (const Paragraph::TextBox& box : kv.second.boxes) { |
| SkScalar adjusted_bottom = |
| line_baselines_[kv.first] + line_max_descent_[kv.first]; |
| if (kv.first < line_ranges_.size() - 1) { |
| adjusted_bottom += |
| -line_max_ascent_[kv.first] + line_max_spacings_[kv.first]; |
| } |
| boxes.emplace_back(SkRect::MakeLTRB(box.rect.fLeft, |
| line_baselines_[kv.first] - |
| line_max_ascent_[kv.first], |
| box.rect.fRight, adjusted_bottom), |
| box.direction); |
| } |
| } |
| } |
| return boxes; |
| } |
| |
| Paragraph::PositionWithAffinity Paragraph::GetGlyphPositionAtCoordinate( |
| double dx, |
| double dy) const { |
| if (line_heights_.empty()) |
| return PositionWithAffinity(0, DOWNSTREAM); |
| |
| size_t y_index; |
| for (y_index = 0; y_index < line_heights_.size() - 1; ++y_index) { |
| if (dy < line_heights_[y_index]) |
| break; |
| } |
| |
| 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 + static_cast<int>(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].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); |
| } |
| |
| // 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); |
| } |
| } |
| |
| Paragraph::Range<size_t> Paragraph::GetWordBoundary(size_t offset) const { |
| if (text_.size() == 0) |
| return Range<size_t>(0, 0); |
| |
| if (!word_breaker_) { |
| UErrorCode status = U_ZERO_ERROR; |
| word_breaker_.reset( |
| icu::BreakIterator::createWordInstance(icu::Locale(), status)); |
| if (!U_SUCCESS(status)) |
| return Range<size_t>(0, 0); |
| } |
| |
| word_breaker_->setText(icu::UnicodeString(false, text_.data(), text_.size())); |
| |
| int32_t prev_boundary = word_breaker_->preceding(offset + 1); |
| int32_t next_boundary = word_breaker_->next(); |
| if (prev_boundary == icu::BreakIterator::DONE) |
| prev_boundary = offset; |
| if (next_boundary == icu::BreakIterator::DONE) |
| next_boundary = offset; |
| return Range<size_t>(prev_boundary, next_boundary); |
| } |
| |
| size_t Paragraph::GetLineCount() const { |
| return line_heights_.size(); |
| } |
| |
| bool Paragraph::DidExceedMaxLines() const { |
| return did_exceed_max_lines_; |
| } |
| |
| void Paragraph::SetDirty(bool dirty) { |
| needs_layout_ = dirty; |
| } |
| |
| } // namespace txt |