diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index d3cf0bf9c8179..d66dfbed6379d 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1021,6 +1021,8 @@ FILE: ../../../flutter/third_party/txt/src/txt/test_font_manager.h FILE: ../../../flutter/third_party/txt/src/txt/text_baseline.h FILE: ../../../flutter/third_party/txt/src/txt/text_decoration.cc FILE: ../../../flutter/third_party/txt/src/txt/text_decoration.h +FILE: ../../../flutter/third_party/txt/src/txt/text_shadow.cc +FILE: ../../../flutter/third_party/txt/src/txt/text_shadow.h FILE: ../../../flutter/third_party/txt/src/txt/text_style.cc FILE: ../../../flutter/third_party/txt/src/txt/text_style.h FILE: ../../../flutter/third_party/txt/src/txt/typeface_font_asset_provider.cc diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 5844565bbf346..50a61384553fa 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -3631,6 +3631,217 @@ class PictureRecorder extends NativeFieldWrapperClass2 { Picture endRecording() native 'PictureRecorder_endRecording'; } +/// A single shadow. +/// +/// Multiple shadows are stacked together in a [TextStyle]. +class Shadow { + /// Construct a shadow. + /// + /// The default shadow is a black shadow with zero offset and zero blur. + /// Default shadows should be completely covered by the casting element, + /// and not be visble. + /// + /// Transparency should be adjusted through the [color] alpha. + /// + /// Shadow order matters due to compositing multiple translucent objects not + /// being commutative. + const Shadow({ + this.color = const Color(_kColorDefault), + this.offset = Offset.zero, + this.blurRadius = 0.0, + }) : assert(color != null, 'Text shadow color was null.'), + assert(offset != null, 'Text shadow offset was null.'), + assert(blurRadius >= 0.0, 'Text shadow blur radius should be non-negative.'); + + static const int _kColorDefault = 0xFF000000; + // Constants for shadow encoding. + static const int _kBytesPerShadow = 16; + static const int _kColorOffset = 0 << 2; + static const int _kXOffset = 1 << 2; + static const int _kYOffset = 2 << 2; + static const int _kBlurOffset = 3 << 2; + + /// Color that the shadow will be drawn with. + /// + /// The shadows are shapes composited directly over the base canvas, and do not + /// represent optical occlusion. + final Color color; + + /// The displacement of the shadow from the casting element. + /// + /// Positive x/y offsets will shift the shadow to the right and down, while + /// negative offsets shift the shadow to the left and up. The offsets are + /// relative to the position of the element that is casting it. + final Offset offset; + + /// The standard deviation of the Gaussian to convolve with the shadow's shape. + final double blurRadius; + + /// Converts a blur radius in pixels to sigmas. + /// + /// See the sigma argument to [MaskFilter.blur]. + /// + // See SkBlurMask::ConvertRadiusToSigma(). + // + static double convertRadiusToSigma(double radius) { + return radius * 0.57735 + 0.5; + } + + /// The [blurRadius] in sigmas instead of logical pixels. + /// + /// See the sigma argument to [MaskFilter.blur]. + double get blurSigma => convertRadiusToSigma(blurRadius); + + /// Create the [Paint] object that corresponds to this shadow description. + /// + /// The [offset] is not represented in the [Paint] object. + /// To honor this as well, the shape should be translated by [offset] before + /// being filled using this [Paint]. + /// + /// This class does not provide a way to disable shadows to avoid inconsistencies + /// in shadow blur rendering, primarily as a method of reducing test flakiness. + /// [toPaint] should be overriden in subclasses to provide this functionality. + Paint toPaint() { + return Paint() + ..color = color + ..maskFilter = MaskFilter.blur(BlurStyle.normal, blurSigma); + } + + /// Returns a new shadow with its [offset] and [blurRadius] scaled by the given + /// factor. + Shadow scale(double factor) { + return Shadow( + color: color, + offset: offset * factor, + blurRadius: blurRadius * factor, + ); + } + + /// Linearly interpolate between two shadows. + /// + /// If either shadow is null, this function linearly interpolates from a + /// a shadow that matches the other shadow in color but has a zero + /// offset and a zero blurRadius. + /// + /// {@template dart.ui.shadow.lerp} + /// The `t` argument represents position on the timeline, with 0.0 meaning + /// that the interpolation has not started, returning `a` (or something + /// equivalent to `a`), 1.0 meaning that the interpolation has finished, + /// returning `b` (or something equivalent to `b`), and values in between + /// meaning that the interpolation is at the relevant point on the timeline + /// between `a` and `b`. The interpolation can be extrapolated beyond 0.0 and + /// 1.0, so negative values and values greater than 1.0 are valid (and can + /// easily be generated by curves such as [Curves.elasticInOut]). + /// + /// Values for `t` are usually obtained from an [Animation], such as + /// an [AnimationController]. + /// {@endtemplate} + static Shadow lerp(Shadow a, Shadow b, double t) { + assert(t != null); + if (a == null && b == null) + return null; + if (a == null) + return b.scale(t); + if (b == null) + return a.scale(1.0 - t); + return Shadow( + color: Color.lerp(a.color, b.color, t), + offset: Offset.lerp(a.offset, b.offset, t), + blurRadius: lerpDouble(a.blurRadius, b.blurRadius, t), + ); + } + + /// Linearly interpolate between two lists of shadows. + /// + /// If the lists differ in length, excess items are lerped with null. + /// + /// {@macro dart.ui.shadow.lerp} + static List lerpList(List a, List b, double t) { + assert(t != null); + if (a == null && b == null) + return null; + a ??= []; + b ??= []; + final List result = []; + final int commonLength = math.min(a.length, b.length); + for (int i = 0; i < commonLength; i += 1) + result.add(Shadow.lerp(a[i], b[i], t)); + for (int i = commonLength; i < a.length; i += 1) + result.add(a[i].scale(1.0 - t)); + for (int i = commonLength; i < b.length; i += 1) + result.add(b[i].scale(t)); + return result; + } + + @override + bool operator ==(dynamic other) { + if (identical(this, other)) + return true; + if (other is! Shadow) + return false; + final Shadow typedOther = other; + return color == typedOther.color && + offset == typedOther.offset && + blurRadius == typedOther.blurRadius; + } + + @override + int get hashCode => hashValues(color, offset, blurRadius); + + /// Determines if lists [a] and [b] are deep equivalent. + /// + /// Returns true if the lists are both null, or if they are both non-null, have + /// the same length, and contain the same Shadows in the same order. Returns + /// false otherwise. + static bool _shadowsListEquals(List a, List b) { + // Compare _shadows + if (a == null) + return b == null; + if (b == null || a.length != b.length) + return false; + for (int index = 0; index < a.length; ++index) + if (a[index] != b[index]) + return false; + return true; + } + + // Serialize [shadows] into ByteData. The format is a single uint_32_t at + // the beginning indicating the number of shadows, followed by _kBytesPerShadow + // bytes for each shadow. + static ByteData _encodeShadows(List shadows) { + if (shadows == null) + return ByteData(0); + + final int byteCount = shadows.length * _kBytesPerShadow; + final ByteData shadowsData = ByteData(byteCount); + + int shadowOffset = 0; + for (int shadowIndex = 0; shadowIndex < shadows.length; ++shadowIndex) { + final Shadow shadow = shadows[shadowIndex]; + if (shadow == null) + continue; + shadowOffset = shadowIndex * _kBytesPerShadow; + + shadowsData.setInt32(_kColorOffset + shadowOffset, + shadow.color.value ^ Shadow._kColorDefault, _kFakeHostEndian); + + shadowsData.setFloat32(_kXOffset + shadowOffset, + shadow.offset.dx, _kFakeHostEndian); + + shadowsData.setFloat32(_kYOffset + shadowOffset, + shadow.offset.dy, _kFakeHostEndian); + + shadowsData.setFloat32(_kBlurOffset + shadowOffset, + shadow.blurRadius, _kFakeHostEndian); + } + + return shadowsData; + } + + @override + String toString() => 'TextShadow($color, $offset, $blurRadius)'; +} + /// Generic callback signature, used by [_futurize]. typedef _Callback = void Function(T result); diff --git a/lib/ui/text.dart b/lib/ui/text.dart index 60498c5386157..99c7da939d0ab 100644 --- a/lib/ui/text.dart +++ b/lib/ui/text.dart @@ -259,6 +259,7 @@ Int32List _encodeTextStyle( Locale locale, Paint background, Paint foreground, + List shadows ) { final Int32List result = new Int32List(8); if (color != null) { @@ -321,6 +322,10 @@ Int32List _encodeTextStyle( result[0] |= 1 << 15; // Passed separately to native. } + if (shadows != null) { + result[0] |= 1 << 16; + // Passed separately to native. + } return result; } @@ -359,6 +364,7 @@ class TextStyle { Locale locale, Paint background, Paint foreground, + List shadows, }) : assert(color == null || foreground == null, 'Cannot provide both a color and a foreground\n' 'The color argument is just a shorthand for "foreground: new Paint()..color = color".' @@ -379,6 +385,7 @@ class TextStyle { locale, background, foreground, + shadows, ), _fontFamily = fontFamily ?? '', _fontSize = fontSize, @@ -387,7 +394,8 @@ class TextStyle { _height = height, _locale = locale, _background = background, - _foreground = foreground; + _foreground = foreground, + _shadows = shadows; final Int32List _encoded; final String _fontFamily; @@ -398,6 +406,7 @@ class TextStyle { final Locale _locale; final Paint _background; final Paint _foreground; + final List _shadows; @override bool operator ==(dynamic other) { @@ -419,6 +428,8 @@ class TextStyle { if (_encoded[index] != typedOther._encoded[index]) return false; } + if (!Shadow._shadowsListEquals(_shadows, typedOther._shadows)) + return false; return true; } @@ -428,21 +439,22 @@ class TextStyle { @override String toString() { return 'TextStyle(' - 'color: ${ _encoded[0] & 0x0002 == 0x0002 ? new Color(_encoded[1]) : "unspecified"}, ' - 'decoration: ${ _encoded[0] & 0x0004 == 0x0004 ? new TextDecoration._(_encoded[2]) : "unspecified"}, ' - 'decorationColor: ${_encoded[0] & 0x0008 == 0x0008 ? new Color(_encoded[3]) : "unspecified"}, ' - 'decorationStyle: ${_encoded[0] & 0x0010 == 0x0010 ? TextDecorationStyle.values[_encoded[4]] : "unspecified"}, ' - 'fontWeight: ${ _encoded[0] & 0x0020 == 0x0020 ? FontWeight.values[_encoded[5]] : "unspecified"}, ' - 'fontStyle: ${ _encoded[0] & 0x0040 == 0x0040 ? FontStyle.values[_encoded[6]] : "unspecified"}, ' - 'textBaseline: ${ _encoded[0] & 0x0080 == 0x0080 ? TextBaseline.values[_encoded[7]] : "unspecified"}, ' - 'fontFamily: ${ _encoded[0] & 0x0100 == 0x0100 ? _fontFamily : "unspecified"}, ' - 'fontSize: ${ _encoded[0] & 0x0200 == 0x0200 ? _fontSize : "unspecified"}, ' - 'letterSpacing: ${ _encoded[0] & 0x0400 == 0x0400 ? "${_letterSpacing}x" : "unspecified"}, ' - 'wordSpacing: ${ _encoded[0] & 0x0800 == 0x0800 ? "${_wordSpacing}x" : "unspecified"}, ' - 'height: ${ _encoded[0] & 0x1000 == 0x1000 ? "${_height}x" : "unspecified"}, ' - 'locale: ${ _encoded[0] & 0x2000 == 0x2000 ? _locale : "unspecified"}, ' - 'background: ${ _encoded[0] & 0x4000 == 0x4000 ? _background : "unspecified"}, ' - 'foreground: ${ _encoded[0] & 0x8000 == 0x8000 ? _foreground : "unspecified"}' + 'color: ${ _encoded[0] & 0x00002 == 0x00002 ? new Color(_encoded[1]) : "unspecified"}, ' + 'decoration: ${ _encoded[0] & 0x00004 == 0x00004 ? new TextDecoration._(_encoded[2]) : "unspecified"}, ' + 'decorationColor: ${_encoded[0] & 0x00008 == 0x00008 ? new Color(_encoded[3]) : "unspecified"}, ' + 'decorationStyle: ${_encoded[0] & 0x00010 == 0x00010 ? TextDecorationStyle.values[_encoded[4]] : "unspecified"}, ' + 'fontWeight: ${ _encoded[0] & 0x00020 == 0x00020 ? FontWeight.values[_encoded[5]] : "unspecified"}, ' + 'fontStyle: ${ _encoded[0] & 0x00040 == 0x00040 ? FontStyle.values[_encoded[6]] : "unspecified"}, ' + 'textBaseline: ${ _encoded[0] & 0x00080 == 0x00080 ? TextBaseline.values[_encoded[7]] : "unspecified"}, ' + 'fontFamily: ${ _encoded[0] & 0x00100 == 0x00100 ? _fontFamily : "unspecified"}, ' + 'fontSize: ${ _encoded[0] & 0x00200 == 0x00200 ? _fontSize : "unspecified"}, ' + 'letterSpacing: ${ _encoded[0] & 0x00400 == 0x00400 ? "${_letterSpacing}x" : "unspecified"}, ' + 'wordSpacing: ${ _encoded[0] & 0x00800 == 0x00800 ? "${_wordSpacing}x" : "unspecified"}, ' + 'height: ${ _encoded[0] & 0x01000 == 0x01000 ? "${_height}x" : "unspecified"}, ' + 'locale: ${ _encoded[0] & 0x02000 == 0x02000 ? _locale : "unspecified"}, ' + 'background: ${ _encoded[0] & 0x04000 == 0x04000 ? _background : "unspecified"}, ' + 'foreground: ${ _encoded[0] & 0x08000 == 0x08000 ? _foreground : "unspecified"}, ' + 'shadows: ${ _encoded[0] & 0x10000 == 0x10000 ? _shadows : "unspecified"}' ')'; } } @@ -1035,6 +1047,7 @@ class Paragraph extends NativeFieldWrapperClass2 { /// After constructing a [Paragraph], call [Paragraph.layout] on it and then /// paint it with [Canvas.drawParagraph]. class ParagraphBuilder extends NativeFieldWrapperClass2 { + /// Creates a [ParagraphBuilder] object, which is used to create a /// [Paragraph]. @pragma('vm:entry-point') @@ -1044,8 +1057,8 @@ class ParagraphBuilder extends NativeFieldWrapperClass2 { /// Applies the given style to the added text until [pop] is called. /// /// See [pop] for details. - void pushStyle(TextStyle style) => _pushStyle(style._encoded, style._fontFamily, style._fontSize, style._letterSpacing, style._wordSpacing, style._height, _encodeLocale(style._locale), style._background?._objects, style._background?._data, style._foreground?._objects, style._foreground?._data); - void _pushStyle(Int32List encoded, String fontFamily, double fontSize, double letterSpacing, double wordSpacing, double height, String locale, List backgroundObjects, ByteData backgroundData, List foregroundObjects, ByteData foregroundData) native 'ParagraphBuilder_pushStyle'; + void pushStyle(TextStyle style) => _pushStyle(style._encoded, style._fontFamily, style._fontSize, style._letterSpacing, style._wordSpacing, style._height, _encodeLocale(style._locale), style._background?._objects, style._background?._data, style._foreground?._objects, style._foreground?._data, Shadow._encodeShadows(style._shadows)); + void _pushStyle(Int32List encoded, String fontFamily, double fontSize, double letterSpacing, double wordSpacing, double height, String locale, List backgroundObjects, ByteData backgroundData, List foregroundObjects, ByteData foregroundData, ByteData shadowsData) native 'ParagraphBuilder_pushStyle'; static String _encodeLocale(Locale locale) => locale?.toString() ?? ''; diff --git a/lib/ui/text/paragraph_builder.cc b/lib/ui/text/paragraph_builder.cc index 15ce9c8167e0c..ea9f7f5a1f0d0 100644 --- a/lib/ui/text/paragraph_builder.cc +++ b/lib/ui/text/paragraph_builder.cc @@ -6,6 +6,7 @@ #include "flutter/common/settings.h" #include "flutter/common/task_runners.h" +#include "flutter/fml/logging.h" #include "flutter/fml/task_runner.h" #include "flutter/lib/ui/text/font_collection.h" #include "flutter/lib/ui/ui_dart_state.h" @@ -16,10 +17,12 @@ #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" +#include "third_party/skia/include/core/SkColor.h" #include "third_party/tonic/converter/dart_converter.h" #include "third_party/tonic/dart_args.h" #include "third_party/tonic/dart_binding_macros.h" #include "third_party/tonic/dart_library_natives.h" +#include "third_party/tonic/typed_data/dart_byte_data.h" namespace blink { namespace { @@ -41,6 +44,7 @@ const int tsHeightIndex = 12; const int tsLocaleIndex = 13; const int tsBackgroundIndex = 14; const int tsForegroundIndex = 15; +const int tsTextShadowsIndex = 16; const int tsColorMask = 1 << tsColorIndex; const int tsTextDecorationMask = 1 << tsTextDecorationIndex; @@ -57,6 +61,7 @@ const int tsHeightMask = 1 << tsHeightIndex; const int tsLocaleMask = 1 << tsLocaleIndex; const int tsBackgroundMask = 1 << tsBackgroundIndex; const int tsForegroundMask = 1 << tsForegroundIndex; +const int tsTextShadowsMask = 1 << tsTextShadowsIndex; // ParagraphStyle @@ -82,6 +87,16 @@ const int psLineHeightMask = 1 << psLineHeightIndex; const int psEllipsisMask = 1 << psEllipsisIndex; const int psLocaleMask = 1 << psLocaleIndex; +// TextShadows decoding + +constexpr uint32_t kColorDefault = 0xFF000000; +constexpr uint32_t kBytesPerShadow = 16; +constexpr uint32_t kShadowPropertiesCount = 4; +constexpr uint32_t kColorOffset = 0; +constexpr uint32_t kXOffset = 1; +constexpr uint32_t kYOffset = 2; +constexpr uint32_t kBlurOffset = 3; + } // namespace static void ParagraphBuilder_constructor(Dart_NativeArguments args) { @@ -148,13 +163,11 @@ ParagraphBuilder::ParagraphBuilder(tonic::Int32List& encoded, if (mask & psMaxLinesMask) style.max_lines = encoded[psMaxLinesIndex]; - if (mask & psEllipsisMask) { + if (mask & psEllipsisMask) style.ellipsis = ellipsis; - } - if (mask & psLocaleMask) { + if (mask & psLocaleMask) style.locale = locale; - } FontCollection& font_collection = UIDartState::Current()->window()->client()->GetFontCollection(); @@ -164,6 +177,30 @@ ParagraphBuilder::ParagraphBuilder(tonic::Int32List& encoded, ParagraphBuilder::~ParagraphBuilder() = default; +void decodeTextShadows(Dart_Handle shadows_data, + std::vector& decoded_shadows) { + decoded_shadows.clear(); + + tonic::DartByteData byte_data(shadows_data); + FML_CHECK(byte_data.length_in_bytes() % kBytesPerShadow == 0); + + const uint32_t* uint_data = static_cast(byte_data.data()); + const float* float_data = static_cast(byte_data.data()); + + size_t shadow_count = byte_data.length_in_bytes() / kBytesPerShadow; + size_t shadow_count_offset = 0; + for (size_t shadow_index = 0; shadow_index < shadow_count; ++shadow_index) { + shadow_count_offset = shadow_index * kShadowPropertiesCount; + SkColor color = + uint_data[shadow_count_offset + kColorOffset] ^ kColorDefault; + decoded_shadows.emplace_back( + color, + SkPoint::Make(float_data[shadow_count_offset + kXOffset], + float_data[shadow_count_offset + kYOffset]), + float_data[shadow_count_offset + kBlurOffset]); + } +} + void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, const std::string& fontFamily, double fontSize, @@ -174,7 +211,8 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, Dart_Handle background_objects, Dart_Handle background_data, Dart_Handle foreground_objects, - Dart_Handle foreground_data) { + Dart_Handle foreground_data, + Dart_Handle shadows_data) { FML_DCHECK(encoded.num_elements() == 8); int32_t mask = encoded[0]; @@ -183,6 +221,8 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, // explicitly given. txt::TextStyle style = m_paragraphBuilder->PeekStyle(); + // Only change the style property from the previous value if a new explicitly + // set value is available if (mask & tsColorMask) style.color = encoded[tsColorIndex]; @@ -249,6 +289,10 @@ void ParagraphBuilder::pushStyle(tonic::Int32List& encoded, } } + if (mask & tsTextShadowsMask) { + decodeTextShadows(shadows_data, style.text_shadows); + } + m_paragraphBuilder->PushStyle(style); } diff --git a/lib/ui/text/paragraph_builder.h b/lib/ui/text/paragraph_builder.h index 56f12b1de6d14..3e7a8301ddf0b 100644 --- a/lib/ui/text/paragraph_builder.h +++ b/lib/ui/text/paragraph_builder.h @@ -44,7 +44,8 @@ class ParagraphBuilder : public RefCountedDartWrappable { Dart_Handle background_objects, Dart_Handle background_data, Dart_Handle foreground_objects, - Dart_Handle foreground_data); + Dart_Handle foreground_data, + Dart_Handle shadows_data); void pop(); diff --git a/third_party/txt/BUILD.gn b/third_party/txt/BUILD.gn index b31467af544d3..2a9e2c1645428 100644 --- a/third_party/txt/BUILD.gn +++ b/third_party/txt/BUILD.gn @@ -89,6 +89,8 @@ source_set("txt") { "src/txt/text_baseline.h", "src/txt/text_decoration.cc", "src/txt/text_decoration.h", + "src/txt/text_shadow.cc", + "src/txt/text_shadow.h", "src/txt/text_style.cc", "src/txt/text_style.h", "src/txt/typeface_font_asset_provider.cc", diff --git a/third_party/txt/src/txt/paragraph.cc b/third_party/txt/src/txt/paragraph.cc index 227ca63464a7e..34a5a2f9507a9 100644 --- a/third_party/txt/src/txt/paragraph.cc +++ b/third_party/txt/src/txt/paragraph.cc @@ -36,6 +36,7 @@ #include "minikin/MinikinFont.h" #include "third_party/icu/source/common/unicode/ubidi.h" #include "third_party/skia/include/core/SkCanvas.h" +#include "third_party/skia/include/core/SkMaskFilter.h" #include "third_party/skia/include/core/SkPaint.h" #include "third_party/skia/include/core/SkTextBlob.h" #include "third_party/skia/include/core/SkTypeface.h" @@ -729,9 +730,9 @@ void Paragraph::Layout(double width, bool force) { auto update_line_metrics = [&](const SkPaint::FontMetrics& metrics, const TextStyle& style) { double line_spacing = - (line_number == 0) - ? -metrics.fAscent * style.height - : (-metrics.fAscent + metrics.fLeading) * style.height; + (line_number == 0) ? -metrics.fAscent * style.height + : (-metrics.fAscent + metrics.fLeading) * + style.height * paragraph_style_.line_height; if (line_spacing > max_line_spacing) { max_line_spacing = line_spacing; if (line_number == 0) { @@ -886,6 +887,7 @@ void Paragraph::Paint(SkCanvas* canvas, double x, double y) { } SkPoint offset = base_offset + record.offset(); PaintBackground(canvas, record, base_offset); + PaintShadow(canvas, record, offset); canvas->drawTextBlob(record.text(), offset.x(), offset.y(), paint); PaintDecorations(canvas, record, base_offset); } @@ -1065,6 +1067,27 @@ void Paragraph::PaintBackground(SkCanvas* canvas, canvas->drawRect(rect, record.style().background); } +void Paragraph::PaintShadow(SkCanvas* canvas, + const PaintRecord& record, + SkPoint offset) { + if (record.style().text_shadows.size() == 0) + return; + for (TextShadow text_shadow : record.style().text_shadows) { + if (!text_shadow.hasShadow()) { + continue; + } + + SkPaint paint; + paint.setColor(text_shadow.color); + if (text_shadow.blur_radius != 0.0) { + paint.setMaskFilter(SkMaskFilter::MakeBlur( + kNormal_SkBlurStyle, text_shadow.blur_radius, false)); + } + canvas->drawTextBlob(record.text(), offset.x() + text_shadow.offset.x(), + offset.y() + text_shadow.offset.y(), paint); + } +} + std::vector Paragraph::GetRectsForRange( size_t start, size_t end, @@ -1101,7 +1124,8 @@ std::vector Paragraph::GetRectsForRange( SkRect::MakeLTRB(left, top, right, bottom), run.direction); } - // Add empty rectangles representing any newline characters within the range. + // Add empty rectangles representing any newline characters within the + // range. for (size_t line_number = 0; line_number < line_ranges_.size(); ++line_number) { const LineRange& line = line_ranges_[line_number]; diff --git a/third_party/txt/src/txt/paragraph.h b/third_party/txt/src/txt/paragraph.h index 152d6c42c1935..740d3aa7b83b7 100644 --- a/third_party/txt/src/txt/paragraph.h +++ b/third_party/txt/src/txt/paragraph.h @@ -206,6 +206,9 @@ class Paragraph { FRIEND_TEST(ParagraphTest, RepeatLayoutParagraph); FRIEND_TEST(ParagraphTest, Ellipsize); FRIEND_TEST(ParagraphTest, UnderlineShiftParagraph); + FRIEND_TEST(ParagraphTest, SimpleShadow); + FRIEND_TEST(ParagraphTest, ComplexShadow); + FRIEND_TEST(ParagraphTest, LineHeightsParagraph); // Starting data to layout. std::vector text_; @@ -349,6 +352,9 @@ class Paragraph { const PaintRecord& record, SkPoint base_offset); + // Draws the shadows onto the canvas. + void PaintShadow(SkCanvas* canvas, const PaintRecord& record, SkPoint offset); + // Obtain a Minikin font collection matching this text style. std::shared_ptr GetMinikinFontCollectionForStyle( const TextStyle& style); diff --git a/third_party/txt/src/txt/styled_runs.h b/third_party/txt/src/txt/styled_runs.h index 24669e098c2f9..13a84faeef815 100644 --- a/third_party/txt/src/txt/styled_runs.h +++ b/third_party/txt/src/txt/styled_runs.h @@ -79,6 +79,9 @@ class StyledRuns { FRIEND_TEST(ParagraphTest, HyphenBreakParagraph); FRIEND_TEST(ParagraphTest, RepeatLayoutParagraph); FRIEND_TEST(ParagraphTest, Ellipsize); + FRIEND_TEST(ParagraphTest, SimpleShadow); + FRIEND_TEST(ParagraphTest, ComplexShadow); + FRIEND_TEST(ParagraphTest, LineHeightsParagraph); struct IndexedRun { size_t style_index = 0; diff --git a/third_party/txt/src/txt/text_shadow.cc b/third_party/txt/src/txt/text_shadow.cc new file mode 100644 index 0000000000000..f6fb9ffb04e04 --- /dev/null +++ b/third_party/txt/src/txt/text_shadow.cc @@ -0,0 +1,50 @@ +/* + * Copyright 2018 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 "text_shadow.h" +#include "third_party/skia/include/core/SkColor.h" + +namespace txt { + +TextShadow::TextShadow() {} +TextShadow::TextShadow(SkColor color, SkPoint offset, double blur_radius) + : color(color), offset(offset), blur_radius(blur_radius) {} + +bool TextShadow::operator==(const TextShadow& other) const { + if (color != other.color) + return false; + if (offset != other.offset) + return false; + if (blur_radius != other.blur_radius) + return false; + + return true; +} + +bool TextShadow::operator!=(const TextShadow& other) const { + return !(*this == other); +} + +bool TextShadow::hasShadow() const { + if (!offset.isZero()) + return true; + if (blur_radius != 0.0) + return true; + + return false; +} + +} // namespace txt diff --git a/third_party/txt/src/txt/text_shadow.h b/third_party/txt/src/txt/text_shadow.h new file mode 100644 index 0000000000000..7904818668318 --- /dev/null +++ b/third_party/txt/src/txt/text_shadow.h @@ -0,0 +1,44 @@ +/* + * Copyright 2018 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_TEXT_SHADOW_H_ +#define LIB_TXT_SRC_TEXT_SHADOW_H_ + +#include "third_party/skia/include/core/SkColor.h" +#include "third_party/skia/include/core/SkPoint.h" + +namespace txt { + +class TextShadow { + public: + SkColor color = SK_ColorBLACK; + SkPoint offset; + double blur_radius = 0.0; + + TextShadow(); + + TextShadow(SkColor color, SkPoint offset, double blur_radius); + + bool operator==(const TextShadow& other) const; + + bool operator!=(const TextShadow& other) const; + + bool hasShadow() const; +}; + +} // namespace txt + +#endif // LIB_TXT_SRC_TEXT_SHADOW_H_ diff --git a/third_party/txt/src/txt/text_style.cc b/third_party/txt/src/txt/text_style.cc index abf9cde1b9048..b2da694ef6a4c 100644 --- a/third_party/txt/src/txt/text_style.cc +++ b/third_party/txt/src/txt/text_style.cc @@ -51,6 +51,13 @@ bool TextStyle::equals(const TextStyle& other) const { return false; if (foreground != other.foreground) return false; + if (text_shadows.size() != other.text_shadows.size()) + return false; + for (size_t shadow_index = 0; shadow_index < text_shadows.size(); + ++shadow_index) { + if (text_shadows[shadow_index] != other.text_shadows[shadow_index]) + return false; + } return true; } diff --git a/third_party/txt/src/txt/text_style.h b/third_party/txt/src/txt/text_style.h index 6d91b5862ff19..7b0d351e8fa3f 100644 --- a/third_party/txt/src/txt/text_style.h +++ b/third_party/txt/src/txt/text_style.h @@ -18,11 +18,13 @@ #define LIB_TXT_SRC_TEXT_STYLE_H_ #include +#include #include "font_style.h" #include "font_weight.h" #include "text_baseline.h" #include "text_decoration.h" +#include "text_shadow.h" #include "third_party/skia/include/core/SkColor.h" #include "third_party/skia/include/core/SkPaint.h" @@ -51,6 +53,7 @@ class TextStyle { SkPaint background; bool has_foreground = false; SkPaint foreground; + std::vector text_shadows; TextStyle(); diff --git a/third_party/txt/tests/paragraph_unittests.cc b/third_party/txt/tests/paragraph_unittests.cc index 88613b2ec1429..e47a362e3fedb 100644 --- a/third_party/txt/tests/paragraph_unittests.cc +++ b/third_party/txt/tests/paragraph_unittests.cc @@ -1731,4 +1731,264 @@ TEST_F(ParagraphTest, UnderlineShiftParagraph) { } } +TEST_F(ParagraphTest, SimpleShadow) { + const char* text = "Hello World Text Dialog"; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_family = "Roboto"; + text_style.color = SK_ColorBLACK; + text_style.text_shadows.emplace_back(SK_ColorBLACK, SkPoint::Make(2.0, 2.0), + 1.0); + builder.PushStyle(text_style); + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + paragraph->Paint(GetCanvas(), 10.0, 15.0); + + ASSERT_EQ(paragraph->text_.size(), std::string{text}.length()); + for (size_t i = 0; i < u16_text.length(); i++) { + ASSERT_EQ(paragraph->text_[i], u16_text[i]); + } + ASSERT_EQ(paragraph->runs_.runs_.size(), 1ull); + ASSERT_EQ(paragraph->runs_.styles_.size(), 2ull); + ASSERT_TRUE(paragraph->runs_.styles_[1].equals(text_style)); + ASSERT_EQ(paragraph->records_[0].style().color, text_style.color); + + ASSERT_EQ(paragraph->records_[0].style().text_shadows.size(), 1ull); + ASSERT_EQ(paragraph->records_[0].style().text_shadows[0], + text_style.text_shadows[0]); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, ComplexShadow) { + const char* text = "Text Chunk "; + auto icu_text = icu::UnicodeString::fromUTF8(text); + std::u16string u16_text(icu_text.getBuffer(), + icu_text.getBuffer() + icu_text.length()); + + txt::ParagraphStyle paragraph_style; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_family = "Roboto"; + text_style.color = SK_ColorBLACK; + text_style.text_shadows.emplace_back(SK_ColorBLACK, SkPoint::Make(2.0, 2.0), + 1.0); + builder.PushStyle(text_style); + builder.AddText(u16_text); + + text_style.text_shadows.emplace_back(SK_ColorRED, SkPoint::Make(2.0, 2.0), + 5.0); + text_style.text_shadows.emplace_back(SK_ColorGREEN, SkPoint::Make(10.0, -5.0), + 3.0); + builder.PushStyle(text_style); + builder.AddText(u16_text); + + builder.Pop(); + builder.AddText(u16_text); + + text_style.text_shadows.emplace_back(SK_ColorGREEN, SkPoint::Make(0.0, -1.0), + 0.0); + builder.PushStyle(text_style); + builder.AddText(u16_text); + + builder.Pop(); + builder.AddText(u16_text); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth()); + paragraph->Paint(GetCanvas(), 10.0, 15.0); + + ASSERT_EQ(paragraph->text_.size(), std::string{text}.length() * 5); + for (size_t i = 0; i < u16_text.length(); i++) { + ASSERT_EQ(paragraph->text_[i], u16_text[i]); + } + + ASSERT_EQ(paragraph->records_[0].style().text_shadows.size(), 1ull); + ASSERT_EQ(paragraph->records_[1].style().text_shadows.size(), 3ull); + ASSERT_EQ(paragraph->records_[2].style().text_shadows.size(), 1ull); + ASSERT_EQ(paragraph->records_[3].style().text_shadows.size(), 4ull); + ASSERT_EQ(paragraph->records_[4].style().text_shadows.size(), 1ull); + for (size_t i = 0; i < 1; ++i) + ASSERT_EQ(paragraph->records_[0].style().text_shadows[i], + text_style.text_shadows[i]); + for (size_t i = 0; i < 3; ++i) + ASSERT_EQ(paragraph->records_[1].style().text_shadows[i], + text_style.text_shadows[i]); + for (size_t i = 0; i < 1; ++i) + ASSERT_EQ(paragraph->records_[2].style().text_shadows[i], + text_style.text_shadows[i]); + for (size_t i = 0; i < 4; ++i) + ASSERT_EQ(paragraph->records_[3].style().text_shadows[i], + text_style.text_shadows[i]); + for (size_t i = 0; i < 1; ++i) + ASSERT_EQ(paragraph->records_[4].style().text_shadows[i], + text_style.text_shadows[i]); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, DISABLE_ON_WINDOWS(LineHeightsParagraph)) { + const char* text = + "This is a very long sentence to test if the text will properly wrap " + "around and go to the next line. Sometimes, short sentence. Longer " + "sentences are okay too because they are nessecary. Very short. " + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " + "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " + "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " + "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + "mollit anim id est laborum. " + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod " + "tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim " + "veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea " + "commodo consequat. Duis aute irure dolor in reprehenderit in voluptate " + "velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint " + "occaecat cupidatat non proident, sunt in culpa qui officia deserunt " + "mollit anim id est laborum."; + 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; + paragraph_style.text_align = TextAlign::left; + double line_height = 2.0; + paragraph_style.line_height = line_height; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.font_family = "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); + + builder.Pop(); + + auto paragraph = builder.Build(); + paragraph->Layout(GetTestCanvasWidth() - 100); + + paragraph->Paint(GetCanvas(), 0, 0); + + ASSERT_TRUE(Snapshot()); + + ASSERT_EQ(paragraph->text_.size(), std::string{text}.length()); + for (size_t i = 0; i < u16_text.length(); i++) { + ASSERT_EQ(paragraph->text_[i], u16_text[i]); + } + ASSERT_EQ(paragraph->runs_.runs_.size(), 1ull); + ASSERT_EQ(paragraph->runs_.styles_.size(), 2ull); + ASSERT_TRUE(paragraph->runs_.styles_[1].equals(text_style)); + ASSERT_EQ(paragraph->records_.size(), paragraph_style.max_lines); + double expected_y = 24; + + ASSERT_TRUE(paragraph->records_[0].style().equals(text_style)); + ASSERT_DOUBLE_EQ(paragraph->records_[0].offset().y(), expected_y); + expected_y += 27.5 * line_height; + ASSERT_DOUBLE_EQ(paragraph->records_[0].offset().x(), 0); + + ASSERT_TRUE(paragraph->records_[1].style().equals(text_style)); + ASSERT_DOUBLE_EQ(paragraph->records_[1].offset().y(), expected_y); + expected_y += 27.5 * line_height; + ASSERT_DOUBLE_EQ(paragraph->records_[1].offset().x(), 0); + + ASSERT_TRUE(paragraph->records_[2].style().equals(text_style)); + ASSERT_DOUBLE_EQ(paragraph->records_[2].offset().y(), expected_y); + expected_y += 27.5 * line_height; + ASSERT_DOUBLE_EQ(paragraph->records_[2].offset().x(), 0); + + ASSERT_TRUE(paragraph->records_[3].style().equals(text_style)); + ASSERT_DOUBLE_EQ(paragraph->records_[3].offset().y(), expected_y); + expected_y += 27.5 * 10 * line_height; + ASSERT_DOUBLE_EQ(paragraph->records_[3].offset().x(), 0); + + ASSERT_TRUE(paragraph->records_[13].style().equals(text_style)); + ASSERT_DOUBLE_EQ(paragraph->records_[13].offset().y(), expected_y); + ASSERT_DOUBLE_EQ(paragraph->records_[13].offset().x(), 0); + + ASSERT_EQ(paragraph_style.text_align, + paragraph->GetParagraphStyle().text_align); + + // Tests for GetGlyphPositionAtCoordinate() + ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(0, 0).position, 0ull); + ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(1, 1).position, 0ull); + ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(1, 35).position, 68ull); + ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(1, 70).position, 68ull); + ASSERT_EQ(paragraph->GetGlyphPositionAtCoordinate(2000, 35).position, 134ull); + + ASSERT_TRUE(Snapshot()); +} + +TEST_F(ParagraphTest, BaselineParagraph) { + const char* text = + "左線読設Byg後碁給能上目秘使約。満毎冠行来昼本可必図将発確年。今属場育" + "図情闘陰野高備込制詩西校客。審対江置講今固残必託地集済決維駆年策。立得"; + 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; + paragraph_style.text_align = TextAlign::justify; + paragraph_style.line_height = 1.5; + txt::ParagraphBuilder builder(paragraph_style, GetTestFontCollection()); + + txt::TextStyle text_style; + text_style.color = SK_ColorBLACK; + text_style.font_size = 55; + text_style.letter_spacing = 2; + text_style.font_family = "Source Han Serif CN"; + text_style.decoration_style = txt::TextDecorationStyle::kSolid; + text_style.decoration_color = SK_ColorBLACK; + builder.PushStyle(text_style); + + 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); + paint.setColor(SK_ColorRED); + GetCanvas()->drawLine(0, paragraph->GetIdeographicBaseline(), + paragraph->GetMaxWidth(), + paragraph->GetIdeographicBaseline(), paint); + + paint.setColor(SK_ColorGREEN); + + GetCanvas()->drawLine(0, paragraph->GetAlphabeticBaseline(), + paragraph->GetMaxWidth(), + paragraph->GetAlphabeticBaseline(), paint); + ASSERT_DOUBLE_EQ(paragraph->GetIdeographicBaseline(), 79.035003662109375); + ASSERT_DOUBLE_EQ(paragraph->GetAlphabeticBaseline(), 63.305000305175781); + + ASSERT_TRUE(Snapshot()); +} + } // namespace txt