未验证 提交 5f529566 编写于 作者: G Gary Qian 提交者: GitHub

Add support for text shadows (#6385)

Text shadows can now be added to a TextStyle by providing a List<Shadow>. The dart:ui Shadow class is now the base class to flutter BoxShadow.
上级 e44c10c9
......@@ -1023,6 +1023,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
......
......@@ -3639,6 +3639,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().
// <https://github.com/google/skia/blob/bb5b77db51d2e149ee66db284903572a5aac09be/src/effects/SkBlurMask.cpp#L23>
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<double>], 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<Shadow> lerpList(List<Shadow> a, List<Shadow> b, double t) {
assert(t != null);
if (a == null && b == null)
return null;
a ??= <Shadow>[];
b ??= <Shadow>[];
final List<Shadow> result = <Shadow>[];
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<Shadow> a, List<Shadow> 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<Shadow> 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<T> = void Function(T result);
......
......@@ -259,6 +259,7 @@ Int32List _encodeTextStyle(
Locale locale,
Paint background,
Paint foreground,
List<Shadow> 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<Shadow> 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<Shadow> _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<dynamic> backgroundObjects, ByteData backgroundData, List<dynamic> 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<dynamic> backgroundObjects, ByteData backgroundData, List<dynamic> foregroundObjects, ByteData foregroundData, ByteData shadowsData) native 'ParagraphBuilder_pushStyle';
static String _encodeLocale(Locale locale) => locale?.toString() ?? '';
......
......@@ -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<txt::TextShadow>& 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<const uint32_t*>(byte_data.data());
const float* float_data = static_cast<const float*>(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);
}
......
......@@ -44,7 +44,8 @@ class ParagraphBuilder : public RefCountedDartWrappable<ParagraphBuilder> {
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();
......
......@@ -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",
......
......@@ -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::TextBox> Paragraph::GetRectsForRange(
size_t start,
size_t end,
......@@ -1101,7 +1124,8 @@ std::vector<Paragraph::TextBox> 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];
......
......@@ -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<uint16_t> 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<minikin::FontCollection> GetMinikinFontCollectionForStyle(
const TextStyle& style);
......
......@@ -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;
......
/*
* 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
/*
* 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_
......@@ -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;
}
......
......@@ -18,11 +18,13 @@
#define LIB_TXT_SRC_TEXT_STYLE_H_
#include <string>
#include <vector>
#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<TextShadow> text_shadows;
TextStyle();
......
......@@ -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
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册