| /* |
| * 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_txt.h" |
| |
| #include <hb.h> |
| #include <minikin/Layout.h> |
| |
| #include <algorithm> |
| #include <cstring> |
| #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; |
| paint->fontFeatureSettings = style.font_features.GetFeatureSettings(); |
| } |
| |
| 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; |
| |
| ParagraphTxt::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 ParagraphTxt::GlyphPosition::Shift(double delta) { |
| x_pos.Shift(delta); |
| } |
| |
| ParagraphTxt::GlyphLine::GlyphLine(std::vector<GlyphPosition>&& p, size_t tcu) |
| : positions(std::move(p)), total_code_units(tcu) {} |
| |
| ParagraphTxt::CodeUnitRun::CodeUnitRun(std::vector<GlyphPosition>&& p, |
| Range<size_t> cu, |
| Range<double> x, |
| size_t line, |
| const SkFontMetrics& metrics, |
| const TextStyle& st, |
| TextDirection dir, |
| const PlaceholderRun* placeholder) |
| : positions(std::move(p)), |
| code_units(cu), |
| x_pos(x), |
| line_number(line), |
| font_metrics(metrics), |
| style(&st), |
| direction(dir), |
| placeholder_run(placeholder) {} |
| |
| void ParagraphTxt::CodeUnitRun::Shift(double delta) { |
| x_pos.Shift(delta); |
| for (GlyphPosition& position : positions) |
| position.Shift(delta); |
| } |
| |
| ParagraphTxt::ParagraphTxt() { |
| breaker_.setLocale(); |
| } |
| |
| ParagraphTxt::~ParagraphTxt() = default; |
| |
| void ParagraphTxt::SetText(std::vector<uint16_t> text, StyledRuns runs) { |
| SetDirty(true); |
| if (text.size() == 0) |
| return; |
| text_ = std::move(text); |
| runs_ = std::move(runs); |
| } |
| |
| void ParagraphTxt::SetInlinePlaceholders( |
| std::vector<PlaceholderRun> inline_placeholders, |
| std::unordered_set<size_t> obj_replacement_char_indexes) { |
| needs_layout_ = true; |
| inline_placeholders_ = std::move(inline_placeholders); |
| obj_replacement_char_indexes_ = std::move(obj_replacement_char_indexes); |
| } |
| |
| bool ParagraphTxt::ComputeLineBreaks() { |
| line_metrics_.clear(); |
| line_widths_.clear(); |
| max_intrinsic_width_ = 0; |
| |
| std::vector<size_t> newline_positions; |
| // Discover and add all hard breaks. |
| 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); |
| } |
| // Break at the end of the paragraph. |
| newline_positions.push_back(text_.size()); |
| |
| // Calculate and add any breaks due to a line being too long. |
| size_t run_index = 0; |
| size_t inline_placeholder_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_metrics_.emplace_back(block_start, block_end, block_end, |
| block_end + 1, true); |
| line_widths_.push_back(0); |
| continue; |
| } |
| |
| // Setup breaker. We wait to set the line width in order to account for the |
| // widths of the inline placeholders, which are calculated in the loop over |
| // the runs. |
| 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); |
| |
| // Check if the run is an object replacement character-only run. We should |
| // leave space for inline placeholder and break around it if appropriate. |
| if (run.end - run.start == 1 && |
| obj_replacement_char_indexes_.count(run.start) != 0 && |
| text_[run.start] == objReplacementChar && |
| inline_placeholder_index < inline_placeholders_.size()) { |
| // Is a inline placeholder run. |
| PlaceholderRun placeholder_run = |
| inline_placeholders_[inline_placeholder_index]; |
| block_total_width += placeholder_run.width; |
| |
| // Inject custom width into minikin breaker. (Uses LibTxt-minikin |
| // patch). |
| breaker_.setCustomCharWidth(run_start, placeholder_run.width); |
| |
| // Called with nullptr as paint in order to use the custom widths passed |
| // above. |
| breaker_.addStyleRun(nullptr, collection, font, run_start, run_end, |
| isRtl); |
| inline_placeholder_index++; |
| } else { |
| // Is a regular text run. |
| 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_metrics_.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 ParagraphTxt::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; |
| |
| // Detect if final trailing run is a single ambiguous whitespace. |
| // We want to bundle the final ambiguous whitespace with the preceding |
| // run in order to maintain logical typing behavior when mixing RTL and LTR |
| // text. We do not want this to be a true ghost run since the contrasting |
| // directionality causes the trailing space to not render at the visual end of |
| // the paragraph. |
| // |
| // This only applies to the final whitespace at the end as other whitespace is |
| // no longer ambiguous when surrounded by additional text. |
| |
| // TODO(garyq): Handle this in the text editor caret code instead at layout |
| // level. |
| bool has_trailing_whitespace = false; |
| int32_t bidi_run_start, bidi_run_length; |
| if (bidi_run_count > 1) { |
| ubidi_getVisualRun(bidi.get(), bidi_run_count - 1, &bidi_run_start, |
| &bidi_run_length); |
| if (!U_SUCCESS(status)) |
| return false; |
| if (bidi_run_length == 1) { |
| 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_WHITE_SPACE)) { |
| // Check if the trailing whitespace occurs before the previous run or |
| // not. If so, this trailing whitespace was a leading whitespace. |
| int32_t second_last_bidi_run_start, second_last_bidi_run_length; |
| ubidi_getVisualRun(bidi.get(), bidi_run_count - 2, |
| &second_last_bidi_run_start, |
| &second_last_bidi_run_length); |
| if (bidi_run_start == |
| second_last_bidi_run_start + second_last_bidi_run_length) { |
| has_trailing_whitespace = true; |
| bidi_run_count--; |
| } |
| } |
| } |
| } |
| |
| // 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) { |
| 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; |
| |
| // Attach the final trailing whitespace as part of this run. |
| if (has_trailing_whitespace && bidi_run_index == bidi_run_count - 1) { |
| bidi_run_length++; |
| } |
| |
| 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; |
| } |
| |
| bool ParagraphTxt::IsStrutValid() const { |
| // Font size must be positive. |
| return (paragraph_style_.strut_enabled && |
| paragraph_style_.strut_font_size >= 0); |
| } |
| |
| void ParagraphTxt::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; |
| |
| if (!IsStrutValid()) |
| 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; |
| 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); |
| |
| const double metrics_height = |
| -strut_metrics.fAscent + strut_metrics.fDescent; |
| |
| if (paragraph_style_.strut_has_height_override) { |
| const double strut_height = |
| paragraph_style_.strut_height * paragraph_style_.strut_font_size; |
| const double metrics_leading = |
| // Zero extra leading if there is no user specified strut leading. |
| paragraph_style_.strut_leading < 0 |
| ? 0 |
| : (paragraph_style_.strut_leading * |
| paragraph_style_.strut_font_size); |
| |
| const double available_height = |
| paragraph_style_.strut_half_leading ? metrics_height : strut_height; |
| |
| strut->ascent = |
| (-strut_metrics.fAscent / metrics_height) * available_height; |
| strut->descent = |
| (strut_metrics.fDescent / metrics_height) * available_height; |
| |
| strut->leading = metrics_leading + strut_height - available_height; |
| } else { |
| strut->ascent = -strut_metrics.fAscent; |
| strut->descent = 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 * |
| paragraph_style_.strut_font_size); |
| } |
| strut->half_leading = strut->leading / 2; |
| strut->line_height = strut->ascent + strut->descent + strut->leading; |
| } |
| } |
| |
| void ParagraphTxt::ComputePlaceholder(PlaceholderRun* placeholder_run, |
| double& ascent, |
| double& descent) { |
| if (placeholder_run != nullptr) { |
| // Calculate how much to shift the ascent and descent to account |
| // for the baseline choice. |
| // |
| // TODO(garyq): implement for various baselines. Currently only |
| // supports for alphabetic and ideographic |
| double baseline_adjustment = 0; |
| switch (placeholder_run->baseline) { |
| case TextBaseline::kAlphabetic: { |
| baseline_adjustment = 0; |
| break; |
| } |
| case TextBaseline::kIdeographic: { |
| baseline_adjustment = -descent / 2; |
| break; |
| } |
| } |
| // Convert the ascent and descent from the font's to the placeholder |
| // rect's. |
| switch (placeholder_run->alignment) { |
| case PlaceholderAlignment::kBaseline: { |
| ascent = baseline_adjustment + placeholder_run->baseline_offset; |
| descent = -baseline_adjustment + placeholder_run->height - |
| placeholder_run->baseline_offset; |
| break; |
| } |
| case PlaceholderAlignment::kAboveBaseline: { |
| ascent = baseline_adjustment + placeholder_run->height; |
| descent = -baseline_adjustment; |
| break; |
| } |
| case PlaceholderAlignment::kBelowBaseline: { |
| descent = baseline_adjustment + placeholder_run->height; |
| ascent = -baseline_adjustment; |
| break; |
| } |
| case PlaceholderAlignment::kTop: { |
| descent = placeholder_run->height - ascent; |
| break; |
| } |
| case PlaceholderAlignment::kBottom: { |
| ascent = placeholder_run->height - descent; |
| break; |
| } |
| case PlaceholderAlignment::kMiddle: { |
| double mid = (ascent - descent) / 2; |
| ascent = mid + placeholder_run->height / 2; |
| descent = -mid + placeholder_run->height / 2; |
| break; |
| } |
| } |
| placeholder_run->baseline_offset = ascent; |
| } |
| } |
| |
| // Implementation outline: |
| // |
| // -For each line: |
| // -Compute Bidi runs, convert into line_runs (keeps in-line-range runs, adds |
| // special runs) |
| // -For each line_run (runs in the line): |
| // -Calculate ellipsis |
| // -Obtain font |
| // -layout.doLayout(...), genereates glyph blobs |
| // -For each glyph blob: |
| // -Convert glyph blobs into pixel metrics/advances |
| // -Store as paint records (for painting) and code unit runs (for metrics |
| // and boxes). |
| // -Apply letter spacing, alignment, justification, etc |
| // -Calculate line vertical layout (ascent, descent, etc) |
| // -Store per-line metrics |
| void ParagraphTxt::Layout(double width) { |
| double rounded_width = floor(width); |
| // Do not allow calling layout multiple times without changing anything. |
| if (!needs_layout_ && rounded_width == width_) { |
| return; |
| } |
| |
| width_ = rounded_width; |
| |
| needs_layout_ = false; |
| |
| records_.clear(); |
| glyph_lines_.clear(); |
| code_unit_runs_.clear(); |
| inline_placeholder_code_unit_runs_.clear(); |
| max_right_ = std::numeric_limits<double>::lowest(); |
| min_left_ = std::numeric_limits<double>::max(); |
| final_line_count_ = 0; |
| |
| 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(SkFontHinting::kSlight); |
| |
| 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_. |
| ComputeStrut(&strut_, font); |
| |
| // Paragraph bounds tracking. |
| size_t line_limit = |
| std::min(paragraph_style_.max_lines, line_metrics_.size()); |
| did_exceed_max_lines_ = (line_metrics_.size() > paragraph_style_.max_lines); |
| |
| size_t placeholder_run_index = 0; |
| for (size_t line_number = 0; line_number < line_limit; ++line_number) { |
| LineMetrics& line_metrics = line_metrics_[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_metrics.hard_break); |
| FindWords(text_, line_metrics.start_index, line_metrics.end_index, &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_metrics.end_excluding_whitespace |
| : line_metrics.end_index; |
| |
| // 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_metrics.end_excluding_whitespace < line_metrics.end_index && |
| bidi_run.start() <= line_metrics.end_index && |
| 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_metrics.end_index), |
| 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_metrics.start_index) { |
| // The run is a placeholder run. |
| if (bidi_run.size() == 1 && |
| text_[bidi_run.start()] == objReplacementChar && |
| obj_replacement_char_indexes_.count(bidi_run.start()) != 0 && |
| placeholder_run_index < inline_placeholders_.size()) { |
| line_runs.emplace_back( |
| std::max(bidi_run.start(), line_metrics.start_index), |
| std::min(bidi_run.end(), line_end_index), bidi_run.direction(), |
| bidi_run.style(), inline_placeholders_[placeholder_run_index]); |
| placeholder_run_index++; |
| } else { |
| line_runs.emplace_back( |
| std::max(bidi_run.start(), line_metrics.start_index), |
| 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; |
| std::vector<CodeUnitRun> line_inline_placeholder_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()); |
| if (!minikin_font_collection) { |
| return; |
| } |
| |
| // 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_metrics.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 + |
| 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, |
| text_start + 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; |
| if (run.is_placeholder_run()) { |
| // The placeholder run's layout should yield one glyph representing |
| // the object replacement character. Replace its width with the |
| // placeholder's width. |
| FML_DCHECK(layout.nGlyphs() == 1); |
| glyph_advance = run.placeholder_run()->width; |
| } else { |
| 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; |
| |
| // Store the font metrics and TextStyle in the LineMetrics for this line |
| // to provide metrics upon user request. We index this RunMetrics |
| // instance at `run.end() - 1` to allow map::lower_bound to access the |
| // correct RunMetrics at any text index. |
| size_t run_key = run.end() - 1; |
| line_metrics.run_metrics.emplace(run_key, &run.style()); |
| SkFontMetrics* metrics = |
| &line_metrics.run_metrics.at(run_key).font_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, 0), |
| builder.make(), *metrics, line_number, |
| record_x_pos.start, record_x_pos.end, |
| run.is_ghost(), run.placeholder_run()); |
| |
| 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; |
| }); |
| |
| double blob_x_pos_start = glyph_positions.front().x_pos.start; |
| double blob_x_pos_end = glyph_positions.back().x_pos.end; |
| line_code_unit_runs.emplace_back( |
| std::move(code_unit_positions), |
| Range<size_t>(run.start(), run.end()), |
| Range<double>(blob_x_pos_start, blob_x_pos_end), line_number, |
| *metrics, run.style(), run.direction(), run.placeholder_run()); |
| |
| if (run.is_placeholder_run()) { |
| line_inline_placeholder_code_unit_runs.push_back( |
| line_code_unit_runs.back()); |
| } |
| |
| if (!run.is_ghost()) { |
| min_left_ = std::min(min_left_, blob_x_pos_start); |
| max_right_ = std::max(max_right_, blob_x_pos_end); |
| } |
| } // for each in glyph_blobs |
| |
| if (run.is_placeholder_run()) { |
| run_x_offset += run.placeholder_run()->width; |
| } else { |
| // 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, justify_line); |
| if (line_x_offset) { |
| for (CodeUnitRun& code_unit_run : line_code_unit_runs) { |
| code_unit_run.Shift(line_x_offset); |
| } |
| for (CodeUnitRun& code_unit_run : |
| line_inline_placeholder_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_metrics_.size() - 1) |
| ? line_metrics_[line_number + 1].start_index |
| : text_.size(); |
| glyph_lines_.emplace_back(std::move(line_glyph_positions), |
| next_line_start - line_metrics.start_index); |
| code_unit_runs_.insert(code_unit_runs_.end(), line_code_unit_runs.begin(), |
| line_code_unit_runs.end()); |
| inline_placeholder_code_unit_runs_.insert( |
| inline_placeholder_code_unit_runs_.end(), |
| line_inline_placeholder_code_unit_runs.begin(), |
| line_inline_placeholder_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 = IsStrutValid() ? strut_.ascent + strut_.half_leading |
| : std::numeric_limits<double>::lowest(); |
| double max_descent = IsStrutValid() ? strut_.descent + strut_.half_leading |
| : std::numeric_limits<double>::lowest(); |
| double max_unscaled_ascent = 0; |
| for (const PaintRecord& paint_record : paint_records) { |
| UpdateLineMetrics(paint_record.metrics(), paint_record.style(), |
| max_ascent, max_descent, max_unscaled_ascent, |
| paint_record.GetPlaceholderRun(), line_number, |
| line_limit); |
| } |
| |
| // 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); |
| UpdateLineMetrics(metrics, style, max_ascent, max_descent, |
| max_unscaled_ascent, nullptr, line_number, line_limit); |
| } |
| |
| // 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_metrics.height = |
| (line_number == 0 ? 0 : line_metrics_[line_number - 1].height) + |
| round(max_ascent + max_descent); |
| line_metrics.baseline = line_metrics.height - max_descent; |
| |
| y_offset += round(max_ascent + prev_max_descent); |
| prev_max_descent = max_descent; |
| |
| line_metrics.line_number = line_number; |
| line_metrics.ascent = max_ascent; |
| line_metrics.descent = max_descent; |
| line_metrics.unscaled_ascent = max_unscaled_ascent; |
| line_metrics.width = line_widths_[line_number]; |
| line_metrics.left = line_x_offset; |
| |
| final_line_count_++; |
| |
| 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; |
| }); |
| |
| longest_line_ = max_right_ - min_left_; |
| } |
| |
| void ParagraphTxt::UpdateLineMetrics(const SkFontMetrics& metrics, |
| const TextStyle& style, |
| double& max_ascent, |
| double& max_descent, |
| double& max_unscaled_ascent, |
| PlaceholderRun* placeholder_run, |
| size_t line_number, |
| size_t line_limit) { |
| if (!strut_.force_strut) { |
| const double metrics_font_height = metrics.fDescent - metrics.fAscent; |
| // The overall height of the glyph blob. If neither the ascent or the |
| // descent is disabled, we have block_height = ascent + descent, where |
| // "ascent" is the extent from the top of the blob to its baseline, and |
| // "descent" is the extent from the text blob's baseline to its bottom. Not |
| // to be mistaken with the font's ascent and descent. |
| const double blob_height = style.has_height_override |
| ? style.height * style.font_size |
| : metrics_font_height + metrics.fLeading; |
| |
| // Scale the ascent and descent such that the sum of ascent and |
| // descent is `style.height * style.font_size`. |
| // |
| // The raw metrics do not add up to fontSize. The state of font |
| // metrics is a mess: |
| // |
| // Each font has 4 sets of vertical metrics: |
| // |
| // * hhea: hheaAscender, hheaDescender, hheaLineGap. |
| // Used by Apple. |
| // * OS/2 typo: typoAscender, typoDescender, typoLineGap. |
| // Used sometimes by Windows for layout. |
| // * OS/2 win: winAscent, winDescent. |
| // Also used by Windows, generally will be cut if extends past |
| // these metrics. |
| // * EM Square: ascent, descent |
| // Not actively used, but this defines the 'scale' of the |
| // units used. |
| // |
| // `Use Typo Metrics` is a boolean that, when enabled, prefers |
| // typo metrics over win metrics. Default is off. Enabled by most |
| // modern fonts. |
| // |
| // In addition to these different sets of metrics, there are also |
| // multiple strategies for using these metrics: |
| // |
| // * Adobe: Set hhea values to typo equivalents. |
| // * Microsoft: Set hhea values to win equivalents. |
| // * Web: Use hhea values for text, regardless of `Use Typo Metrics` |
| // The hheaLineGap is distributed half across the top and half |
| // across the bottom of the line. |
| // Exceptions: |
| // Windows: All browsers respect `Use Typo Metrics` |
| // Firefox respects `Use Typo Metrics`. |
| // |
| // This pertains to this code in that it is ambiguous which set of |
| // metrics we are actually using via SkFontMetrics. This in turn |
| // means that if we use the raw metrics, we will see differences |
| // between platforms as well as unpredictable line heights. |
| // |
| // A more thorough explanation is available at |
| // https://glyphsapp.com/tutorials/vertical-metrics |
| // |
| // Doing this ascent/descent normalization to the EM Square allows |
| // a sane, consistent, and reasonable "blob_height" to be specified, |
| // though it breaks with what is done by any of the platforms above. |
| const bool shouldNormalizeFont = |
| style.has_height_override && !style.half_leading; |
| const double font_height = |
| shouldNormalizeFont ? style.font_size : metrics_font_height; |
| |
| // Reserve the outermost vertical space we want to distribute evenly over |
| // and under the text ("half-leading"). |
| double leading; |
| if (style.half_leading) { |
| leading = blob_height - font_height; |
| } else { |
| leading = style.has_height_override ? 0.0 : metrics.fLeading; |
| } |
| const double half_leading = leading / 2; |
| |
| // Proportionally distribute the remaining vertical space above and below |
| // the glyph blob's baseline, per the font's ascent/discent ratio. |
| const double available_vspace = blob_height - leading; |
| const double modifiedAscent = |
| -metrics.fAscent / metrics_font_height * available_vspace + |
| half_leading; |
| const double modifiedDescent = |
| metrics.fDescent / metrics_font_height * available_vspace + |
| half_leading; |
| |
| const bool disableAscent = |
| line_number == 0 && paragraph_style_.text_height_behavior & |
| TextHeightBehavior::kDisableFirstAscent; |
| const bool disableDescent = line_number == line_limit - 1 && |
| paragraph_style_.text_height_behavior & |
| TextHeightBehavior::kDisableLastDescent; |
| |
| double ascent = disableAscent ? -metrics.fAscent : modifiedAscent; |
| double descent = disableDescent ? metrics.fDescent : modifiedDescent; |
| |
| ComputePlaceholder(placeholder_run, ascent, descent); |
| |
| max_ascent = std::max(ascent, max_ascent); |
| max_descent = std::max(descent, max_descent); |
| } |
| |
| max_unscaled_ascent = |
| std::max(placeholder_run == nullptr ? -metrics.fAscent |
| : placeholder_run->baseline_offset, |
| max_unscaled_ascent); |
| }; |
| |
| double ParagraphTxt::GetLineXOffset(double line_total_advance, |
| bool justify_line) { |
| if (isinf(width_)) |
| return 0; |
| |
| TextAlign align = paragraph_style_.effective_align(); |
| |
| if (align == TextAlign::right || |
| (align == TextAlign::justify && |
| paragraph_style_.text_direction == TextDirection::rtl && |
| !justify_line)) { |
| return width_ - line_total_advance; |
| } else if (align == TextAlign::center) { |
| return (width_ - line_total_advance) / 2; |
| } else { |
| return 0; |
| } |
| } |
| |
| const ParagraphStyle& ParagraphTxt::GetParagraphStyle() const { |
| return paragraph_style_; |
| } |
| |
| double ParagraphTxt::GetAlphabeticBaseline() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| // Currently -fAscent |
| return alphabetic_baseline_; |
| } |
| |
| double ParagraphTxt::GetIdeographicBaseline() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| // TODO(garyq): Currently -fAscent + fUnderlinePosition. Verify this. |
| return ideographic_baseline_; |
| } |
| |
| double ParagraphTxt::GetMaxIntrinsicWidth() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return max_intrinsic_width_; |
| } |
| |
| double ParagraphTxt::GetMinIntrinsicWidth() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return min_intrinsic_width_; |
| } |
| |
| size_t ParagraphTxt::TextSize() const { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return text_.size(); |
| } |
| |
| double ParagraphTxt::GetHeight() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return final_line_count_ == 0 ? 0 |
| : line_metrics_[final_line_count_ - 1].height; |
| } |
| |
| double ParagraphTxt::GetMaxWidth() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return width_; |
| } |
| |
| double ParagraphTxt::GetLongestLine() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return longest_line_; |
| } |
| |
| void ParagraphTxt::SetParagraphStyle(const ParagraphStyle& style) { |
| needs_layout_ = true; |
| paragraph_style_ = style; |
| } |
| |
| void ParagraphTxt::SetFontCollection( |
| std::shared_ptr<FontCollection> font_collection) { |
| font_collection_ = std::move(font_collection); |
| } |
| |
| std::shared_ptr<minikin::FontCollection> |
| ParagraphTxt::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> ParagraphTxt::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 ParagraphTxt::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(); |
| if (record.GetPlaceholderRun() == nullptr) { |
| PaintShadow(canvas, record, offset); |
| canvas->drawTextBlob(record.text(), offset.x(), offset.y(), paint); |
| } |
| PaintDecorations(canvas, record, base_offset); |
| } |
| } |
| |
| void ParagraphTxt::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: { |
| ComputeWavyDecoration( |
| path, x, y, width, |
| underline_thickness * record.style().decoration_thickness_multiplier); |
| 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::kStrikeoutPositionIsValid_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 ParagraphTxt::ComputeWavyDecoration(SkPath& path, |
| double x, |
| double y, |
| double width, |
| double thickness) { |
| int wave_count = 0; |
| double x_start = 0; |
| // One full wavelength is 4 * thickness. |
| double quarter = thickness; |
| path.moveTo(x, y); |
| double remaining = width; |
| while (x_start + (quarter * 2) < width) { |
| path.rQuadTo(quarter, wave_count % 2 == 0 ? -quarter : quarter, quarter * 2, |
| 0); |
| x_start += quarter * 2; |
| remaining = width - x_start; |
| ++wave_count; |
| } |
| // Manually add a final partial quad for the remaining width that do |
| // not fit nicely into a half-wavelength. |
| // The following math is based off of quadratic bezier equations: |
| // |
| // * Let P(x) be the equation for the curve. |
| // * Let P0 = start, P1 = control point, P2 = end |
| // * P(x) = -2x^2 - 2x |
| // * P0 = (0, 0) |
| // * P1 = 2P(0.5) - 0.5 * P0 - 0.5 * P2 |
| // * P2 = P(remaining / (wavelength / 2)) |
| // |
| // Simplified implementation coursesy of @jim-flar at |
| // https://github.com/flutter/engine/pull/9468#discussion_r297872739 |
| // Unsimplified original version at |
| // https://github.com/flutter/engine/pull/9468#discussion_r297879129 |
| |
| double x1 = remaining / 2; |
| double y1 = remaining / 2 * (wave_count % 2 == 0 ? -1 : 1); |
| double x2 = remaining; |
| double y2 = (remaining - remaining * remaining / (quarter * 2)) * |
| (wave_count % 2 == 0 ? -1 : 1); |
| path.rQuadTo(x1, y1, x2, y2); |
| } |
| |
| void ParagraphTxt::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 ParagraphTxt::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_sigma > 0.5) { |
| paint.setMaskFilter(SkMaskFilter::MakeBlur( |
| kNormal_SkBlurStyle, text_shadow.blur_sigma, false)); |
| } |
| canvas->drawTextBlob(record.text(), offset.x() + text_shadow.offset.x(), |
| offset.y() + text_shadow.offset.y(), paint); |
| } |
| } |
| |
| std::vector<Paragraph::TextBox> ParagraphTxt::GetRectsForRange( |
| size_t start, |
| size_t end, |
| RectHeightStyle rect_height_style, |
| RectWidthStyle rect_width_style) { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| // 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 = std::numeric_limits<SkScalar>::lowest(); |
| SkScalar min_left = std::numeric_limits<SkScalar>::max(); |
| }; |
| |
| std::map<size_t, LineBoxMetrics> line_box_metrics; |
| // Text direction of the first line so we can extend the correct side for |
| // RectWidthStyle::kMax. |
| TextDirection first_line_dir = TextDirection::ltr; |
| std::map<size_t, size_t> newline_x_positions; |
| |
| // 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; |
| |
| // Update new line x position with the ending of last bidi run on the line |
| newline_x_positions[run.line_number] = |
| run.direction == TextDirection::ltr ? run.x_pos.end : run.x_pos.start; |
| |
| if (run.code_units.end <= start) |
| continue; |
| |
| double baseline = line_metrics_[run.line_number].baseline; |
| SkScalar top = baseline + run.font_metrics.fAscent; |
| SkScalar bottom = baseline + run.font_metrics.fDescent; |
| |
| if (run.placeholder_run != |
| nullptr) { // Use inline placeholder size as height. |
| top = baseline - run.placeholder_run->baseline_offset; |
| bottom = baseline + run.placeholder_run->height - |
| run.placeholder_run->baseline_offset; |
| } |
| |
| 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_box_metrics[run.line_number].max_right = |
| std::max(line_box_metrics[run.line_number].max_right, right); |
| line_box_metrics[run.line_number].min_left = |
| std::min(line_box_metrics[run.line_number].min_left, left); |
| if (min_line == run.line_number) { |
| first_line_dir = run.direction; |
| } |
| } |
| line_box_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_metrics_.size(); |
| ++line_number) { |
| LineMetrics& line = line_metrics_[line_number]; |
| if (line.start_index >= end) |
| break; |
| if (line.end_including_newline <= start) |
| continue; |
| if (line_box_metrics.find(line_number) == line_box_metrics.end()) { |
| if (line.end_index != line.end_including_newline && |
| line.end_index >= start && line.end_including_newline <= end) { |
| SkScalar x; |
| auto it = newline_x_positions.find(line_number); |
| if (it != newline_x_positions.end()) { |
| x = it->second; |
| } else { |
| x = GetLineXOffset(0, false); |
| } |
| SkScalar top = |
| (line_number > 0) ? line_metrics_[line_number - 1].height : 0; |
| SkScalar bottom = line_metrics_[line_number].height; |
| line_box_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_box_metrics) { |
| // Handle rect_width_styles. We skip the last line because not everything is |
| // selected. |
| |
| LineMetrics& line = |
| line_metrics_[fmin(line_metrics_.size() - 1, fmax(0, kv.first))]; |
| if (rect_width_style == RectWidthStyle::kMax && kv.first != max_line) { |
| if (line_box_metrics[kv.first].min_left > min_left_ && |
| (kv.first != min_line || first_line_dir == TextDirection::rtl)) { |
| line_box_metrics[kv.first].boxes.emplace_back( |
| SkRect::MakeLTRB(min_left_, line.baseline - line.unscaled_ascent, |
| line_box_metrics[kv.first].min_left, |
| line.baseline + line.descent), |
| TextDirection::rtl); |
| } |
| if (line_box_metrics[kv.first].max_right < max_right_ && |
| (kv.first != min_line || first_line_dir == TextDirection::ltr)) { |
| line_box_metrics[kv.first].boxes.emplace_back( |
| SkRect::MakeLTRB(line_box_metrics[kv.first].max_right, |
| line.baseline - line.unscaled_ascent, max_right_, |
| line.baseline + line.descent), |
| 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.baseline - line.ascent, |
| box.rect.fRight, line.baseline + line.descent), |
| box.direction); |
| } |
| } else if (rect_height_style == |
| RectHeightStyle::kIncludeLineSpacingMiddle) { |
| SkScalar adjusted_bottom = line.baseline + line.descent; |
| if (kv.first < line_metrics_.size() - 1) { |
| adjusted_bottom += (line_metrics_[kv.first + 1].ascent - |
| line_metrics_[kv.first + 1].unscaled_ascent) / |
| 2; |
| } |
| SkScalar adjusted_top = line.baseline - line.unscaled_ascent; |
| if (kv.first != 0) { |
| adjusted_top -= (line.ascent - line.unscaled_ascent) / 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.baseline - line.unscaled_ascent |
| : line.baseline - line.ascent; |
| boxes.emplace_back( |
| SkRect::MakeLTRB(box.rect.fLeft, adjusted_top, box.rect.fRight, |
| line.baseline + line.descent), |
| box.direction); |
| } |
| } else if (rect_height_style == |
| RectHeightStyle::kIncludeLineSpacingBottom) { |
| for (const Paragraph::TextBox& box : kv.second.boxes) { |
| SkScalar adjusted_bottom = line.baseline + line.descent; |
| if (kv.first < line_metrics_.size() - 1) { |
| adjusted_bottom += -line.unscaled_ascent + line.ascent; |
| } |
| boxes.emplace_back( |
| SkRect::MakeLTRB(box.rect.fLeft, |
| line.baseline - line.unscaled_ascent, |
| box.rect.fRight, adjusted_bottom), |
| box.direction); |
| } |
| } else if (rect_height_style == RectHeightStyle::kStrut) { |
| if (IsStrutValid()) { |
| for (const Paragraph::TextBox& box : kv.second.boxes) { |
| boxes.emplace_back( |
| SkRect::MakeLTRB(box.rect.fLeft, line.baseline - strut_.ascent, |
| box.rect.fRight, line.baseline + strut_.descent), |
| box.direction); |
| } |
| } else { |
| // Fall back to tight bounds if the strut is invalid. |
| boxes.insert(boxes.end(), kv.second.boxes.begin(), |
| kv.second.boxes.end()); |
| } |
| } |
| } |
| return boxes; |
| } |
| |
| Paragraph::PositionWithAffinity ParagraphTxt::GetGlyphPositionAtCoordinate( |
| double dx, |
| double dy) { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| if (final_line_count_ <= 0) |
| return PositionWithAffinity(0, DOWNSTREAM); |
| |
| size_t y_index; |
| for (y_index = 0; y_index < final_line_count_ - 1; ++y_index) { |
| if (dy < line_metrics_[y_index].height) |
| 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) { |
| gp = &line_glyph_position.back(); |
| } |
| |
| // 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); |
| } |
| } |
| |
| // We don't cache this because since this returns all boxes, it is usually |
| // unnecessary to call this multiple times in succession. |
| std::vector<Paragraph::TextBox> ParagraphTxt::GetRectsForPlaceholders() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| // 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 = std::numeric_limits<SkScalar>::lowest(); |
| SkScalar min_left = std::numeric_limits<SkScalar>::max(); |
| }; |
| |
| std::vector<Paragraph::TextBox> boxes; |
| |
| // Generate initial boxes and calculate metrics. |
| for (const CodeUnitRun& run : inline_placeholder_code_unit_runs_) { |
| // Check to see if we are finished. |
| double baseline = line_metrics_[run.line_number].baseline; |
| SkScalar top = baseline + run.font_metrics.fAscent; |
| SkScalar bottom = baseline + run.font_metrics.fDescent; |
| |
| if (run.placeholder_run != |
| nullptr) { // Use inline placeholder size as height. |
| top = baseline - run.placeholder_run->baseline_offset; |
| bottom = baseline + run.placeholder_run->height - |
| run.placeholder_run->baseline_offset; |
| } |
| |
| // Calculate left and right. |
| SkScalar left, right; |
| left = run.x_pos.start; |
| right = run.x_pos.end; |
| |
| boxes.emplace_back(SkRect::MakeLTRB(left, top, right, bottom), |
| run.direction); |
| } |
| return boxes; |
| } |
| |
| Paragraph::Range<size_t> ParagraphTxt::GetWordBoundary(size_t offset) { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| 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); |
| } |
| |
| icu::UnicodeString icu_text(false, text_.data(), text_.size()); |
| word_breaker_->setText(icu_text); |
| |
| 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 ParagraphTxt::GetLineCount() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return final_line_count_; |
| } |
| |
| bool ParagraphTxt::DidExceedMaxLines() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return did_exceed_max_lines_; |
| } |
| |
| void ParagraphTxt::SetDirty(bool dirty) { |
| needs_layout_ = dirty; |
| } |
| |
| std::vector<LineMetrics>& ParagraphTxt::GetLineMetrics() { |
| FML_DCHECK(!needs_layout_) << "only valid after layout"; |
| return line_metrics_; |
| } |
| |
| } // namespace txt |