Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
211 changes: 211 additions & 0 deletions lib/ui/painting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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().
// <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);

Expand Down
49 changes: 31 additions & 18 deletions lib/ui/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ Int32List _encodeTextStyle(
Locale locale,
Paint background,
Paint foreground,
List<Shadow> shadows
) {
final Int32List result = new Int32List(8);
if (color != null) {
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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".'
Expand All @@ -379,6 +385,7 @@ class TextStyle {
locale,
background,
foreground,
shadows,
),
_fontFamily = fontFamily ?? '',
_fontSize = fontSize,
Expand All @@ -387,7 +394,8 @@ class TextStyle {
_height = height,
_locale = locale,
_background = background,
_foreground = foreground;
_foreground = foreground,
_shadows = shadows;

final Int32List _encoded;
final String _fontFamily;
Expand All @@ -398,6 +406,7 @@ class TextStyle {
final Locale _locale;
final Paint _background;
final Paint _foreground;
final List<Shadow> _shadows;

@override
bool operator ==(dynamic other) {
Expand All @@ -419,6 +428,8 @@ class TextStyle {
if (_encoded[index] != typedOther._encoded[index])
return false;
}
if (!Shadow._shadowsListEquals(_shadows, typedOther._shadows))
return false;
return true;
}

Expand All @@ -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"}'
')';
}
}
Expand Down Expand Up @@ -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')
Expand All @@ -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() ?? '';

Expand Down
Loading