blob: a076c6bcd8fe48035fcf0ffdf0df1a65ae363264 [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.
part of ui;
enum FontStyle {
normal,
italic,
}
enum PlaceholderAlignment {
baseline,
aboveBaseline,
belowBaseline,
top,
bottom,
middle,
}
class FontWeight {
const FontWeight._(this.index);
final int index;
static const FontWeight w100 = FontWeight._(0);
static const FontWeight w200 = FontWeight._(1);
static const FontWeight w300 = FontWeight._(2);
static const FontWeight w400 = FontWeight._(3);
static const FontWeight w500 = FontWeight._(4);
static const FontWeight w600 = FontWeight._(5);
static const FontWeight w700 = FontWeight._(6);
static const FontWeight w800 = FontWeight._(7);
static const FontWeight w900 = FontWeight._(8);
static const FontWeight normal = w400;
static const FontWeight bold = w700;
static const List<FontWeight> values = <FontWeight>[
w100,
w200,
w300,
w400,
w500,
w600,
w700,
w800,
w900
];
static FontWeight? lerp(FontWeight? a, FontWeight? b, double t) {
assert(t != null); // ignore: unnecessary_null_comparison
if (a == null && b == null) {
return null;
}
return values[engine.clampInt(
lerpDouble(a?.index ?? normal.index, b?.index ?? normal.index, t)!
.round(),
0,
8,
)];
}
@override
String toString() {
return const <int, String>{
0: 'FontWeight.w100',
1: 'FontWeight.w200',
2: 'FontWeight.w300',
3: 'FontWeight.w400',
4: 'FontWeight.w500',
5: 'FontWeight.w600',
6: 'FontWeight.w700',
7: 'FontWeight.w800',
8: 'FontWeight.w900',
}[index]!;
}
}
class FontFeature {
const FontFeature(this.feature, [this.value = 1])
: assert(feature != null), // ignore: unnecessary_null_comparison
assert(feature.length == 4,
'Feature tag must be exactly four characters long.'),
assert(value != null), // ignore: unnecessary_null_comparison
assert(value >= 0, 'Feature value must be zero or a positive integer.');
const FontFeature.enable(String feature) : this(feature, 1);
const FontFeature.disable(String feature) : this(feature, 0);
const FontFeature.alternative(this.value) : feature = 'aalt';
const FontFeature.alternativeFractions()
: feature = 'afrc',
value = 1;
const FontFeature.contextualAlternates()
: feature = 'calt',
value = 1;
const FontFeature.caseSensitiveForms()
: feature = 'case',
value = 1;
factory FontFeature.characterVariant(int value) {
assert(value >= 1);
assert(value <= 20);
return FontFeature('cv${value.toString().padLeft(2, "0")}');
}
const FontFeature.denominator()
: feature = 'dnom',
value = 1;
const FontFeature.fractions()
: feature = 'frac',
value = 1;
const FontFeature.historicalForms()
: feature = 'hist',
value = 1;
const FontFeature.historicalLigatures()
: feature = 'hlig',
value = 1;
const FontFeature.liningFigures()
: feature = 'lnum',
value = 1;
const FontFeature.localeAware({bool enable = true})
: feature = 'locl',
value = enable ? 1 : 0;
const FontFeature.notationalForms([this.value = 1])
: feature = 'nalt',
assert(value >= 0);
const FontFeature.numerators()
: feature = 'numr',
value = 1;
const FontFeature.oldstyleFigures()
: feature = 'onum',
value = 1;
const FontFeature.ordinalForms()
: feature = 'ordn',
value = 1;
const FontFeature.proportionalFigures()
: feature = 'pnum',
value = 1;
const FontFeature.randomize()
: feature = 'rand',
value = 1;
const FontFeature.stylisticAlternates()
: feature = 'salt',
value = 1;
const FontFeature.scientificInferiors()
: feature = 'sinf',
value = 1;
factory FontFeature.stylisticSet(int value) {
assert(value >= 1);
assert(value <= 20);
return FontFeature('ss${value.toString().padLeft(2, "0")}');
}
const FontFeature.subscripts()
: feature = 'subs',
value = 1;
const FontFeature.superscripts()
: feature = 'sups',
value = 1;
const FontFeature.swash([this.value = 1])
: feature = 'swsh',
assert(value >= 0);
const FontFeature.tabularFigures()
: feature = 'tnum',
value = 1;
const FontFeature.slashedZero()
: feature = 'zero',
value = 1;
final String feature;
final int value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is FontFeature &&
other.feature == feature &&
other.value == value;
}
@override
int get hashCode => hashValues(feature, value);
@override
String toString() => "FontFeature('$feature', $value)";
}
// The order of this enum must match the order of the values in RenderStyleConstants.h's ETextAlign.
enum TextAlign {
left,
right,
center,
justify,
start,
end,
}
enum TextBaseline {
alphabetic,
ideographic,
}
class TextDecoration {
const TextDecoration._(this._mask);
factory TextDecoration.combine(List<TextDecoration> decorations) {
int mask = 0;
for (final TextDecoration decoration in decorations) {
mask |= decoration._mask;
}
return TextDecoration._(mask);
}
final int _mask;
bool contains(TextDecoration other) {
return (_mask | other._mask) == _mask;
}
static const TextDecoration none = TextDecoration._(0x0);
static const TextDecoration underline = TextDecoration._(0x1);
static const TextDecoration overline = TextDecoration._(0x2);
static const TextDecoration lineThrough = TextDecoration._(0x4);
@override
bool operator ==(Object other) {
return other is TextDecoration && other._mask == _mask;
}
@override
int get hashCode => _mask.hashCode;
@override
String toString() {
if (_mask == 0) {
return 'TextDecoration.none';
}
final List<String> values = <String>[];
if (_mask & underline._mask != 0) {
values.add('underline');
}
if (_mask & overline._mask != 0) {
values.add('overline');
}
if (_mask & lineThrough._mask != 0) {
values.add('lineThrough');
}
if (values.length == 1) {
return 'TextDecoration.${values[0]}';
}
return 'TextDecoration.combine([${values.join(", ")}])';
}
}
enum TextDecorationStyle { solid, double, dotted, dashed, wavy }
enum TextLeadingDistribution {
proportional,
even,
}
class TextHeightBehavior {
const TextHeightBehavior({
this.applyHeightToFirstAscent = true,
this.applyHeightToLastDescent = true,
this.leadingDistribution = TextLeadingDistribution.proportional,
});
final bool applyHeightToFirstAscent;
final bool applyHeightToLastDescent;
final TextLeadingDistribution leadingDistribution;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is TextHeightBehavior &&
other.applyHeightToFirstAscent == applyHeightToFirstAscent &&
other.applyHeightToLastDescent == applyHeightToLastDescent &&
other.leadingDistribution == leadingDistribution;
}
@override
int get hashCode {
return hashValues(
applyHeightToFirstAscent,
applyHeightToLastDescent,
);
}
@override
String toString() {
return 'TextHeightBehavior('
'applyHeightToFirstAscent: $applyHeightToFirstAscent, '
'applyHeightToLastDescent: $applyHeightToLastDescent, '
'leadingDistribution: $leadingDistribution'
')';
}
}
abstract class TextStyle {
factory TextStyle({
Color? color,
TextDecoration? decoration,
Color? decorationColor,
TextDecorationStyle? decorationStyle,
double? decorationThickness,
FontWeight? fontWeight,
FontStyle? fontStyle,
TextBaseline? textBaseline,
String? fontFamily,
List<String>? fontFamilyFallback,
double? fontSize,
double? letterSpacing,
double? wordSpacing,
double? height,
TextLeadingDistribution? leadingDistribution,
Locale? locale,
Paint? background,
Paint? foreground,
List<Shadow>? shadows,
List<FontFeature>? fontFeatures,
}) {
if (engine.useCanvasKit) {
return engine.CkTextStyle(
color: color,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
fontWeight: fontWeight,
fontStyle: fontStyle,
textBaseline: textBaseline,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
fontSize: fontSize,
letterSpacing: letterSpacing,
wordSpacing: wordSpacing,
height: height,
leadingDistribution: leadingDistribution,
locale: locale,
background: background as engine.CkPaint?,
foreground: foreground as engine.CkPaint?,
shadows: shadows,
fontFeatures: fontFeatures,
);
} else {
return engine.EngineTextStyle(
color: color,
decoration: decoration,
decorationColor: decorationColor,
decorationStyle: decorationStyle,
decorationThickness: decorationThickness,
fontWeight: fontWeight,
fontStyle: fontStyle,
textBaseline: textBaseline,
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
fontSize: fontSize,
letterSpacing: letterSpacing,
wordSpacing: wordSpacing,
height: height,
locale: locale,
background: background,
foreground: foreground,
shadows: shadows,
fontFeatures: fontFeatures,
);
}
}
}
abstract class ParagraphStyle {
// See: https://github.com/flutter/flutter/issues/9819
factory ParagraphStyle({
TextAlign? textAlign,
TextDirection? textDirection,
int? maxLines,
String? fontFamily,
double? fontSize,
double? height,
TextHeightBehavior? textHeightBehavior,
FontWeight? fontWeight,
FontStyle? fontStyle,
StrutStyle? strutStyle,
String? ellipsis,
Locale? locale,
}) {
if (engine.useCanvasKit) {
return engine.CkParagraphStyle(
textAlign: textAlign,
textDirection: textDirection,
maxLines: maxLines,
fontFamily: fontFamily,
fontSize: fontSize,
height: height,
textHeightBehavior: textHeightBehavior,
fontWeight: fontWeight,
fontStyle: fontStyle,
strutStyle: strutStyle,
ellipsis: ellipsis,
locale: locale,
);
} else {
return engine.EngineParagraphStyle(
textAlign: textAlign,
textDirection: textDirection,
maxLines: maxLines,
fontFamily: fontFamily,
fontSize: fontSize,
height: height,
textHeightBehavior: textHeightBehavior,
fontWeight: fontWeight,
fontStyle: fontStyle,
strutStyle: strutStyle,
ellipsis: ellipsis,
locale: locale,
);
}
}
}
abstract class StrutStyle {
/// Creates a new StrutStyle object.
///
/// * `fontFamily`: The name of the font to use when painting the text (e.g.,
/// Roboto).
///
/// * `fontFamilyFallback`: An ordered list of font family names that will be searched for when
/// the font in `fontFamily` cannot be found.
///
/// * `fontSize`: The size of glyphs (in logical pixels) to use when painting
/// the text.
///
/// * `lineHeight`: The minimum height of the line boxes, as a multiple of the
/// font size. The lines of the paragraph will be at least
/// `(lineHeight + leading) * fontSize` tall when fontSize
/// is not null. When fontSize is null, there is no minimum line height. Tall
/// glyphs due to baseline alignment or large [TextStyle.fontSize] may cause
/// the actual line height after layout to be taller than specified here.
/// [fontSize] must be provided for this property to take effect.
///
/// * `leading`: The minimum amount of leading between lines as a multiple of
/// the font size. [fontSize] must be provided for this property to take effect.
///
/// * `fontWeight`: The typeface thickness to use when painting the text
/// (e.g., bold).
///
/// * `fontStyle`: The typeface variant to use when drawing the letters (e.g.,
/// italics).
///
/// * `forceStrutHeight`: When true, the paragraph will force all lines to be exactly
/// `(lineHeight + leading) * fontSize` tall from baseline to baseline.
/// [TextStyle] is no longer able to influence the line height, and any tall
/// glyphs may overlap with lines above. If a [fontFamily] is specified, the
/// total ascent of the first line will be the min of the `Ascent + half-leading`
/// of the [fontFamily] and `(lineHeight + leading) * fontSize`. Otherwise, it
/// will be determined by the Ascent + half-leading of the first text.
factory StrutStyle({
String? fontFamily,
List<String>? fontFamilyFallback,
double? fontSize,
double? height,
TextLeadingDistribution? leadingDistribution,
double? leading,
FontWeight? fontWeight,
FontStyle? fontStyle,
bool? forceStrutHeight,
}) {
if (engine.useCanvasKit) {
return engine.CkStrutStyle(
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
fontSize: fontSize,
height: height,
leadingDistribution: leadingDistribution,
leading: leading,
fontWeight: fontWeight,
fontStyle: fontStyle,
forceStrutHeight: forceStrutHeight,
);
} else {
return engine.EngineStrutStyle(
fontFamily: fontFamily,
fontFamilyFallback: fontFamilyFallback,
fontSize: fontSize,
height: height,
leadingDistribution: leadingDistribution,
leading: leading,
fontWeight: fontWeight,
fontStyle: fontStyle,
forceStrutHeight: forceStrutHeight,
);
}
}
}
// The order of this enum must match the order of the values in TextDirection.h's TextDirection.
enum TextDirection {
rtl,
ltr,
}
class TextBox {
const TextBox.fromLTRBD(
this.left,
this.top,
this.right,
this.bottom,
this.direction,
);
final double left;
final double top;
final double right;
final double bottom;
final TextDirection direction;
Rect toRect() => Rect.fromLTRB(left, top, right, bottom);
double get start {
return (direction == TextDirection.ltr) ? left : right;
}
double get end {
return (direction == TextDirection.ltr) ? right : left;
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
if (other.runtimeType != runtimeType) {
return false;
}
return other is TextBox &&
other.left == left &&
other.top == top &&
other.right == right &&
other.bottom == bottom &&
other.direction == direction;
}
@override
int get hashCode => hashValues(left, top, right, bottom, direction);
@override
String toString() {
return 'TextBox.fromLTRBD(${left.toStringAsFixed(1)}, ${top.toStringAsFixed(1)}, ${right.toStringAsFixed(1)}, ${bottom.toStringAsFixed(1)}, $direction)';
}
}
enum TextAffinity {
upstream,
downstream,
}
class TextPosition {
const TextPosition({
required this.offset,
this.affinity = TextAffinity.downstream,
}) : assert(offset != null), // ignore: unnecessary_null_comparison
assert(affinity != null); // ignore: unnecessary_null_comparison
final int offset;
final TextAffinity affinity;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is TextPosition &&
other.offset == offset &&
other.affinity == affinity;
}
@override
int get hashCode => hashValues(offset, affinity);
@override
String toString() {
return '$runtimeType(offset: $offset, affinity: $affinity)';
}
}
class TextRange {
const TextRange({
required this.start,
required this.end,
}) : assert(start >= -1),
assert(end >= -1);
const TextRange.collapsed(int offset)
: assert(offset >= -1),
start = offset,
end = offset;
static const TextRange empty = TextRange(start: -1, end: -1);
final int start;
final int end;
bool get isValid => start >= 0 && end >= 0;
bool get isCollapsed => start == end;
bool get isNormalized => end >= start;
String textBefore(String text) {
assert(isNormalized);
return text.substring(0, start);
}
String textAfter(String text) {
assert(isNormalized);
return text.substring(end);
}
String textInside(String text) {
assert(isNormalized);
return text.substring(start, end);
}
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is TextRange && other.start == start && other.end == end;
}
@override
int get hashCode => hashValues(
start.hashCode,
end.hashCode,
);
@override
String toString() => 'TextRange(start: $start, end: $end)';
}
class ParagraphConstraints {
const ParagraphConstraints({
required this.width,
}) : assert(width != null); // ignore: unnecessary_null_comparison
final double width;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is ParagraphConstraints && other.width == width;
}
@override
int get hashCode => width.hashCode;
@override
String toString() => '$runtimeType(width: $width)';
}
enum BoxHeightStyle {
tight,
max,
includeLineSpacingMiddle,
includeLineSpacingTop,
includeLineSpacingBottom,
strut,
}
enum BoxWidthStyle {
// Provide tight bounding boxes that fit widths to the runs of each line
// independently.
tight,
max,
}
abstract class LineMetrics {
factory LineMetrics({
required bool hardBreak,
required double ascent,
required double descent,
required double unscaledAscent,
required double height,
required double width,
required double left,
required double baseline,
required int lineNumber,
}) = engine.EngineLineMetrics;
bool get hardBreak;
double get ascent;
double get descent;
double get unscaledAscent;
double get height;
double get width;
double get left;
double get baseline;
int get lineNumber;
}
abstract class Paragraph {
double get width;
double get height;
double get longestLine;
double get minIntrinsicWidth;
double get maxIntrinsicWidth;
double get alphabeticBaseline;
double get ideographicBaseline;
bool get didExceedMaxLines;
void layout(ParagraphConstraints constraints);
List<TextBox> getBoxesForRange(int start, int end,
{BoxHeightStyle boxHeightStyle = BoxHeightStyle.tight,
BoxWidthStyle boxWidthStyle = BoxWidthStyle.tight});
TextPosition getPositionForOffset(Offset offset);
TextRange getWordBoundary(TextPosition position);
TextRange getLineBoundary(TextPosition position);
List<TextBox> getBoxesForPlaceholders();
List<LineMetrics> computeLineMetrics();
}
abstract class ParagraphBuilder {
factory ParagraphBuilder(ParagraphStyle style) {
if (engine.useCanvasKit) {
return engine.CkParagraphBuilder(style);
}
return engine.CanvasParagraphBuilder(style as engine.EngineParagraphStyle);
}
void pushStyle(TextStyle style);
void pop();
void addText(String text);
Paragraph build();
int get placeholderCount;
List<double> get placeholderScales;
void addPlaceholder(
double width,
double height,
PlaceholderAlignment alignment, {
double scale = 1.0,
double? baselineOffset,
TextBaseline? baseline,
});
}
Future<void> loadFontFromList(Uint8List list, {String? fontFamily}) {
if (engine.useCanvasKit) {
return engine.skiaFontCollection
.loadFontFromList(list, fontFamily: fontFamily)
.then((_) => engine.sendFontChangeMessage());
} else {
return _fontCollection!
.loadFontFromList(list, fontFamily: fontFamily!)
.then((_) => engine.sendFontChangeMessage());
}
}