diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 62ab580b50e..a11a605e97e 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -1059,6 +1059,8 @@ FILE: ../../../flutter/third_party/txt/src/txt/paragraph_builder.cc FILE: ../../../flutter/third_party/txt/src/txt/paragraph_builder.h FILE: ../../../flutter/third_party/txt/src/txt/paragraph_style.cc FILE: ../../../flutter/third_party/txt/src/txt/paragraph_style.h +FILE: ../../../flutter/third_party/txt/src/txt/placeholder_run.cc +FILE: ../../../flutter/third_party/txt/src/txt/placeholder_run.h FILE: ../../../flutter/third_party/txt/src/txt/styled_runs.cc FILE: ../../../flutter/third_party/txt/src/txt/styled_runs.h FILE: ../../../flutter/third_party/txt/src/txt/test_font_manager.cc diff --git a/engine/src/flutter/lib/ui/text.dart b/engine/src/flutter/lib/ui/text.dart index 74d82bae347..bfdfd898e9c 100644 --- a/engine/src/flutter/lib/ui/text.dart +++ b/engine/src/flutter/lib/ui/text.dart @@ -1381,72 +1381,116 @@ class ParagraphConstraints { /// Defines various ways to vertically bound the boxes returned by /// [Paragraph.getBoxesForRange]. enum BoxHeightStyle { - /// Provide tight bounding boxes that fit heights per run. This style may result - /// in uneven bounding boxes that do not nicely connect with adjacent boxes. - tight, + /// Provide tight bounding boxes that fit heights per run. This style may result + /// in uneven bounding boxes that do not nicely connect with adjacent boxes. + tight, - /// The height of the boxes will be the maximum height of all runs in the - /// line. All boxes in the same line will be the same height. This does not - /// guarantee that the boxes will cover the entire vertical height of the line - /// when there is additional line spacing. - /// - /// See [RectHeightStyle.includeLineSpacingTop], [RectHeightStyle.includeLineSpacingMiddle], - /// and [RectHeightStyle.includeLineSpacingBottom] for styles that will cover - /// the entire line. - max, + /// The height of the boxes will be the maximum height of all runs in the + /// line. All boxes in the same line will be the same height. + /// + /// This does not guarantee that the boxes will cover the entire vertical height of the line + /// when there is additional line spacing. + /// + /// See [RectHeightStyle.includeLineSpacingTop], [RectHeightStyle.includeLineSpacingMiddle], + /// and [RectHeightStyle.includeLineSpacingBottom] for styles that will cover + /// the entire line. + max, - /// Extends the top and bottom edge of the bounds to fully cover any line - /// spacing. - /// - /// The top and bottom of each box will cover half of the - /// space above and half of the space below the line. - /// - /// {@template flutter.dart:ui.boxHeightStyle.includeLineSpacing} - /// The top edge of each line should be the same as the bottom edge - /// of the line above. There should be no gaps in vertical coverage given any - /// amount of line spacing. Line spacing is not included above the first line - /// and below the last line due to no additional space present there. - /// {@endtemplate} - includeLineSpacingMiddle, + /// Extends the top and bottom edge of the bounds to fully cover any line + /// spacing. + /// + /// The top and bottom of each box will cover half of the + /// space above and half of the space below the line. + /// + /// {@template flutter.dart:ui.boxHeightStyle.includeLineSpacing} + /// The top edge of each line should be the same as the bottom edge + /// of the line above. There should be no gaps in vertical coverage given any + /// amount of line spacing. Line spacing is not included above the first line + /// and below the last line due to no additional space present there. + /// {@endtemplate} + includeLineSpacingMiddle, - /// Extends the top edge of the bounds to fully cover any line spacing. - /// - /// The line spacing will be added to the top of the box. - /// - /// {@macro flutter.dart:ui.rectHeightStyle.includeLineSpacing} - includeLineSpacingTop, + /// Extends the top edge of the bounds to fully cover any line spacing. + /// + /// The line spacing will be added to the top of the box. + /// + /// {@macro flutter.dart:ui.rectHeightStyle.includeLineSpacing} + includeLineSpacingTop, - /// Extends the bottom edge of the bounds to fully cover any line spacing. - /// - /// The line spacing will be added to the bottom of the box. - /// - /// {@macro flutter.dart:ui.boxHeightStyle.includeLineSpacing} - includeLineSpacingBottom, + /// Extends the bottom edge of the bounds to fully cover any line spacing. + /// + /// The line spacing will be added to the bottom of the box. + /// + /// {@macro flutter.dart:ui.boxHeightStyle.includeLineSpacing} + includeLineSpacingBottom, - /// Calculate box heights based on the metrics of this paragraph's [StrutStyle]. - /// - /// Boxes based on the strut will have consistent heights throughout the - /// entire paragraph. The top edge of each line will align with the bottom - /// edge of the previous line. It is possible for glyphs to extend outside - /// these boxes. - /// - /// Will fall back to tight bounds if the strut is disabled or invalid. - strut, + /// Calculate box heights based on the metrics of this paragraph's [StrutStyle]. + /// + /// Boxes based on the strut will have consistent heights throughout the + /// entire paragraph. The top edge of each line will align with the bottom + /// edge of the previous line. It is possible for glyphs to extend outside + /// these boxes. + strut, } /// Defines various ways to horizontally bound the boxes returned by /// [Paragraph.getBoxesForRange]. enum BoxWidthStyle { - // Provide tight bounding boxes that fit widths to the runs of each line - // independently. - tight, + // Provide tight bounding boxes that fit widths to the runs of each line + // independently. + tight, - /// Adds up to two additional boxes as needed at the beginning and/or end - /// of each line so that the widths of the boxes in line are the same width - /// as the widest line in the paragraph. The additional boxes on each line - /// are only added when the relevant box at the relevant edge of that line - /// does not span the maximum width of the paragraph. - max, + /// Adds up to two additional boxes as needed at the beginning and/or end + /// of each line so that the widths of the boxes in line are the same width + /// as the widest line in the paragraph. + /// + /// The additional boxes on each line are only added when the relevant box + /// at the relevant edge of that line does not span the maximum width of + /// the paragraph. + max, +} + +/// Where to vertically align the placeholder relative to the surrounding text. +/// +/// Used by [ParagraphBuilder.addPlaceholder]. +enum PlaceholderAlignment { + /// Match the baseline of the placeholder with the baseline. + /// + /// The [TextBaseline] to use must be specified and non-null when using this + /// alignment mode. + baseline, + + /// Align the bottom edge of the placeholder with the baseline such that the + /// placeholder sits on top of the baseline. + /// + /// The [TextBaseline] to use must be specified and non-null when using this + /// alignment mode. + aboveBaseline, + + /// Align the top edge of the placeholder with the baseline specified + /// such that the placeholder hangs below the baseline. + /// + /// The [TextBaseline] to use must be specified and non-null when using this + /// alignment mode. + belowBaseline, + + /// Align the top edge of the placeholder with the top edge of the font. + /// + /// When the placeholder is very tall, the extra space will hang from + /// the top and extend through the bottom of the line. + top, + + /// Align the bottom edge of the placeholder with the top edge of the font. + /// + /// When the placeholder is very tall, the extra space will rise from the + /// bottom and extend through the top of the line. + bottom, + + /// Align the middle of the placeholder with the middle of the text. + /// + /// When the placeholder is very tall, the extra space will grow equally + /// from the top and bottom of the line. + middle, } /// A paragraph of text. @@ -1525,6 +1569,9 @@ class Paragraph extends NativeFieldWrapperClass2 { /// parameters default to the tight option, which will provide close-fitting /// boxes and will not account for any line spacing. /// + /// Coordinates of the TextBox are relative to the upper-left corner of the paragraph, + /// where positive y values indicate down. + /// /// The [boxHeightStyle] and [boxWidthStyle] parameters must not be null. /// /// See [BoxHeightStyle] and [BoxWidthStyle] for full descriptions of each option. @@ -1533,9 +1580,16 @@ class Paragraph extends NativeFieldWrapperClass2 { assert(boxWidthStyle != null); return _getBoxesForRange(start, end, boxHeightStyle.index, boxWidthStyle.index); } - List _getBoxesForRange(int start, int end, int boxHeightStyle, int boxWidthStyle) native 'Paragraph_getRectsForRange'; + /// Returns a list of text boxes that enclose all placeholders in the paragraph. + /// + /// The order of the boxes are in the same order as passed in through [addPlaceholder]. + /// + /// Coordinates of the [TextBox] are relative to the upper-left corner of the paragraph, + /// where positive y values indicate down. + List getBoxesForPlaceholders() native 'Paragraph_getRectsForPlaceholders'; + /// Returns the text position closest to the given offset. TextPosition getPositionForOffset(Offset offset) { final List encoded = _getPositionForOffset(offset.dx, offset.dy); @@ -1576,6 +1630,7 @@ class ParagraphBuilder extends NativeFieldWrapperClass2 { /// [Paragraph]. @pragma('vm:entry-point') ParagraphBuilder(ParagraphStyle style) { + _placeholderCount = 0; List strutFontFamilies; if (style._strutStyle != null) { strutFontFamilies = []; @@ -1607,6 +1662,14 @@ class ParagraphBuilder extends NativeFieldWrapperClass2 { String locale ) native 'ParagraphBuilder_constructor'; + /// The number of placeholders currently in the paragraph. + int get placeholderCount => _placeholderCount; + int _placeholderCount; + + /// The scales of the placeholders in the paragraph. + List get placeholderScales => _placeholderScales; + List _placeholderScales = []; + /// Applies the given style to the added text until [pop] is called. /// /// See [pop] for details. @@ -1682,6 +1745,71 @@ class ParagraphBuilder extends NativeFieldWrapperClass2 { } String _addText(String text) native 'ParagraphBuilder_addText'; + /// Adds an inline placeholder space to the paragraph. + /// + /// The paragraph will contain a rectangular space with no text of the dimensions + /// specified. + /// + /// The `width` and `height` parameters specify the size of the placeholder rectangle. + /// + /// The `alignment` parameter specifies how the placeholder rectangle will be vertically + /// aligned with the surrounding text. When [PlaceholderAlignment.baseline], + /// [PlaceholderAlignment.aboveBaseline], and [PlaceholderAlignment.belowBaseline] + /// alignment modes are used, the baseline needs to be set with the `baseline`. + /// When using [PlaceholderAlignment.baseline], `baselineOffset` indicates the distance + /// of the baseline down from the top of of the rectangle. The default `baselineOffset` + /// is the `height`. + /// + /// Examples: + /// + /// * For a 30x50 placeholder with the bottom edge aligned with the bottom of the text, use: + /// `addPlaceholder(30, 50, PlaceholderAlignment.bottom);` + /// * For a 30x50 placeholder that is vertically centered around the text, use: + /// `addPlaceholder(30, 50, PlaceholderAlignment.middle);`. + /// * For a 30x50 placeholder that sits completely on top of the alphabetic baseline, use: + /// `addPlaceholder(30, 50, PlaceholderAlignment.aboveBaseline, baseline: TextBaseline.alphabetic)`. + /// * For a 30x50 placeholder with 40 pixels above and 10 pixels below the alphabetic baseline, use: + /// `addPlaceholder(30, 50, PlaceholderAlignment.baseline, baseline: TextBaseline.alphabetic, baselineOffset: 40)`. + /// + /// Lines are permitted to break around each placeholder. + /// + /// Decorations will be drawn based on the font defined in the most recently + /// pushed [TextStyle]. The decorations are drawn as if unicode text were present + /// in the placeholder space, and will draw the same regardless of the height and + /// alignment of the placeholder. To hide or manually adjust decorations to fit, + /// a text style with the desired decoration behavior should be pushed before + /// adding a placeholder. + /// + /// Any decorations drawn through a placeholder will exist on the same canvas/layer + /// as the text. This means any content drawn on top of the space reserved by + /// the placeholder will be drawn over the decoration, possibly obscuring the + /// decoration. + /// + /// Placeholders are represented by a unicode 0xFFFC "object replacement character" + /// in the text buffer. For each placeholder, one object replacement character is + /// added on to the text buffer. + /// + /// The `scale` parameter will scale the `width` and `height` by the specified amount, + /// and keep track of the scale. The scales of placeholders added can be accessed + /// through [placeholderScales]. This is primarily used for acessibility scaling. + void addPlaceholder(double width, double height, PlaceholderAlignment alignment, { + double scale = 1.0, + double baselineOffset, + TextBaseline baseline, + }) { + // Require a baseline to be specified if using a baseline-based alignment. + assert((alignment == PlaceholderAlignment.aboveBaseline || + alignment == PlaceholderAlignment.belowBaseline || + alignment == PlaceholderAlignment.baseline) ? baseline != null : true); + // Default the baselineOffset to height if null. This will place the placeholder + // fully above the baseline, similar to [PlaceholderAlignment.aboveBaseline]. + baselineOffset = baselineOffset ?? height; + _addPlaceholder(width * scale, height * scale, alignment.index, (baselineOffset == null ? height : baselineOffset) * scale, baseline == null ? null : baseline.index); + _placeholderCount++; + _placeholderScales.add(scale); + } + String _addPlaceholder(double width, double height, int alignment, double baselineOffset, int baseline) native 'ParagraphBuilder_addPlaceholder'; + /// Applies the given paragraph style and returns a [Paragraph] containing the /// added text and associated styling. /// diff --git a/engine/src/flutter/lib/ui/text/paragraph.cc b/engine/src/flutter/lib/ui/text/paragraph.cc index c79978bf4ea..3821d4bc4e9 100644 --- a/engine/src/flutter/lib/ui/text/paragraph.cc +++ b/engine/src/flutter/lib/ui/text/paragraph.cc @@ -19,19 +19,20 @@ namespace flutter { IMPLEMENT_WRAPPERTYPEINFO(ui, Paragraph); -#define FOR_EACH_BINDING(V) \ - V(Paragraph, width) \ - V(Paragraph, height) \ - V(Paragraph, longestLine) \ - V(Paragraph, minIntrinsicWidth) \ - V(Paragraph, maxIntrinsicWidth) \ - V(Paragraph, alphabeticBaseline) \ - V(Paragraph, ideographicBaseline) \ - V(Paragraph, didExceedMaxLines) \ - V(Paragraph, layout) \ - V(Paragraph, paint) \ - V(Paragraph, getWordBoundary) \ - V(Paragraph, getRectsForRange) \ +#define FOR_EACH_BINDING(V) \ + V(Paragraph, width) \ + V(Paragraph, height) \ + V(Paragraph, longestLine) \ + V(Paragraph, minIntrinsicWidth) \ + V(Paragraph, maxIntrinsicWidth) \ + V(Paragraph, alphabeticBaseline) \ + V(Paragraph, ideographicBaseline) \ + V(Paragraph, didExceedMaxLines) \ + V(Paragraph, layout) \ + V(Paragraph, paint) \ + V(Paragraph, getWordBoundary) \ + V(Paragraph, getRectsForRange) \ + V(Paragraph, getRectsForPlaceholders) \ V(Paragraph, getPositionForOffset) DART_BIND_ALL(Paragraph, FOR_EACH_BINDING) @@ -98,6 +99,10 @@ std::vector Paragraph::getRectsForRange(unsigned start, static_cast(boxWidthStyle)); } +std::vector Paragraph::getRectsForPlaceholders() { + return m_paragraphImpl->getRectsForPlaceholders(); +} + Dart_Handle Paragraph::getPositionForOffset(double dx, double dy) { return m_paragraphImpl->getPositionForOffset(dx, dy); } diff --git a/engine/src/flutter/lib/ui/text/paragraph.h b/engine/src/flutter/lib/ui/text/paragraph.h index c0d5613999c..3fea79b2245 100644 --- a/engine/src/flutter/lib/ui/text/paragraph.h +++ b/engine/src/flutter/lib/ui/text/paragraph.h @@ -47,6 +47,7 @@ class Paragraph : public RefCountedDartWrappable { unsigned end, unsigned boxHeightStyle, unsigned boxWidthStyle); + std::vector getRectsForPlaceholders(); Dart_Handle getPositionForOffset(double dx, double dy); Dart_Handle getWordBoundary(unsigned offset); diff --git a/engine/src/flutter/lib/ui/text/paragraph_builder.cc b/engine/src/flutter/lib/ui/text/paragraph_builder.cc index d0c55dabce5..1682cbe4147 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_builder.cc +++ b/engine/src/flutter/lib/ui/text/paragraph_builder.cc @@ -14,6 +14,7 @@ #include "flutter/third_party/txt/src/txt/font_style.h" #include "flutter/third_party/txt/src/txt/font_weight.h" #include "flutter/third_party/txt/src/txt/paragraph_style.h" +#include "flutter/third_party/txt/src/txt/text_baseline.h" #include "flutter/third_party/txt/src/txt/text_decoration.h" #include "flutter/third_party/txt/src/txt/text_style.h" #include "third_party/icu/source/common/unicode/ustring.h" @@ -132,10 +133,11 @@ static void ParagraphBuilder_constructor(Dart_NativeArguments args) { IMPLEMENT_WRAPPERTYPEINFO(ui, ParagraphBuilder); -#define FOR_EACH_BINDING(V) \ - V(ParagraphBuilder, pushStyle) \ - V(ParagraphBuilder, pop) \ - V(ParagraphBuilder, addText) \ +#define FOR_EACH_BINDING(V) \ + V(ParagraphBuilder, pushStyle) \ + V(ParagraphBuilder, pop) \ + V(ParagraphBuilder, addText) \ + V(ParagraphBuilder, addPlaceholder) \ V(ParagraphBuilder, build) FOR_EACH_BINDING(DART_NATIVE_CALLBACK) @@ -459,6 +461,20 @@ Dart_Handle ParagraphBuilder::addText(const std::u16string& text) { return Dart_Null(); } +Dart_Handle ParagraphBuilder::addPlaceholder(double width, + double height, + unsigned alignment, + double baseline_offset, + unsigned baseline) { + txt::PlaceholderRun placeholder_run( + width, height, static_cast(alignment), + static_cast(baseline), baseline_offset); + + m_paragraphBuilder->AddPlaceholder(placeholder_run); + + return Dart_Null(); +} + fml::RefPtr ParagraphBuilder::build() { return Paragraph::Create(m_paragraphBuilder->Build()); } diff --git a/engine/src/flutter/lib/ui/text/paragraph_builder.h b/engine/src/flutter/lib/ui/text/paragraph_builder.h index 15135aebb26..eebc5e7f268 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_builder.h +++ b/engine/src/flutter/lib/ui/text/paragraph_builder.h @@ -6,6 +6,7 @@ #define FLUTTER_LIB_UI_TEXT_PARAGRAPH_BUILDER_H_ #include + #include "flutter/lib/ui/dart_wrapper.h" #include "flutter/lib/ui/painting/paint.h" #include "flutter/lib/ui/text/paragraph.h" @@ -56,6 +57,18 @@ class ParagraphBuilder : public RefCountedDartWrappable { Dart_Handle addText(const std::u16string& text); + // Pushes the information requried to leave an open space, where Flutter may + // draw a custom placeholder into. + // + // Internally, this method adds a single object replacement character (0xFFFC) + // and emplaces a new PlaceholderRun instance to the vector of inline + // placeholders. + Dart_Handle addPlaceholder(double width, + double height, + unsigned alignment, + double baseline_offset, + unsigned baseline); + fml::RefPtr build(); static void RegisterNatives(tonic::DartLibraryNatives* natives); diff --git a/engine/src/flutter/lib/ui/text/paragraph_impl.h b/engine/src/flutter/lib/ui/text/paragraph_impl.h index 5c5f76e2fbd..c0dd8e4811f 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_impl.h +++ b/engine/src/flutter/lib/ui/text/paragraph_impl.h @@ -41,6 +41,8 @@ class ParagraphImpl { txt::Paragraph::RectHeightStyle rect_height_style, txt::Paragraph::RectWidthStyle rect_width_style) = 0; + virtual std::vector getRectsForPlaceholders() = 0; + virtual Dart_Handle getPositionForOffset(double dx, double dy) = 0; virtual Dart_Handle getWordBoundary(unsigned offset) = 0; diff --git a/engine/src/flutter/lib/ui/text/paragraph_impl_txt.cc b/engine/src/flutter/lib/ui/text/paragraph_impl_txt.cc index 6cb5df3bfa6..ab9467a572f 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_impl_txt.cc +++ b/engine/src/flutter/lib/ui/text/paragraph_impl_txt.cc @@ -79,6 +79,17 @@ std::vector ParagraphImplTxt::getRectsForRange( return result; } +std::vector ParagraphImplTxt::getRectsForPlaceholders() { + std::vector result; + std::vector boxes = + m_paragraph->GetRectsForPlaceholders(); + for (const txt::Paragraph::TextBox& box : boxes) { + result.emplace_back(box.rect, + static_cast(box.direction)); + } + return result; +} + Dart_Handle ParagraphImplTxt::getPositionForOffset(double dx, double dy) { Dart_Handle result = Dart_NewListOf(Dart_CoreType_Int, 2); txt::Paragraph::PositionWithAffinity pos = diff --git a/engine/src/flutter/lib/ui/text/paragraph_impl_txt.h b/engine/src/flutter/lib/ui/text/paragraph_impl_txt.h index 4069855e74c..e1323202b3d 100644 --- a/engine/src/flutter/lib/ui/text/paragraph_impl_txt.h +++ b/engine/src/flutter/lib/ui/text/paragraph_impl_txt.h @@ -34,6 +34,7 @@ class ParagraphImplTxt : public ParagraphImpl { unsigned end, txt::Paragraph::RectHeightStyle rect_height_style, txt::Paragraph::RectWidthStyle rect_width_style) override; + std::vector getRectsForPlaceholders() override; Dart_Handle getPositionForOffset(double dx, double dy) override; Dart_Handle getWordBoundary(unsigned offset) override; diff --git a/engine/src/flutter/third_party/txt/BUILD.gn b/engine/src/flutter/third_party/txt/BUILD.gn index 56aba933d7b..a8b68b0408b 100644 --- a/engine/src/flutter/third_party/txt/BUILD.gn +++ b/engine/src/flutter/third_party/txt/BUILD.gn @@ -95,6 +95,8 @@ source_set("txt") { "src/txt/paragraph_builder.h", "src/txt/paragraph_style.cc", "src/txt/paragraph_style.h", + "src/txt/placeholder_run.cc", + "src/txt/placeholder_run.h", "src/txt/platform.h", "src/txt/styled_runs.cc", "src/txt/styled_runs.h", diff --git a/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.cpp b/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.cpp index 35f7d9da2bd..52417d28e35 100644 --- a/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.cpp +++ b/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.cpp @@ -356,6 +356,14 @@ void LineBreaker::pushBreak(int offset, float width, uint8_t hyphenEdit) { mFirstTabIndex = INT_MAX; } +// libtxt: Add ability to set custom char widths. This allows manual definition +// of the widths of arbitrary glyphs. To linebreak properly, call addStyleRun +// with nullptr as the paint property, which will lead it to assume the width +// has already been calculated. Used for properly breaking inline widgets. +void LineBreaker::setCustomCharWidth(size_t offset, float width) { + mCharWidths[offset] = (width); +} + void LineBreaker::addReplacement(size_t start, size_t end, float width) { mCharWidths[start] = width; std::fill(&mCharWidths[start + 1], &mCharWidths[end], 0.0f); diff --git a/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.h b/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.h index 1dc88e1f1d7..12dd439d40a 100644 --- a/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.h +++ b/engine/src/flutter/third_party/txt/src/minikin/LineBreaker.h @@ -149,6 +149,13 @@ class LineBreaker { size_t computeBreaks(); + // libtxt: Add ability to set custom char widths. This allows manual + // definition of the widths of arbitrary glyphs. To linebreak properly, call + // addStyleRun with nullptr as the paint property, which will lead it to + // assume the width has already been calculated. Used for properly breaking + // inline placeholders. + void setCustomCharWidth(size_t offset, float width); + const int* getBreaks() const { return mBreaks.data(); } const float* getWidths() const { return mWidths.data(); } diff --git a/engine/src/flutter/third_party/txt/src/txt/paint_record.cc b/engine/src/flutter/third_party/txt/src/txt/paint_record.cc index e5cc56ae974..21ff440886e 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paint_record.cc +++ b/engine/src/flutter/third_party/txt/src/txt/paint_record.cc @@ -15,6 +15,7 @@ */ #include "paint_record.h" + #include "flutter/fml/logging.h" namespace txt { @@ -38,6 +39,25 @@ PaintRecord::PaintRecord(TextStyle style, x_end_(x_end), is_ghost_(is_ghost) {} +PaintRecord::PaintRecord(TextStyle style, + SkPoint offset, + sk_sp text, + SkFontMetrics metrics, + size_t line, + double x_start, + double x_end, + bool is_ghost, + PlaceholderRun* placeholder_run) + : style_(style), + offset_(offset), + text_(std::move(text)), + metrics_(metrics), + line_(line), + x_start_(x_start), + x_end_(x_end), + is_ghost_(is_ghost), + placeholder_run_(placeholder_run) {} + PaintRecord::PaintRecord(TextStyle style, sk_sp text, SkFontMetrics metrics, @@ -59,6 +79,7 @@ PaintRecord::PaintRecord(PaintRecord&& other) { text_ = std::move(other.text_); metrics_ = other.metrics_; line_ = other.line_; + placeholder_run_ = other.placeholder_run_; x_start_ = other.x_start_; x_end_ = other.x_end_; is_ghost_ = other.is_ghost_; @@ -73,6 +94,7 @@ PaintRecord& PaintRecord::operator=(PaintRecord&& other) { x_start_ = other.x_start_; x_end_ = other.x_end_; is_ghost_ = other.is_ghost_; + placeholder_run_ = other.placeholder_run_; return *this; } diff --git a/engine/src/flutter/third_party/txt/src/txt/paint_record.h b/engine/src/flutter/third_party/txt/src/txt/paint_record.h index 1302735c4d9..33917488ccc 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paint_record.h +++ b/engine/src/flutter/third_party/txt/src/txt/paint_record.h @@ -19,6 +19,7 @@ #include "flutter/fml/logging.h" #include "flutter/fml/macros.h" +#include "placeholder_run.h" #include "text_style.h" #include "third_party/skia/include/core/SkFontMetrics.h" #include "third_party/skia/include/core/SkTextBlob.h" @@ -43,6 +44,16 @@ class PaintRecord { double x_end, bool is_ghost); + PaintRecord(TextStyle style, + SkPoint offset, + sk_sp text, + SkFontMetrics metrics, + size_t line, + double x_start, + double x_end, + bool is_ghost, + PlaceholderRun* placeholder_run); + PaintRecord(TextStyle style, sk_sp text, SkFontMetrics metrics, @@ -71,8 +82,12 @@ class PaintRecord { double x_end() const { return x_end_; } double GetRunWidth() const { return x_end_ - x_start_; } + PlaceholderRun* GetPlaceholderRun() const { return placeholder_run_; } + bool isGhost() const { return is_ghost_; } + bool isPlaceholder() const { return placeholder_run_ == nullptr; } + private: TextStyle style_; // offset_ is the overall offset of the origin of the SkTextBlob. @@ -87,6 +102,10 @@ class PaintRecord { // 'Ghost' runs represent trailing whitespace. 'Ghost' runs should not have // decorations painted on them and do not impact layout of visible glyphs. bool is_ghost_ = false; + // Stores the corresponding PlaceholderRun that the record corresponds to. + // When this is nullptr, then the record is of normal text and does not + // represent an inline placeholder. + PlaceholderRun* placeholder_run_ = nullptr; FML_DISALLOW_COPY_AND_ASSIGN(PaintRecord); }; diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph.cc b/engine/src/flutter/third_party/txt/src/txt/paragraph.cc index 449b0d13dff..33712f77161 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph.cc +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph.cc @@ -209,13 +209,15 @@ Paragraph::CodeUnitRun::CodeUnitRun(std::vector&& p, Range x, size_t line, const SkFontMetrics& metrics, - TextDirection dir) + TextDirection dir, + const PlaceholderRun* placeholder) : positions(std::move(p)), code_units(cu), x_pos(x), line_number(line), font_metrics(metrics), - direction(dir) {} + direction(dir), + placeholder_run(placeholder) {} void Paragraph::CodeUnitRun::Shift(double delta) { x_pos.Shift(delta); @@ -237,21 +239,33 @@ void Paragraph::SetText(std::vector text, StyledRuns runs) { runs_ = std::move(runs); } +void Paragraph::SetInlinePlaceholders( + std::vector inline_placeholders, + std::unordered_set obj_replacement_char_indexes) { + needs_layout_ = true; + inline_placeholders_ = std::move(inline_placeholders); + obj_replacement_char_indexes_ = std::move(obj_replacement_char_indexes); +} + bool Paragraph::ComputeLineBreaks() { line_ranges_.clear(); line_widths_.clear(); max_intrinsic_width_ = 0; std::vector newline_positions; + // Discover and add all hard breaks. for (size_t i = 0; i < text_.size(); ++i) { ULineBreak ulb = static_cast( 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 = @@ -266,6 +280,9 @@ bool Paragraph::ComputeLineBreaks() { continue; } + // Setup breaker. We wait to set the line width in order to account for the + // widths of the inline placeholders, which are calcualted 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); @@ -301,15 +318,38 @@ bool Paragraph::ComputeLineBreaks() { 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; + + // 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(); @@ -480,6 +520,80 @@ void Paragraph::ComputeStrut(StrutMetrics* strut, SkFont& font) { } } +void Paragraph::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 Paragraph::Layout(double width, bool force) { double rounded_width = floor(width); // Do not allow calling layout multiple times without changing anything. @@ -508,6 +622,7 @@ void Paragraph::Layout(double width, bool force) { line_baselines_.clear(); glyph_lines_.clear(); code_unit_runs_.clear(); + inline_placeholder_code_unit_runs_.clear(); line_max_spacings_.clear(); line_max_descent_.clear(); line_max_ascent_.clear(); @@ -527,6 +642,7 @@ void Paragraph::Layout(double width, bool force) { size_t line_limit = std::min(paragraph_style_.max_lines, line_ranges_.size()); did_exceed_max_lines_ = (line_ranges_.size() > paragraph_style_.max_lines); + size_t placeholder_run_index = 0; for (size_t line_number = 0; line_number < line_limit; ++line_number) { const LineRange& line_range = line_ranges_[line_number]; @@ -583,9 +699,21 @@ void Paragraph::Layout(double width, bool force) { // 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()); + // 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_range.start), + 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_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) { @@ -603,6 +731,7 @@ void Paragraph::Layout(double width, bool force) { std::vector line_glyph_positions; std::vector line_code_unit_runs; + std::vector line_inline_placeholder_code_unit_runs; double run_x_offset = 0; double justify_x_offset = 0; std::vector paint_records; @@ -814,15 +943,25 @@ void Paragraph::Layout(double width, bool force) { if (glyph_positions.empty()) continue; + SkFontMetrics metrics; font.getMetrics(&metrics); Range 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()); + if (run.is_placeholder_run()) { + 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.start + run.placeholder_run()->width, run.is_ghost(), + run.placeholder_run()); + run_x_offset += run.placeholder_run()->width; + } else { + 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(), @@ -840,20 +979,29 @@ void Paragraph::Layout(double width, bool force) { std::move(code_unit_positions), Range(run.start(), run.end()), Range(glyph_positions.front().x_pos.start, - glyph_positions.back().x_pos.end), - line_number, metrics, run.direction()); + run.is_placeholder_run() + ? glyph_positions.back().x_pos.start + + run.placeholder_run()->width + : glyph_positions.back().x_pos.end), + line_number, metrics, 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_, 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()) { + // if (!run.is_ghost() || run.is_rtl()) { + if ((!run.is_ghost() || run.is_rtl()) && !run.is_placeholder_run()) { run_x_offset += layout.getAdvance(); } } // for each in line_runs @@ -864,6 +1012,10 @@ void Paragraph::Layout(double width, bool force) { 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); } @@ -876,28 +1028,39 @@ void Paragraph::Layout(double width, bool force) { next_line_start - line_range.start); 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 = strut_.ascent + strut_.half_leading; double max_descent = strut_.descent + strut_.half_leading; - SkScalar max_unscaled_ascent = 0; + double max_unscaled_ascent = 0; auto update_line_metrics = [&](const SkFontMetrics& metrics, - const TextStyle& style) { + const TextStyle& style, + PlaceholderRun* placeholder_run) { 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; + + ComputePlaceholder(placeholder_run, ascent, descent); + + max_ascent = std::max(ascent, max_ascent); max_descent = std::max(descent, max_descent); } - max_unscaled_ascent = std::max(-metrics.fAscent, max_unscaled_ascent); + max_unscaled_ascent = std::max(placeholder_run == nullptr + ? -metrics.fAscent + : placeholder_run->baseline_offset, + max_unscaled_ascent); }; for (const PaintRecord& paint_record : paint_records) { - update_line_metrics(paint_record.metrics(), paint_record.style()); + update_line_metrics(paint_record.metrics(), paint_record.style(), + paint_record.GetPlaceholderRun()); } // If no fonts were actually rendered, then compute a baseline based on the @@ -908,7 +1071,7 @@ void Paragraph::Layout(double width, bool force) { font.setTypeface(GetDefaultSkiaTypeface(style)); font.setSize(style.font_size); font.getMetrics(&metrics); - update_line_metrics(metrics, style); + update_line_metrics(metrics, style, nullptr); } // Calculate the baselines. This is only done on the first line. @@ -1063,8 +1226,10 @@ void Paragraph::Paint(SkCanvas* canvas, double x, double y) { paint.setColor(record.style().color); } SkPoint offset = base_offset + record.offset(); - PaintShadow(canvas, record, offset); - canvas->drawTextBlob(record.text(), offset.x(), offset.y(), paint); + if (record.GetPlaceholderRun() == nullptr) { + PaintShadow(canvas, record, offset); + canvas->drawTextBlob(record.text(), offset.x(), offset.y(), paint); + } PaintDecorations(canvas, record, base_offset); } } @@ -1300,6 +1465,13 @@ std::vector Paragraph::GetRectsForRange( 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); @@ -1538,6 +1710,46 @@ Paragraph::PositionWithAffinity Paragraph::GetGlyphPositionAtCoordinate( } } +// 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::GetRectsForPlaceholders() const { + // Struct that holds calculated metrics for each line. + struct LineBoxMetrics { + std::vector 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::vector 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_baselines_[run.line_number]; + 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 Paragraph::GetWordBoundary(size_t offset) const { if (text_.size() == 0) return Range(0, 0); diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph.h b/engine/src/flutter/third_party/txt/src/txt/paragraph.h index 2328ba8c957..c4922691012 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph.h +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph.h @@ -27,6 +27,7 @@ #include "minikin/LineBreaker.h" #include "paint_record.h" #include "paragraph_style.h" +#include "placeholder_run.h" #include "styled_runs.h" #include "third_party/googletest/googletest/include/gtest/gtest_prod.h" // nogncheck #include "third_party/skia/include/core/SkFontMetrics.h" @@ -39,6 +40,14 @@ namespace txt { using GlyphID = uint32_t; +// Constant with the unicode codepoint for the "Object replacement character". +// Used as a stand-in character for Placeholder boxes. +const int objReplacementChar = 0xFFFC; +// Constant with the unicode codepoint for the "Replacement character". This is +// the character that commonly renders as a black diamond with a white question +// mark. Used to replace non-placeholder instances of 0xFFFC in the text buffer. +const int replacementChar = 0xFFFD; + // Paragraph provides Layout, metrics, and painting capabilities for text. Once // a Paragraph is constructed with ParagraphBuilder::Build(), an example basic // workflow can be this: @@ -190,6 +199,17 @@ class Paragraph { // with the top left corner as the origin, and +y direction as down. PositionWithAffinity GetGlyphPositionAtCoordinate(double dx, double dy) const; + // Returns a vector of bounding boxes that bound all inline placeholders in + // the paragraph. + // + // There will be one box for each inline placeholder. The boxes will be in the + // same order as they were added to the paragraph. The bounds will always be + // tight and should fully enclose the area where the placeholder should be. + // + // More granular boxes may be obtained through GetRectsForRange, which will + // return bounds on both text as well as inline placeholders. + std::vector GetRectsForPlaceholders() const; + // Finds the first and last glyphs that define a word containing the glyph at // index offset. Range GetWordBoundary(size_t offset) const; @@ -236,10 +256,23 @@ class Paragraph { FRIEND_TEST(ParagraphTest, SimpleShadow); FRIEND_TEST(ParagraphTest, ComplexShadow); FRIEND_TEST(ParagraphTest, FontFallbackParagraph); + FRIEND_TEST(ParagraphTest, InlinePlaceholder0xFFFCParagraph); FRIEND_TEST(ParagraphTest, FontFeaturesParagraph); // Starting data to layout. std::vector text_; + // A vector of PlaceholderRuns, which detail the sizes, positioning and break + // behavior of the empty spaces to leave. Each placeholder span corresponds to + // a 0xFFFC (object replacement character) in text_, which indicates the + // position in the text where the placeholder will occur. There should be an + // equal number of 0xFFFC characters and elements in this vector. + std::vector inline_placeholders_; + // The indexes of the boxes that correspond to an inline placeholder. + std::vector inline_placeholder_boxes_; + // The indexes of instances of 0xFFFC that correspond to placeholders. This is + // necessary since the user may pass in manually entered 0xFFFC values using + // AddText(). + std::unordered_set obj_replacement_char_indexes_; StyledRuns runs_; ParagraphStyle paragraph_style_; std::shared_ptr font_collection_; @@ -304,19 +337,35 @@ class Paragraph { bool is_ghost) : start_(s), end_(e), direction_(d), style_(&st), is_ghost_(is_ghost) {} + // Constructs a placeholder bidi run. + BidiRun(size_t s, + size_t e, + TextDirection d, + const TextStyle& st, + PlaceholderRun& placeholder) + : start_(s), + end_(e), + direction_(d), + style_(&st), + placeholder_run_(&placeholder) {} + size_t start() const { return start_; } size_t end() const { return end_; } + size_t size() const { return end_ - start_; } TextDirection direction() const { return direction_; } const TextStyle& style() const { return *style_; } + PlaceholderRun* placeholder_run() const { return placeholder_run_; } bool is_rtl() const { return direction_ == TextDirection::rtl; } // Tracks if the run represents trailing whitespace. bool is_ghost() const { return is_ghost_; } + bool is_placeholder_run() const { return placeholder_run_ != nullptr; } private: size_t start_, end_; TextDirection direction_; const TextStyle* style_; bool is_ghost_; + PlaceholderRun* placeholder_run_ = nullptr; }; struct GlyphPosition { @@ -347,13 +396,15 @@ class Paragraph { size_t line_number; SkFontMetrics font_metrics; TextDirection direction; + const PlaceholderRun* placeholder_run; CodeUnitRun(std::vector&& p, Range cu, Range x, size_t line, const SkFontMetrics& metrics, - TextDirection dir); + TextDirection dir, + const PlaceholderRun* placeholder); void Shift(double delta); }; @@ -364,6 +415,8 @@ class Paragraph { // Holds the positions of each range of code units in the text. // Sorted in code unit index order. std::vector code_unit_runs_; + // Holds the positions of the inline placeholders. + std::vector inline_placeholder_code_unit_runs_; // The max width of the paragraph as provided in the most recent Layout() // call. @@ -394,6 +447,10 @@ class Paragraph { void SetFontCollection(std::shared_ptr font_collection); + void SetInlinePlaceholders( + std::vector inline_placeholders, + std::unordered_set obj_replacement_char_indexes); + // Break the text into lines. bool ComputeLineBreaks(); @@ -403,6 +460,13 @@ class Paragraph { // Calculates and populates strut based on paragraph_style_ strut info. void ComputeStrut(StrutMetrics* strut, SkFont& font); + // Adjusts the ascent and descent based on the existence and type of + // placeholder. This method sets the proper metrics to achieve the different + // PlaceholderAlignment options. + void ComputePlaceholder(PlaceholderRun* placeholder_run, + double& ascent, + double& descent); + bool IsStrutValid() const; // Calculate the starting X offset of a line based on the line's width and diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.cc b/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.cc index cd9d1591499..c486dc2ca88 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.cc +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.cc @@ -77,11 +77,21 @@ void ParagraphBuilder::AddText(const char* text) { AddText(u16_text); } +void ParagraphBuilder::AddPlaceholder(PlaceholderRun& span) { + obj_replacement_char_indexes_.insert(text_.size()); + runs_.StartRun(PeekStyleIndex(), text_.size()); + AddText(std::u16string(1ull, objReplacementChar)); + runs_.StartRun(PeekStyleIndex(), text_.size()); + inline_placeholders_.push_back(span); +} + std::unique_ptr ParagraphBuilder::Build() { runs_.EndRunIfNeeded(text_.size()); std::unique_ptr paragraph = std::make_unique(); paragraph->SetText(std::move(text_), std::move(runs_)); + paragraph->SetInlinePlaceholders(std::move(inline_placeholders_), + std::move(obj_replacement_char_indexes_)); paragraph->SetParagraphStyle(paragraph_style_); paragraph->SetFontCollection(font_collection_); SetParagraphStyle(paragraph_style_); diff --git a/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.h b/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.h index 31ae5bf06f3..b6c2f240647 100644 --- a/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.h +++ b/engine/src/flutter/third_party/txt/src/txt/paragraph_builder.h @@ -24,6 +24,7 @@ #include "font_collection.h" #include "paragraph.h" #include "paragraph_style.h" +#include "placeholder_run.h" #include "styled_runs.h" #include "text_style.h" @@ -66,6 +67,14 @@ class ParagraphBuilder { // Converts to u16string before adding. void AddText(const char* text); + // Pushes the information requried to leave an open space, where Flutter may + // draw a custom placeholder into. + // + // Internally, this method adds a single object replacement character (0xFFFC) + // and emplaces a new PlaceholderRun instance to the vector of inline + // placeholders. + void AddPlaceholder(PlaceholderRun& span); + void SetParagraphStyle(const ParagraphStyle& style); // Constructs a Paragraph object that can be used to layout and paint the text @@ -74,6 +83,15 @@ class ParagraphBuilder { private: std::vector text_; + // A vector of PlaceholderRuns, which detail the sizes, positioning and break + // behavior of the empty spaces to leave. Each placeholder span corresponds to + // a 0xFFFC (object replacement character) in text_, which indicates the + // position in the text where the placeholder will occur. There should be an + // equal number of 0xFFFC characters and elements in this vector. + std::vector inline_placeholders_; + // The indexes of the obj replacement characters added through + // ParagraphBuilder::addPlaceholder(). + std::unordered_set obj_replacement_char_indexes_; std::vector style_stack_; std::shared_ptr font_collection_; StyledRuns runs_; diff --git a/engine/src/flutter/third_party/txt/src/txt/placeholder_run.cc b/engine/src/flutter/third_party/txt/src/txt/placeholder_run.cc new file mode 100644 index 00000000000..3f20d468ff5 --- /dev/null +++ b/engine/src/flutter/third_party/txt/src/txt/placeholder_run.cc @@ -0,0 +1,34 @@ +/* + * Copyright 2019 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 "placeholder_run.h" + +namespace txt { + +PlaceholderRun::PlaceholderRun() {} + +PlaceholderRun::PlaceholderRun(double width, + double height, + PlaceholderAlignment alignment, + TextBaseline baseline, + double baseline_offset) + : width(width), + height(height), + alignment(alignment), + baseline(baseline), + baseline_offset(baseline_offset) {} + +} // namespace txt diff --git a/engine/src/flutter/third_party/txt/src/txt/placeholder_run.h b/engine/src/flutter/third_party/txt/src/txt/placeholder_run.h new file mode 100644 index 00000000000..ed0c5de1235 --- /dev/null +++ b/engine/src/flutter/third_party/txt/src/txt/placeholder_run.h @@ -0,0 +1,89 @@ +/* + * Copyright 2019 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. + */ + +#ifndef LIB_TXT_SRC_PLACEHOLDER_RUN_H_ +#define LIB_TXT_SRC_PLACEHOLDER_RUN_H_ + +#include "text_baseline.h" + +namespace txt { + +/// Where to vertically align the placeholder relative to the surrounding text. +enum class PlaceholderAlignment { + /// Match the baseline of the placeholder with the baseline. + kBaseline, + + /// Align the bottom edge of the placeholder with the baseline such that the + /// placeholder sits on top of the baseline. + kAboveBaseline, + + /// Align the top edge of the placeholder with the baseline specified in + /// such that the placeholder hangs below the baseline. + kBelowBaseline, + + /// Align the top edge of the placeholder with the top edge of the font. + /// When the placeholder is very tall, the extra space will hang from + /// the top and extend through the bottom of the line. + kTop, + + /// Align the bottom edge of the placeholder with the top edge of the font. + /// When the placeholder is very tall, the extra space will rise from + /// the bottom and extend through the top of the line. + kBottom, + + /// Align the middle of the placeholder with the middle of the text. When the + /// placeholder is very tall, the extra space will grow equally from + /// the top and bottom of the line. + kMiddle, +}; + +// Represents the metrics required to fully define a rect that will fit a +// placeholder. +// +// LibTxt will leave an empty space in the layout of the text of the size +// defined by this class. After layout, the framework will draw placeholders +// into the reserved space. +class PlaceholderRun { + public: + double width = 0; + double height = 0; + + PlaceholderAlignment alignment; + + TextBaseline baseline; + + // Distance from the top edge of the rect to the baseline position. This + // baseline will be aligned against the alphabetic baseline of the surrounding + // text. + // + // Positive values drop the baseline lower (positions the rect higher) and + // small or negative values will cause the rect to be positioned underneath + // the line. When baseline == height, the bottom edge of the rect will rest on + // the alphabetic baseline. + double baseline_offset = 0; + + PlaceholderRun(); + + PlaceholderRun(double width, + double height, + PlaceholderAlignment alignment, + TextBaseline baseline, + double baseline_offset); +}; + +} // namespace txt + +#endif // LIB_TXT_SRC_PLACEHOLDER_RUN_H_ diff --git a/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc b/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc index c7c9086f539..1a073080b5b 100644 --- a/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc +++ b/engine/src/flutter/third_party/txt/tests/paragraph_unittests.cc @@ -24,6 +24,7 @@ #include "txt/font_weight.h" #include "txt/paragraph.h" #include "txt/paragraph_builder.h" +#include "txt/placeholder_run.h" #include "txt_test_utils.h" #define DISABLE_ON_WINDOWS(TEST) DISABLE_TEST_WINDOWS(TEST) @@ -69,6 +70,1048 @@ TEST_F(ParagraphTest, SimpleParagraph) { ASSERT_TRUE(Snapshot()); } +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(50, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 0); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.AddPlaceholder(placeholder_run); + txt::PlaceholderRun placeholder_run2(5, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 50); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddText(u16_text); + builder.AddPlaceholder(placeholder_run2); + + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddText(u16_text); + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForRange(0, 3, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + // ASSERT_TRUE(Snapshot()); + EXPECT_EQ(boxes.size(), 1ull); + + paint.setColor(SK_ColorGREEN); + boxes = + paragraph->GetRectsForRange(0, 1, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + + paint.setColor(SK_ColorRED); + boxes = paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(4, 17, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 7ull); + EXPECT_FLOAT_EQ(boxes[1].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[1].rect.top(), 50); + EXPECT_FLOAT_EQ(boxes[1].rect.right(), 140.92188); + EXPECT_FLOAT_EQ(boxes[1].rect.bottom(), 100); + + EXPECT_FLOAT_EQ(boxes[3].rect.left(), 231.34375); + EXPECT_FLOAT_EQ(boxes[3].rect.top(), 50); + EXPECT_FLOAT_EQ(boxes[3].rect.right(), 231.34375 + 50); + EXPECT_FLOAT_EQ(boxes[3].rect.bottom(), 100); + + EXPECT_FLOAT_EQ(boxes[4].rect.left(), 281.34375); + EXPECT_FLOAT_EQ(boxes[4].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[4].rect.right(), 281.34375 + 5); + EXPECT_FLOAT_EQ(boxes[4].rect.bottom(), 50); + + EXPECT_FLOAT_EQ(boxes[6].rect.left(), 336.34375); + EXPECT_FLOAT_EQ(boxes[6].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[6].rect.right(), 336.34375 + 5); + EXPECT_FLOAT_EQ(boxes[6].rect.bottom(), 50); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderBaselineParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(55, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 38.34734); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the box is in the right place + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 145.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(5, 6, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the other text didn't just shift to accomodate it. + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 75.324219); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 14.226246); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 44.694996); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, + DISABLE_ON_WINDOWS(InlinePlaceholderAboveBaselineParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(55, 50, + PlaceholderAlignment::kAboveBaseline, + TextBaseline::kAlphabetic, 903129.129308); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the box is in the right place + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), -0.34765625); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 145.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 49.652344); + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(5, 6, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the other text didn't just shift to accomodate it. + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 75.324219); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 25.53125); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 56); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, + DISABLE_ON_WINDOWS(InlinePlaceholderBelowBaselineParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(55, 50, + PlaceholderAlignment::kBelowBaseline, + TextBaseline::kAlphabetic, 903129.129308); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the box is in the right place + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 24); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 145.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 74); + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(5, 6, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the other text didn't just shift to accomodate it. + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 75.324219); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), -0.12109375); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 30.347656); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderBottomParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(55, 50, PlaceholderAlignment::kBottom, + TextBaseline::kAlphabetic, 0); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the box is in the right place + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 145.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(0, 1, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the other text didn't just shift to accomodate it. + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0.5); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 19.53125); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 16.097656); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderTopParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(55, 50, PlaceholderAlignment::kTop, + TextBaseline::kAlphabetic, 0); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the box is in the right place + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 145.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(0, 1, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the other text didn't just shift to accomodate it. + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 0.5); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 16.097656); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 30.46875); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderMiddleParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(55, 50, PlaceholderAlignment::kMiddle, + TextBaseline::kAlphabetic, 0); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the box is in the right place + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 145.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(5, 6, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the other text didn't just shift to accomodate it. + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 75.324219); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 9.765625); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 40.234375); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, + DISABLE_ON_WINDOWS(InlinePlaceholderIdeographicBaselineParagraph)) { + const char* text = "給能上目秘使"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Source Han Serif CN"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(55, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kIdeographic, 38.34734); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the box is in the right place + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 162.5); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 217.5); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(5, 6, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + // Verify the other text didn't just shift to accomodate it. + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 135.5); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 4.7033391); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 162.5); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 42.065342); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderBreakParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(50, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 50); + txt::PlaceholderRun placeholder_run2(25, 25, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 12.5); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddText(u16_text); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddText(u16_text); + builder.AddPlaceholder(placeholder_run2); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth() - 100); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kTight; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForRange(0, 3, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + + paint.setColor(SK_ColorGREEN); + boxes = paragraph->GetRectsForRange(175, 176, rect_height_style, + rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 1ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 31.695312); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 218.53125); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 47.292969); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 249); + + paint.setColor(SK_ColorRED); + boxes = paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(4, 45, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 30ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 59.726562); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 26.378906); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 56.847656); + + EXPECT_FLOAT_EQ(boxes[11].rect.left(), 606.34375); + EXPECT_FLOAT_EQ(boxes[11].rect.top(), 38); + EXPECT_FLOAT_EQ(boxes[11].rect.right(), 631.34375); + EXPECT_FLOAT_EQ(boxes[11].rect.bottom(), 63); + + EXPECT_FLOAT_EQ(boxes[17].rect.left(), 0.5); + EXPECT_FLOAT_EQ(boxes[17].rect.top(), 63.5); + EXPECT_FLOAT_EQ(boxes[17].rect.right(), 50.5); + EXPECT_FLOAT_EQ(boxes[17].rect.bottom(), 113.5); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholderGetRectsParagraph)) { + const char* text = "012 34"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + builder.AddText(u16_text); + + txt::PlaceholderRun placeholder_run(50, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 50); + txt::PlaceholderRun placeholder_run2(5, 20, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 10); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run); + + builder.AddText(u16_text); + + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run2); + + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddText(u16_text); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddPlaceholder(placeholder_run); + builder.AddPlaceholder(placeholder_run2); + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 34ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 90.921875); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 140.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + EXPECT_FLOAT_EQ(boxes[16].rect.left(), 800.92188); + EXPECT_FLOAT_EQ(boxes[16].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[16].rect.right(), 850.92188); + EXPECT_FLOAT_EQ(boxes[16].rect.bottom(), 50); + + EXPECT_FLOAT_EQ(boxes[33].rect.left(), 503.38281); + EXPECT_FLOAT_EQ(boxes[33].rect.top(), 160); + EXPECT_FLOAT_EQ(boxes[33].rect.right(), 508.38281); + EXPECT_FLOAT_EQ(boxes[33].rect.bottom(), 180); + + Paragraph::RectHeightStyle rect_height_style = + Paragraph::RectHeightStyle::kMax; + Paragraph::RectWidthStyle rect_width_style = + Paragraph::RectWidthStyle::kTight; + paint.setColor(SK_ColorBLUE); + boxes = + paragraph->GetRectsForRange(30, 50, rect_height_style, rect_width_style); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 8ull); + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 216.09766); + // Top should be taller than "tight" + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 60); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 290.92188); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 120); + + EXPECT_FLOAT_EQ(boxes[1].rect.left(), 290.92188); + EXPECT_FLOAT_EQ(boxes[1].rect.top(), 60); + EXPECT_FLOAT_EQ(boxes[1].rect.right(), 340.92188); + EXPECT_FLOAT_EQ(boxes[1].rect.bottom(), 120); + + EXPECT_FLOAT_EQ(boxes[2].rect.left(), 340.92188); + EXPECT_FLOAT_EQ(boxes[2].rect.top(), 60); + EXPECT_FLOAT_EQ(boxes[2].rect.right(), 345.92188); + EXPECT_FLOAT_EQ(boxes[2].rect.bottom(), 120); + + ASSERT_TRUE(Snapshot()); +} + +#if OS_LINUX +// Tests if manually inserted 0xFFFC characters are replaced to 0xFFFD in order +// to not interfere with the placeholder box layout. +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(InlinePlaceholder0xFFFCParagraph)) { + const char* text = "ab\uFFFCcd"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + // Used to generate the replaced version. + const char* text2 = "ab\uFFFDcd"; + auto icu_text2 = icu::UnicodeString::fromUTF8(text2); + std::u16string u16_text2(icu_text2.getBuffer(), + icu_text2.getBuffer() + icu_text2.length()); + + txt::ParagraphStyle paragraph_style; + paragraph_style.max_lines = 14; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_families = std::vector(1, "Roboto"); + text_style.font_size = 26; + text_style.letter_spacing = 1; + text_style.word_spacing = 5; + text_style.color = SK_ColorBLACK; + text_style.height = 1; + text_style.decoration = TextDecoration::kUnderline; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + std::vector truth_text; + + builder.AddText(u16_text); + truth_text.insert(truth_text.end(), u16_text2.begin(), u16_text2.end()); + builder.AddText(u16_text); + truth_text.insert(truth_text.end(), u16_text2.begin(), u16_text2.end()); + + txt::PlaceholderRun placeholder_run(50, 50, PlaceholderAlignment::kBaseline, + TextBaseline::kAlphabetic, 25); + builder.AddPlaceholder(placeholder_run); + truth_text.push_back(0xFFFC); + + builder.AddText(u16_text); + truth_text.insert(truth_text.end(), u16_text2.begin(), u16_text2.end()); + builder.AddText(u16_text); + truth_text.insert(truth_text.end(), u16_text2.begin(), u16_text2.end()); + + builder.AddPlaceholder(placeholder_run); + truth_text.push_back(0xFFFC); + builder.AddPlaceholder(placeholder_run); + truth_text.push_back(0xFFFC); + builder.AddText(u16_text); + truth_text.insert(truth_text.end(), u16_text2.begin(), u16_text2.end()); + builder.AddText(u16_text); + truth_text.insert(truth_text.end(), u16_text2.begin(), u16_text2.end()); + builder.AddPlaceholder(placeholder_run); + truth_text.push_back(0xFFFC); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + + paragraph->Paint(GetCanvas(), 0, 0); + + for (size_t i = 0; i < truth_text.size(); ++i) { + EXPECT_EQ(paragraph->text_[i], truth_text[i]); + } + + SkPaint paint; + paint.setStyle(SkPaint::kStroke_Style); + paint.setAntiAlias(true); + paint.setStrokeWidth(1); + + paint.setColor(SK_ColorRED); + + paint.setColor(SK_ColorRED); + std::vector boxes = + paragraph->GetRectsForPlaceholders(); + for (size_t i = 0; i < boxes.size(); ++i) { + GetCanvas()->drawRect(boxes[i].rect, paint); + } + EXPECT_EQ(boxes.size(), 4ull); + + EXPECT_FLOAT_EQ(boxes[0].rect.left(), 177.83594); + EXPECT_FLOAT_EQ(boxes[0].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[0].rect.right(), 227.83594); + EXPECT_FLOAT_EQ(boxes[0].rect.bottom(), 50); + + EXPECT_FLOAT_EQ(boxes[3].rect.left(), 682.50781); + EXPECT_FLOAT_EQ(boxes[3].rect.top(), 0); + EXPECT_FLOAT_EQ(boxes[3].rect.right(), 732.50781); + EXPECT_FLOAT_EQ(boxes[3].rect.bottom(), 50); + + ASSERT_TRUE(Snapshot()); +} +#endif + TEST_F(ParagraphTest, SimpleRedParagraph) { const char* text = "I am RED"; auto icu_text = icu::UnicodeString::fromUTF8(text);