blob: ef3d98d14b7046000570b0e91fb163dfa94da4da [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;
// This constant must be consistent with `kTextHeightNone` defined in
// flutter/lib/ui/text.dart.
// To change the sentinel value, search for "kTextHeightNone" in the source code.
const double kTextHeightNone = 0.0;
enum FontStyle { normal, italic }
enum PlaceholderAlignment { baseline, aboveBaseline, belowBaseline, top, bottom, middle }
class FontWeight {
const FontWeight(this.value)
: assert(value >= 1, 'Font weight must be between 1 and 1000'),
assert(value <= 1000, 'Font weight must be between 1 and 1000');
final int value;
int get index => (value ~/ 100 - 1).clamp(0, 8);
static const FontWeight w100 = FontWeight(100);
static const FontWeight w200 = FontWeight(200);
static const FontWeight w300 = FontWeight(300);
static const FontWeight w400 = FontWeight(400);
static const FontWeight w500 = FontWeight(500);
static const FontWeight w600 = FontWeight(600);
static const FontWeight w700 = FontWeight(700);
static const FontWeight w800 = FontWeight(800);
static const FontWeight w900 = FontWeight(900);
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,
];
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is FontWeight && other.value == value;
}
@override
int get hashCode => value;
static FontWeight? lerp(FontWeight? a, FontWeight? b, double t) {
if (a == null && b == null) {
return null;
}
return FontWeight(
lerpDouble(a?.value ?? normal.value, b?.value ?? normal.value, t)!.round().clamp(100, 900),
);
}
@override
String toString() {
if (value % 100 != 0) {
return 'FontWeight($value)';
}
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.length == 4, 'Feature tag must be exactly four characters long.'),
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 => Object.hash(feature, value);
@override
String toString() => "FontFeature('$feature', $value)";
}
class FontVariation {
const FontVariation(this.axis, this.value)
: assert(axis.length == 4, 'Axis tag must be exactly four characters long.'),
assert(
value >= -32768.0 && value < 32768.0,
'Value must be representable as a signed 16.16 fixed-point number, i.e. it must be in this range: -32768.0 ≤ value < 32768.0',
);
const FontVariation.italic(this.value)
: assert(value >= 0.0),
assert(value <= 1.0),
axis = 'ital';
const FontVariation.opticalSize(this.value) : assert(value > 0.0), axis = 'opsz';
const FontVariation.slant(this.value)
: assert(value > -90.0),
assert(value < 90.0),
axis = 'slnt';
const FontVariation.width(this.value) : assert(value >= 0.0), axis = 'wdth';
const FontVariation.weight(this.value) : assert(value >= 1), assert(value <= 1000), axis = 'wght';
final String axis;
final double value;
@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}
return other is FontVariation && other.axis == axis && other.value == value;
}
@override
int get hashCode => Object.hash(axis, value);
static FontVariation? lerp(FontVariation? a, FontVariation? b, double t) {
if (a?.axis != b?.axis || (a == null && b == null)) {
return t < 0.5 ? a : b;
}
return FontVariation(
a!.axis,
clampDouble(lerpDouble(a.value, b!.value, t)!, -32768.0, 32768.0 - 1.0 / 65536.0),
);
}
@override
String toString() => "FontVariation('$axis', $value)";
}
final class GlyphInfo {
GlyphInfo(
this.graphemeClusterLayoutBounds,
this.graphemeClusterCodeUnitRange,
this.writingDirection,
);
final Rect graphemeClusterLayoutBounds;
final TextRange graphemeClusterCodeUnitRange;
final TextDirection writingDirection;
@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is GlyphInfo &&
graphemeClusterLayoutBounds == other.graphemeClusterLayoutBounds &&
graphemeClusterCodeUnitRange == other.graphemeClusterCodeUnitRange &&
writingDirection == other.writingDirection;
}
@override
int get hashCode =>
Object.hash(graphemeClusterLayoutBounds, graphemeClusterCodeUnitRange, writingDirection);
@override
String toString() =>
'Glyph($graphemeClusterLayoutBounds, textRange: $graphemeClusterCodeUnitRange, direction: $writingDirection)';
}
// 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) {
var mask = 0;
for (final decoration in decorations) {
mask |= decoration._mask;
}
return TextDecoration._(mask);
}
final int _mask;
int get maskValue => _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 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 Object.hash(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,
List<FontVariation>? fontVariations,
}) => engine.renderer.createTextStyle(
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,
foreground: foreground,
shadows: shadows,
fontFeatures: fontFeatures,
fontVariations: fontVariations,
);
}
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,
}) => engine.renderer.createParagraphStyle(
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,
}) => engine.renderer.createStrutStyle(
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 => Object.hash(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});
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 => Object.hash(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 => Object.hash(start.hashCode, end.hashCode);
@override
String toString() => 'TextRange(start: $start, end: $end)';
}
class ParagraphConstraints {
const ParagraphConstraints({required this.width});
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.renderer.createLineMetrics(
hardBreak: hardBreak,
ascent: ascent,
descent: descent,
unscaledAscent: unscaledAscent,
height: height,
width: width,
left: left,
baseline: baseline,
lineNumber: lineNumber,
);
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);
GlyphInfo? getGlyphInfoAt(int codeUnitOffset);
GlyphInfo? getClosestGlyphInfoForOffset(Offset offset);
TextRange getWordBoundary(TextPosition position);
TextRange getLineBoundary(TextPosition position);
List<TextBox> getBoxesForPlaceholders();
List<LineMetrics> computeLineMetrics();
LineMetrics? getLineMetricsAt(int lineNumber);
int get numberOfLines;
int? getLineNumberAt(int codeUnitOffset);
void dispose();
bool get debugDisposed;
}
abstract class ParagraphBuilder {
factory ParagraphBuilder(ParagraphStyle style) => engine.renderer.createParagraphBuilder(style);
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}) async {
await engine.renderer.fontCollection.loadFontFromList(list, fontFamily: fontFamily);
engine.sendFontChangeMessage();
}