blob: a033f78f2e521e85cb02d2fcaf0321fa41ebc71a [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.10
part of engine;
class CkParagraphStyle implements ui.ParagraphStyle {
CkParagraphStyle({
ui.TextAlign? textAlign,
ui.TextDirection? textDirection,
int? maxLines,
String? fontFamily,
double? fontSize,
double? height,
ui.TextHeightBehavior? textHeightBehavior,
ui.FontWeight? fontWeight,
ui.FontStyle? fontStyle,
ui.StrutStyle? strutStyle,
String? ellipsis,
ui.Locale? locale,
}) : skParagraphStyle = toSkParagraphStyle(
textAlign,
textDirection,
maxLines,
fontFamily,
fontSize,
height,
textHeightBehavior,
fontWeight,
fontStyle,
strutStyle,
ellipsis,
locale,
) {
_textDirection = textDirection ?? ui.TextDirection.ltr;
_fontFamily = fontFamily;
_fontSize = fontSize;
_fontWeight = fontWeight;
_fontStyle = fontStyle;
}
SkParagraphStyle skParagraphStyle;
ui.TextDirection? _textDirection;
String? _fontFamily;
double? _fontSize;
ui.FontWeight? _fontWeight;
ui.FontStyle? _fontStyle;
static SkTextStyleProperties toSkTextStyleProperties(
String? fontFamily,
double? fontSize,
ui.FontWeight? fontWeight,
ui.FontStyle? fontStyle,
) {
final SkTextStyleProperties skTextStyle = SkTextStyleProperties();
if (fontWeight != null || fontStyle != null) {
skTextStyle.fontStyle = toSkFontStyle(fontWeight, fontStyle);
}
if (fontSize != null) {
skTextStyle.fontSize = fontSize;
}
if (fontFamily == null ||
!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
fontFamily = 'Roboto';
}
skTextStyle.fontFamilies = [fontFamily];
return skTextStyle;
}
static SkStrutStyleProperties toSkStrutStyleProperties(ui.StrutStyle value) {
EngineStrutStyle style = value as EngineStrutStyle;
final SkStrutStyleProperties skStrutStyle = SkStrutStyleProperties();
if (style._fontFamily != null) {
final List<String> fontFamilies = <String>[style._fontFamily!];
if (style._fontFamilyFallback != null) {
fontFamilies.addAll(style._fontFamilyFallback!);
}
skStrutStyle.fontFamilies = fontFamilies;
} else {
// If no strut font family is given, default to Roboto.
skStrutStyle.fontFamilies = ['Roboto'];
}
if (style._fontSize != null) {
skStrutStyle.fontSize = style._fontSize;
}
if (style._height != null) {
skStrutStyle.heightMultiplier = style._height;
}
if (style._leading != null) {
skStrutStyle.leading = style._leading;
}
if (style._fontWeight != null || style._fontStyle != null) {
skStrutStyle.fontStyle =
toSkFontStyle(style._fontWeight, style._fontStyle);
}
if (style._forceStrutHeight != null) {
skStrutStyle.forceStrutHeight = style._forceStrutHeight;
}
skStrutStyle.strutEnabled = true;
return skStrutStyle;
}
static SkParagraphStyle toSkParagraphStyle(
ui.TextAlign? textAlign,
ui.TextDirection? textDirection,
int? maxLines,
String? fontFamily,
double? fontSize,
double? height,
ui.TextHeightBehavior? textHeightBehavior,
ui.FontWeight? fontWeight,
ui.FontStyle? fontStyle,
ui.StrutStyle? strutStyle,
String? ellipsis,
ui.Locale? locale,
) {
final SkParagraphStyleProperties properties = SkParagraphStyleProperties();
if (textAlign != null) {
properties.textAlign = toSkTextAlign(textAlign);
}
if (textDirection != null) {
properties.textDirection = toSkTextDirection(textDirection);
}
if (maxLines != null) {
properties.maxLines = maxLines;
}
if (height != null) {
properties.heightMultiplier = height;
}
if (textHeightBehavior != null) {
properties.textHeightBehavior = textHeightBehavior.encode();
}
if (ellipsis != null) {
properties.ellipsis = ellipsis;
}
if (strutStyle != null) {
properties.strutStyle = toSkStrutStyleProperties(strutStyle);
}
properties.textStyle =
toSkTextStyleProperties(fontFamily, fontSize, fontWeight, fontStyle);
return canvasKit.ParagraphStyle(properties);
}
CkTextStyle getTextStyle() {
return CkTextStyle(
fontFamily: _fontFamily,
fontSize: _fontSize,
fontWeight: _fontWeight,
fontStyle: _fontStyle,
);
}
}
class CkTextStyle implements ui.TextStyle {
SkTextStyle skTextStyle;
ui.Color? color;
ui.TextDecoration? decoration;
ui.Color? decorationColor;
ui.TextDecorationStyle? decorationStyle;
double? decorationThickness;
ui.FontWeight? fontWeight;
ui.FontStyle? fontStyle;
ui.TextBaseline? textBaseline;
String? fontFamily;
List<String>? fontFamilyFallback;
double? fontSize;
double? letterSpacing;
double? wordSpacing;
double? height;
ui.Locale? locale;
CkPaint? background;
CkPaint? foreground;
List<ui.Shadow>? shadows;
List<ui.FontFeature>? fontFeatures;
factory CkTextStyle({
ui.Color? color,
ui.TextDecoration? decoration,
ui.Color? decorationColor,
ui.TextDecorationStyle? decorationStyle,
double? decorationThickness,
ui.FontWeight? fontWeight,
ui.FontStyle? fontStyle,
ui.TextBaseline? textBaseline,
String? fontFamily,
List<String>? fontFamilyFallback,
double? fontSize,
double? letterSpacing,
double? wordSpacing,
double? height,
ui.Locale? locale,
CkPaint? background,
CkPaint? foreground,
List<ui.Shadow>? shadows,
List<ui.FontFeature>? fontFeatures,
}) {
final SkTextStyleProperties properties = SkTextStyleProperties();
if (background != null) {
properties.backgroundColor = makeFreshSkColor(background.color);
}
if (color != null) {
properties.color = makeFreshSkColor(color);
}
if (decoration != null) {
int decorationValue = canvasKit.NoDecoration;
if (decoration.contains(ui.TextDecoration.underline)) {
decorationValue |= canvasKit.UnderlineDecoration;
}
if (decoration.contains(ui.TextDecoration.overline)) {
decorationValue |= canvasKit.OverlineDecoration;
}
if (decoration.contains(ui.TextDecoration.lineThrough)) {
decorationValue |= canvasKit.LineThroughDecoration;
}
properties.decoration = decorationValue;
}
if (decorationThickness != null) {
properties.decorationThickness = decorationThickness;
}
if (decorationColor != null) {
properties.decorationColor = makeFreshSkColor(decorationColor);
}
if (decorationStyle != null) {
properties.decorationStyle = toSkTextDecorationStyle(decorationStyle);
}
if (textBaseline != null) {
properties.textBaseline = toSkTextBaseline(textBaseline);
}
if (fontSize != null) {
properties.fontSize = fontSize;
}
if (letterSpacing != null) {
properties.letterSpacing = letterSpacing;
}
if (wordSpacing != null) {
properties.wordSpacing = wordSpacing;
}
if (height != null) {
properties.heightMultiplier = height;
}
if (locale != null) {
properties.locale = locale.toLanguageTag();
}
if (fontFamily == null ||
!skiaFontCollection.registeredFamilies.contains(fontFamily)) {
fontFamily = 'Roboto';
}
List<String> fontFamilies = <String>[fontFamily];
if (fontFamilyFallback != null &&
!fontFamilyFallback.every((font) => fontFamily == font)) {
fontFamilies.addAll(fontFamilyFallback);
}
properties.fontFamilies = fontFamilies;
if (fontWeight != null || fontStyle != null) {
properties.fontStyle = toSkFontStyle(fontWeight, fontStyle);
}
if (foreground != null) {
properties.foregroundColor = makeFreshSkColor(foreground.color);
}
if (shadows != null) {
List<SkTextShadow> ckShadows = <SkTextShadow>[];
for (ui.Shadow shadow in shadows) {
final ckShadow = SkTextShadow();
ckShadow.color = makeFreshSkColor(shadow.color);
ckShadow.offset = toSkPoint(shadow.offset);
ckShadow.blurRadius = shadow.blurRadius;
ckShadows.add(ckShadow);
}
properties.shadows = ckShadows;
}
if (fontFeatures != null) {
List<SkFontFeature> ckFontFeatures = <SkFontFeature>[];
for (ui.FontFeature fontFeature in fontFeatures) {
SkFontFeature ckFontFeature = SkFontFeature();
ckFontFeature.name = fontFeature.feature;
ckFontFeature.value = fontFeature.value;
ckFontFeatures.add(ckFontFeature);
}
properties.fontFeatures = ckFontFeatures;
}
return CkTextStyle._(
canvasKit.TextStyle(properties),
color,
decoration,
decorationColor,
decorationStyle,
decorationThickness,
fontWeight,
fontStyle,
textBaseline,
fontFamily,
fontFamilyFallback,
fontSize,
letterSpacing,
wordSpacing,
height,
locale,
background,
foreground,
shadows,
fontFeatures,
);
}
/// Merges this text style with [other] and returns the new text style.
///
/// The values in this text style are used unless [other] specifically
/// overrides it.
CkTextStyle mergeWith(CkTextStyle other) {
return CkTextStyle(
color: other.color ?? color,
decoration: other.decoration ?? decoration,
decorationColor: other.decorationColor ?? decorationColor,
decorationStyle: other.decorationStyle ?? decorationStyle,
decorationThickness: other.decorationThickness ?? decorationThickness,
fontWeight: other.fontWeight ?? fontWeight,
fontStyle: other.fontStyle ?? fontStyle,
textBaseline: other.textBaseline ?? textBaseline,
fontFamily: other.fontFamily ?? fontFamily,
fontFamilyFallback: other.fontFamilyFallback ?? fontFamilyFallback,
fontSize: other.fontSize ?? fontSize,
letterSpacing: other.letterSpacing ?? letterSpacing,
wordSpacing: other.wordSpacing ?? wordSpacing,
height: other.height ?? height,
locale: other.locale ?? locale,
background: other.background ?? background,
foreground: other.foreground ?? foreground,
shadows: other.shadows ?? shadows,
fontFeatures: other.fontFeatures ?? fontFeatures,
);
}
CkTextStyle._(
this.skTextStyle,
this.color,
this.decoration,
this.decorationColor,
this.decorationStyle,
this.decorationThickness,
this.fontWeight,
this.fontStyle,
this.textBaseline,
this.fontFamily,
this.fontFamilyFallback,
this.fontSize,
this.letterSpacing,
this.wordSpacing,
this.height,
this.locale,
this.background,
this.foreground,
this.shadows,
this.fontFeatures,
);
}
SkFontStyle toSkFontStyle(ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) {
final style = SkFontStyle();
if (fontWeight != null) {
style.weight = toSkFontWeight(fontWeight);
}
if (fontStyle != null) {
style.slant = toSkFontSlant(fontStyle);
}
return style;
}
class CkParagraph extends ManagedSkiaObject<SkParagraph>
implements ui.Paragraph {
CkParagraph(
this._initialParagraph, this._paragraphStyle, this._paragraphCommands);
/// The result of calling `build()` on the JS CkParagraphBuilder.
///
/// This may be invalidated later.
final SkParagraph _initialParagraph;
/// The paragraph style used to build this paragraph.
///
/// This is used to resurrect the paragraph if the initial paragraph
/// is deleted.
final CkParagraphStyle _paragraphStyle;
/// The paragraph builder commands used to build this paragraph.
///
/// This is used to resurrect the paragraph if the initial paragraph
/// is deleted.
final List<_ParagraphCommand> _paragraphCommands;
/// The constraints from the last time we layed the paragraph out.
///
/// This is used to resurrect the paragraph if the initial paragraph
/// is deleted.
ui.ParagraphConstraints? _lastLayoutConstraints;
@override
SkParagraph createDefault() => _initialParagraph;
@override
SkParagraph resurrect() {
final builder = CkParagraphBuilder(_paragraphStyle);
for (_ParagraphCommand command in _paragraphCommands) {
switch (command.type) {
case _ParagraphCommandType.addText:
builder.addText(command.text!);
break;
case _ParagraphCommandType.pop:
builder.pop();
break;
case _ParagraphCommandType.pushStyle:
builder.pushStyle(command.style!);
break;
case _ParagraphCommandType.addPlaceholder:
builder._addPlaceholder(command.placeholderStyle!);
break;
}
}
final SkParagraph result = builder._buildCkParagraph();
if (_lastLayoutConstraints != null) {
// We need to set the Skia object early so layout works.
rawSkiaObject = result;
this.layout(_lastLayoutConstraints!);
}
return result;
}
@override
void delete() {
rawSkiaObject?.delete();
}
@override
bool get isResurrectionExpensive => true;
@override
double get alphabeticBaseline => skiaObject.getAlphabeticBaseline();
@override
bool get didExceedMaxLines => skiaObject.didExceedMaxLines();
@override
double get height => skiaObject.getHeight();
@override
double get ideographicBaseline => skiaObject.getIdeographicBaseline();
@override
double get longestLine => skiaObject.getLongestLine();
@override
double get maxIntrinsicWidth => skiaObject.getMaxIntrinsicWidth();
@override
double get minIntrinsicWidth => skiaObject.getMinIntrinsicWidth();
@override
double get width => skiaObject.getMaxWidth();
@override
List<ui.TextBox> getBoxesForPlaceholders() {
List<List<double>> skRects = skiaObject.getRectsForPlaceholders();
return skRectsToTextBoxes(skRects);
}
@override
List<ui.TextBox> getBoxesForRange(
int start,
int end, {
ui.BoxHeightStyle boxHeightStyle: ui.BoxHeightStyle.tight,
ui.BoxWidthStyle boxWidthStyle: ui.BoxWidthStyle.tight,
}) {
if (start < 0 || end < 0) {
return const <ui.TextBox>[];
}
List<List<double>> skRects = skiaObject.getRectsForRange(
start,
end,
toSkRectHeightStyle(boxHeightStyle),
toSkRectWidthStyle(boxWidthStyle),
);
return skRectsToTextBoxes(skRects);
}
List<ui.TextBox> skRectsToTextBoxes(List<List<double>> skRects) {
List<ui.TextBox> result = <ui.TextBox>[];
for (int i = 0; i < skRects.length; i++) {
final List<double> rect = skRects[i];
result.add(ui.TextBox.fromLTRBD(
rect[0],
rect[1],
rect[2],
rect[3],
_paragraphStyle._textDirection!,
));
}
return result;
}
@override
ui.TextPosition getPositionForOffset(ui.Offset offset) {
final SkTextPosition positionWithAffinity =
skiaObject.getGlyphPositionAtCoordinate(
offset.dx,
offset.dy,
);
return fromPositionWithAffinity(positionWithAffinity);
}
@override
ui.TextRange getWordBoundary(ui.TextPosition position) {
final SkTextRange skRange = skiaObject.getWordBoundary(position.offset);
return ui.TextRange(start: skRange.start, end: skRange.end);
}
@override
void layout(ui.ParagraphConstraints constraints) {
assert(constraints.width != null); // ignore: unnecessary_null_comparison
_lastLayoutConstraints = constraints;
// Infinite width breaks layout, just use a very large number instead.
// TODO(het): Remove this once https://bugs.chromium.org/p/skia/issues/detail?id=9874
// is fixed.
double width;
const double largeFiniteWidth = 1000000;
if (constraints.width.isInfinite) {
width = largeFiniteWidth;
} else {
width = constraints.width;
}
// TODO(het): CanvasKit throws an exception when laid out with
// a font that wasn't registered.
try {
skiaObject.layout(width);
} catch (e) {
html.window.console.warn('CanvasKit threw an exception while laying '
'out the paragraph. The font was "${_paragraphStyle._fontFamily}". '
'Exception:\n$e');
rethrow;
}
}
@override
ui.TextRange getLineBoundary(ui.TextPosition position) {
// TODO(hterkelsen): Implement this when it's added to CanvasKit
throw UnimplementedError('getLineBoundary');
}
@override
List<ui.LineMetrics> computeLineMetrics() {
// TODO(hterkelsen): Implement this when it's added to CanvasKit
throw UnimplementedError('computeLineMetrics');
}
}
class CkParagraphBuilder implements ui.ParagraphBuilder {
final SkParagraphBuilder _paragraphBuilder;
final CkParagraphStyle _style;
final List<_ParagraphCommand> _commands;
int _placeholderCount;
final List<double> _placeholderScales;
final List<CkTextStyle> _styleStack;
CkParagraphBuilder(ui.ParagraphStyle style)
: _commands = <_ParagraphCommand>[],
_style = style as CkParagraphStyle,
_placeholderCount = 0,
_placeholderScales = <double>[],
_styleStack = <CkTextStyle>[],
_paragraphBuilder = canvasKit.ParagraphBuilder.MakeFromFontProvider(
style.skParagraphStyle,
skiaFontCollection.fontProvider,
);
@override
void addPlaceholder(
double width,
double height,
ui.PlaceholderAlignment alignment, {
double scale = 1.0,
double? baselineOffset,
ui.TextBaseline? baseline,
}) {
// Require a baseline to be specified if using a baseline-based alignment.
assert((alignment == ui.PlaceholderAlignment.aboveBaseline ||
alignment == ui.PlaceholderAlignment.belowBaseline ||
alignment == ui.PlaceholderAlignment.baseline)
? baseline != null
: true);
_placeholderCount++;
_placeholderScales.add(scale);
SkPlaceholderStyleProperties placeholderStyle = toSkPlaceholderStyle(
width * scale,
height * scale,
alignment,
(baselineOffset ?? height) * scale,
baseline ?? ui.TextBaseline.alphabetic,
);
_addPlaceholder(placeholderStyle);
}
void _addPlaceholder(SkPlaceholderStyleProperties placeholderStyle) {
_commands.add(_ParagraphCommand.addPlaceholder(placeholderStyle));
_paragraphBuilder.addPlaceholder(placeholderStyle);
}
static SkPlaceholderStyleProperties toSkPlaceholderStyle(
double width,
double height,
ui.PlaceholderAlignment alignment,
double baselineOffset,
ui.TextBaseline baseline,
) {
final properties = SkPlaceholderStyleProperties();
properties.width = width;
properties.height = height;
properties.alignment = toSkPlaceholderAlignment(alignment);
properties.offset = baselineOffset;
properties.baseline = toSkTextBaseline(baseline);
return properties;
}
@override
void addText(String text) {
_commands.add(_ParagraphCommand.addText(text));
_paragraphBuilder.addText(text);
}
@override
ui.Paragraph build() {
final builtParagraph = _buildCkParagraph();
return CkParagraph(builtParagraph, _style, _commands);
}
/// Builds the CkParagraph with the builder and deletes the builder.
SkParagraph _buildCkParagraph() {
final SkParagraph result = _paragraphBuilder.build();
_paragraphBuilder.delete();
return result;
}
@override
int get placeholderCount => _placeholderCount;
@override
List<double> get placeholderScales => _placeholderScales;
@override
void pop() {
_commands.add(const _ParagraphCommand.pop());
_styleStack.removeLast();
_paragraphBuilder.pop();
}
CkTextStyle _peekStyle() =>
_styleStack.isEmpty ? _style.getTextStyle() : _styleStack.last;
@override
void pushStyle(ui.TextStyle style) {
final CkTextStyle baseStyle = _peekStyle();
final CkTextStyle ckStyle = style as CkTextStyle;
final CkTextStyle skStyle = baseStyle.mergeWith(ckStyle);
_styleStack.add(skStyle);
_commands.add(_ParagraphCommand.pushStyle(ckStyle));
if (skStyle.foreground != null || skStyle.background != null) {
final SkPaint foreground = skStyle.foreground?.skiaObject ?? SkPaint();
final SkPaint background = skStyle.background?.skiaObject ?? SkPaint();
_paragraphBuilder.pushPaintStyle(
skStyle.skTextStyle, foreground, background);
} else {
_paragraphBuilder.pushStyle(skStyle.skTextStyle);
}
}
}
class _ParagraphCommand {
final _ParagraphCommandType type;
final String? text;
final CkTextStyle? style;
final SkPlaceholderStyleProperties? placeholderStyle;
const _ParagraphCommand._(
this.type,
this.text,
this.style,
this.placeholderStyle,
);
const _ParagraphCommand.addText(String text)
: this._(_ParagraphCommandType.addText, text, null, null);
const _ParagraphCommand.pop()
: this._(_ParagraphCommandType.pop, null, null, null);
const _ParagraphCommand.pushStyle(CkTextStyle style)
: this._(_ParagraphCommandType.pushStyle, null, style, null);
const _ParagraphCommand.addPlaceholder(
SkPlaceholderStyleProperties placeholderStyle)
: this._(
_ParagraphCommandType.addPlaceholder, null, null, placeholderStyle);
}
enum _ParagraphCommandType {
addText,
pop,
pushStyle,
addPlaceholder,
}