text_style.dart 19.8 KB
Newer Older
1 2 3 4
// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

5
import 'dart:ui' as ui show ParagraphStyle, TextStyle, lerpDouble;
6

7 8
import 'package:flutter/foundation.dart';

9
import 'basic_types.dart';
10

Florian Loitsch's avatar
Florian Loitsch committed
11
/// An immutable style in which paint text.
12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
///
/// ## Sample code
///
/// ### Bold
///
/// Here, a single line of text in a [Text] widget is given a specific style
/// override. The style is mixed with the ambient [DefaultTextStyle] by the
/// [Text] widget.
///
/// ```dart
/// new Text(
///   'No, we need bold strokes. We need this plan.',
///   style: new TextStyle(fontWeight: FontWeight.bold),
/// )
/// ```
///
/// ### Italics
///
/// As in the previous example, the [Text] widget is given a specific style
/// override which is implicitly mixed with the ambient [DefaultTextStyle].
///
/// ```dart
/// new Text(
///   'Welcome to the present, we\'re running a real nation.',
///   style: new TextStyle(fontStyle: FontStyle.italic),
/// )
/// ```
///
/// ### Opacity
///
/// Each line here is progressively more opaque. The base color is
43 44 45 46 47
/// [material.Colors.black], and [Color.withOpacity] is used to create a
/// derivative color with the desired opacity. The root [TextSpan] for this
/// [RichText] widget is explicitly given the ambient [DefaultTextStyle], since
/// [RichText] does not do that automatically. The inner [TextStyle] objects are
/// implicitly mixed with the parent [TextSpan]'s [TextSpan.style].
48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132
///
/// ```dart
/// new RichText(
///   text: new TextSpan(
///     style: DefaultTextStyle.of(context).style,
///     children: <TextSpan>[
///       new TextSpan(
///         text: 'You don\'t have the votes.\n',
///         style: new TextStyle(color: Colors.black.withOpacity(0.6)),
///       ),
///       new TextSpan(
///         text: 'You don\'t have the votes!\n',
///         style: new TextStyle(color: Colors.black.withOpacity(0.8)),
///       ),
///       new TextSpan(
///         text: 'You\'re gonna need congressional approval and you don\'t have the votes!\n',
///         style: new TextStyle(color: Colors.black.withOpacity(1.0)),
///       ),
///     ],
///   ),
/// )
/// ```
///
/// ### Size
///
/// In this example, the ambient [DefaultTextStyle] is explicitly manipulated to
/// obtain a [TextStyle] that doubles the default font size.
///
/// ```dart
/// new Text(
///   'These are wise words, enterprising men quote \'em.',
///   style: DefaultTextStyle.of(context).style.apply(fontSizeFactor: 2.0),
/// )
/// ```
///
/// ### Line height
///
/// The [height] property can be used to change the line height. Here, the line
/// height is set to 100 logical pixels, so that the text is very spaced out.
///
/// ```dart
/// new Text(
///   'Don\'t act surprised, you guys, cuz I wrote \'em!',
///   style: new TextStyle(height: 100.0),
/// )
/// ```
///
/// ### Wavy red underline with black text
///
/// Styles can be combined. In this example, the misspelt word is drawn in black
/// text and underlined with a wavy red line to indicate a spelling error. (The
/// remainder is styled according to the Flutter default text styles, not the
/// ambient [DefaultTextStyle], since no explicit style is given and [RichText]
/// does not automatically use the ambient [DefaultTextStyle].)
///
/// ```dart
/// new RichText(
///   text: new TextSpan(
///     text: 'Don\'t tax the South ',
///     children: <TextSpan>[
///       new TextSpan(
///         text: 'cuz',
///         style: new TextStyle(
///           color: Colors.black,
///           decoration: TextDecoration.underline,
///           decorationColor: Colors.red,
///           decorationStyle: TextDecorationStyle.wavy,
///         ),
///       ),
///       new TextSpan(
///         text: ' we got it made in the shade',
///       ),
///     ],
///   ),
/// )
/// ```
///
/// See also:
///
///  * [Text], the widget for showing text in a single style.
///  * [DefaultTextStyle], the widget that specifies the default text styles for
///    [Text] widgets, configured using a [TextStyle].
///  * [RichText], the widget for showing a paragraph of mix-style text.
///  * [TextSpan], the class that wraps a [TextStyle] for the purposes of
///    passing it to a [RichText].
133
@immutable
134
class TextStyle extends Diagnosticable {
135
  /// Creates a text style.
136
  const TextStyle({
137
    this.inherit: true,
138 139 140 141
    this.color,
    this.fontFamily,
    this.fontSize,
    this.fontWeight,
142
    this.fontStyle,
143
    this.letterSpacing,
144
    this.wordSpacing,
145
    this.textBaseline,
146 147 148
    this.height,
    this.decoration,
    this.decorationColor,
149
    this.decorationStyle,
150
  }) : assert(inherit != null);
151

152 153 154 155 156 157
  /// Whether null values are replaced with their value in an ancestor text
  /// style (e.g., in a [TextSpan] tree).
  ///
  /// If this is false, properties that don't have explicit values will revert
  /// to the defaults: white in color, a font size of 10 pixels, in a sans-serif
  /// font face.
158 159
  final bool inherit;

160
  /// The color to use when painting the text.
161
  final Color color;
162

163
  /// The name of the font to use when painting the text (e.g., Roboto).
164
  final String fontFamily;
165

166
  /// The size of glyphs (in logical pixels) to use when painting the text.
167 168 169 170
  ///
  /// During painting, the [fontSize] is multiplied by the current
  /// `textScaleFactor` to let users make it easier to read text by increasing
  /// its size.
171 172
  final double fontSize;

173
  /// The typeface thickness to use when painting the text (e.g., bold).
174
  final FontWeight fontWeight;
175

176
  /// The typeface variant to use when drawing the letters (e.g., italics).
177
  final FontStyle fontStyle;
178

179
  /// The amount of space (in logical pixels) to add between each letter.
xster's avatar
xster committed
180
  /// A negative value can be used to bring the letters closer.
181 182
  final double letterSpacing;

183 184 185
  /// The amount of space (in logical pixels) to add at each sequence of
  /// white-space (i.e. between each word). A negative value can be used to
  /// bring the words closer.
186 187
  final double wordSpacing;

188 189
  /// The common baseline that should be aligned between this text span and its
  /// parent text span, or, for the root text spans, with the line box.
190
  final TextBaseline textBaseline;
191

192 193 194
  /// The height of this text span, as a multiple of the font size.
  ///
  /// If applied to the root [TextSpan], this value sets the line height, which
195 196
  /// is the minimum distance between subsequent text baselines, as multiple of
  /// the font size.
197 198
  final double height;

199
  /// The decorations to paint near the text (e.g., an underline).
200
  final TextDecoration decoration;
201

202
  /// The color in which to paint the text decorations.
203
  final Color decorationColor;
204

205
  /// The style in which to paint the text decorations (e.g., dashed).
206 207
  final TextDecorationStyle decorationStyle;

208 209
  /// Creates a copy of this text style but with the given fields replaced with
  /// the new values.
210 211 212 213 214
  TextStyle copyWith({
    Color color,
    String fontFamily,
    double fontSize,
    FontWeight fontWeight,
215
    FontStyle fontStyle,
216
    double letterSpacing,
217
    double wordSpacing,
218
    TextBaseline textBaseline,
219
    double height,
220
    TextDecoration decoration,
221
    Color decorationColor,
222
    TextDecorationStyle decorationStyle,
223 224
  }) {
    return new TextStyle(
225
      inherit: inherit,
Ian Hickson's avatar
Ian Hickson committed
226 227 228 229 230 231 232 233 234 235 236
      color: color ?? this.color,
      fontFamily: fontFamily ?? this.fontFamily,
      fontSize: fontSize ?? this.fontSize,
      fontWeight: fontWeight ?? this.fontWeight,
      fontStyle: fontStyle ?? this.fontStyle,
      letterSpacing: letterSpacing ?? this.letterSpacing,
      wordSpacing: wordSpacing ?? this.wordSpacing,
      textBaseline: textBaseline ?? this.textBaseline,
      height: height ?? this.height,
      decoration: decoration ?? this.decoration,
      decorationColor: decorationColor ?? this.decorationColor,
237
      decorationStyle: decorationStyle ?? this.decorationStyle,
238 239 240
    );
  }

Ian Hickson's avatar
Ian Hickson committed
241 242 243
  /// Creates a copy of this text style but with the numeric fields multiplied
  /// by the given factors and then incremented by the given deltas.
  ///
244 245
  /// For example, `style.apply(fontSizeFactor: 2.0, fontSizeDelta: 1.0)` would
  /// return a [TextStyle] whose [fontSize] is `style.fontSize * 2.0 + 1.0`.
Ian Hickson's avatar
Ian Hickson committed
246 247 248 249 250 251 252 253 254 255 256
  ///
  /// For the [fontWeight], the delta is applied to the [FontWeight] enum index
  /// values, so that for instance `style.apply(fontWeightDelta: -2)` when
  /// applied to a `style` whose [fontWeight] is [FontWeight.w500] will return a
  /// [TextStyle] with a [FontWeight.w300].
  ///
  /// The arguments must not be null.
  ///
  /// If the underlying values are null, then the corresponding factors and/or
  /// deltas must not be specified.
  ///
257
  /// The non-numeric fields can be controlled using the corresponding arguments.
Ian Hickson's avatar
Ian Hickson committed
258
  TextStyle apply({
259 260
    Color color,
    String fontFamily,
Ian Hickson's avatar
Ian Hickson committed
261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286
    double fontSizeFactor: 1.0,
    double fontSizeDelta: 0.0,
    int fontWeightDelta: 0,
    double letterSpacingFactor: 1.0,
    double letterSpacingDelta: 0.0,
    double wordSpacingFactor: 1.0,
    double wordSpacingDelta: 0.0,
    double heightFactor: 1.0,
    double heightDelta: 0.0,
  }) {
    assert(fontSizeFactor != null);
    assert(fontSizeDelta != null);
    assert(fontSize != null || (fontSizeFactor == 1.0 && fontSizeDelta == 0.0));
    assert(fontWeightDelta != null);
    assert(fontWeight != null || fontWeightDelta == 0.0);
    assert(letterSpacingFactor != null);
    assert(letterSpacingDelta != null);
    assert(letterSpacing != null || (letterSpacingFactor == 1.0 && letterSpacingDelta == 0.0));
    assert(wordSpacingFactor != null);
    assert(wordSpacingDelta != null);
    assert(wordSpacing != null || (wordSpacingFactor == 1.0 && wordSpacingDelta == 0.0));
    assert(heightFactor != null);
    assert(heightDelta != null);
    assert(heightFactor != null || (heightFactor == 1.0 && heightDelta == 0.0));
    return new TextStyle(
      inherit: inherit,
287 288
      color: color ?? this.color,
      fontFamily: fontFamily ?? this.fontFamily,
Ian Hickson's avatar
Ian Hickson committed
289 290 291 292 293 294 295 296 297 298 299 300 301
      fontSize: fontSize == null ? null : fontSize * fontSizeFactor + fontSizeDelta,
      fontWeight: fontWeight == null ? null : FontWeight.values[(fontWeight.index + fontWeightDelta).clamp(0, FontWeight.values.length - 1)],
      fontStyle: fontStyle,
      letterSpacing: letterSpacing == null ? null : letterSpacing * letterSpacingFactor + letterSpacingDelta,
      wordSpacing: wordSpacing == null ? null : wordSpacing * wordSpacingFactor + wordSpacingDelta,
      textBaseline: textBaseline,
      height: height == null ? null : height * heightFactor + heightDelta,
      decoration: decoration,
      decorationColor: decorationColor,
      decorationStyle: decorationStyle,
    );
  }

302
  /// Returns a new text style that matches this text style but with some values
303 304
  /// replaced by the non-null parameters of the given text style. If the given
  /// text style is null, simply returns this text style.
305
  TextStyle merge(TextStyle other) {
306 307 308
    if (other == null)
      return this;
    assert(other.inherit);
309 310 311 312 313
    return copyWith(
      color: other.color,
      fontFamily: other.fontFamily,
      fontSize: other.fontSize,
      fontWeight: other.fontWeight,
314
      fontStyle: other.fontStyle,
315
      letterSpacing: other.letterSpacing,
316
      wordSpacing: other.wordSpacing,
317
      textBaseline: other.textBaseline,
318 319 320 321 322 323 324
      height: other.height,
      decoration: other.decoration,
      decorationColor: other.decorationColor,
      decorationStyle: other.decorationStyle
    );
  }

325 326 327 328 329 330 331 332 333 334
  /// Interpolate between two text styles.
  ///
  /// This will not work well if the styles don't set the same fields.
  static TextStyle lerp(TextStyle begin, TextStyle end, double t) {
    assert(begin.inherit == end.inherit);
    return new TextStyle(
      inherit: end.inherit,
      color: Color.lerp(begin.color, end.color, t),
      fontFamily: t < 0.5 ? begin.fontFamily : end.fontFamily,
      fontSize: ui.lerpDouble(begin.fontSize ?? end.fontSize, end.fontSize ?? begin.fontSize, t),
Ian Hickson's avatar
Ian Hickson committed
335
      fontWeight: FontWeight.lerp(begin.fontWeight, end.fontWeight, t),
336 337 338 339 340 341 342 343 344 345
      fontStyle: t < 0.5 ? begin.fontStyle : end.fontStyle,
      letterSpacing: ui.lerpDouble(begin.letterSpacing ?? end.letterSpacing, end.letterSpacing ?? begin.letterSpacing, t),
      wordSpacing: ui.lerpDouble(begin.wordSpacing ?? end.wordSpacing, end.wordSpacing ?? begin.wordSpacing, t),
      textBaseline: t < 0.5 ? begin.textBaseline : end.textBaseline,
      height: ui.lerpDouble(begin.height ?? end.height, end.height ?? begin.height, t),
      decoration: t < 0.5 ? begin.decoration : end.decoration,
      decorationColor: Color.lerp(begin.decorationColor, end.decorationColor, t),
      decorationStyle: t < 0.5 ? begin.decorationStyle : end.decorationStyle
    );
  }
346

347
  /// The style information for text runs, encoded for use by `dart:ui`.
348
  ui.TextStyle getTextStyle({ double textScaleFactor: 1.0 }) {
349 350 351 352 353 354 355
    return new ui.TextStyle(
      color: color,
      decoration: decoration,
      decorationColor: decorationColor,
      decorationStyle: decorationStyle,
      fontWeight: fontWeight,
      fontStyle: fontStyle,
356
      textBaseline: textBaseline,
357
      fontFamily: fontFamily,
358
      fontSize: fontSize == null ? null : fontSize * textScaleFactor,
359 360
      letterSpacing: letterSpacing,
      wordSpacing: wordSpacing,
Adam Barth's avatar
Adam Barth committed
361
      height: height
362 363 364
    );
  }

365
  /// The style information for paragraphs, encoded for use by `dart:ui`.
366 367 368 369
  ///
  /// The `textScaleFactor` argument must not be null. If omitted, it defaults
  /// to 1.0. The other arguments may be null. The `maxLines` argument, if
  /// specified and non-null, must be greater than zero.
370 371 372 373
  ui.ParagraphStyle getParagraphStyle({
      TextAlign textAlign,
      double textScaleFactor: 1.0,
      String ellipsis,
374
      int maxLines,
375 376 377
  }) {
    assert(textScaleFactor != null);
    assert(maxLines == null || maxLines > 0);
378
    return new ui.ParagraphStyle(
379
      textAlign: textAlign,
Adam Barth's avatar
Adam Barth committed
380 381 382
      fontWeight: fontWeight,
      fontStyle: fontStyle,
      fontFamily: fontFamily,
383
      fontSize: fontSize == null ? null : fontSize * textScaleFactor,
384
      lineHeight: height,
385
      maxLines: maxLines,
386
      ellipsis: ellipsis,
387 388
    );
  }
389

390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416
  /// Describe the difference between this style and another, in terms of how
  /// much damage it will make to the rendering.
  ///
  /// See also:
  ///
  ///  * [TextSpan.compareTo], which does the same thing for entire [TextSpan]s.
  RenderComparison compareTo(TextStyle other) {
    if (identical(this, other))
      return RenderComparison.identical;
    if (inherit != other.inherit ||
        fontFamily != other.fontFamily ||
        fontSize != other.fontSize ||
        fontWeight != other.fontWeight ||
        fontStyle != other.fontStyle ||
        letterSpacing != other.letterSpacing ||
        wordSpacing != other.wordSpacing ||
        textBaseline != other.textBaseline ||
        height != other.height)
      return RenderComparison.layout;
    if (color != other.color ||
        decoration != other.decoration ||
        decorationColor != other.decorationColor ||
        decorationStyle != other.decorationStyle)
      return RenderComparison.paint;
    return RenderComparison.identical;
  }

417
  @override
Hixie's avatar
Hixie committed
418
  bool operator ==(dynamic other) {
419 420
    if (identical(this, other))
      return true;
Hixie's avatar
Hixie committed
421 422 423
    if (other is! TextStyle)
      return false;
    final TextStyle typedOther = other;
424 425
    return inherit == typedOther.inherit &&
           color == typedOther.color &&
Hixie's avatar
Hixie committed
426 427 428 429
           fontFamily == typedOther.fontFamily &&
           fontSize == typedOther.fontSize &&
           fontWeight == typedOther.fontWeight &&
           fontStyle == typedOther.fontStyle &&
430
           letterSpacing == typedOther.letterSpacing &&
431
           wordSpacing == typedOther.wordSpacing &&
Hixie's avatar
Hixie committed
432
           textBaseline == typedOther.textBaseline &&
433
           height == typedOther.height &&
Hixie's avatar
Hixie committed
434 435 436
           decoration == typedOther.decoration &&
           decorationColor == typedOther.decorationColor &&
           decorationStyle == typedOther.decorationStyle;
437 438
  }

439
  @override
440
  int get hashCode {
441
    return hashValues(
442
      inherit,
443 444 445 446 447 448
      color,
      fontFamily,
      fontSize,
      fontWeight,
      fontStyle,
      letterSpacing,
449
      wordSpacing,
450
      textBaseline,
451
      height,
452 453 454 455
      decoration,
      decorationColor,
      decorationStyle
    );
456 457
  }

458
  @override
459
  String toStringShort() => '$runtimeType';
460 461

  /// Adds all properties prefixing property names with the optional `prefix`.
462 463 464
  @override
  void debugFillProperties(DiagnosticPropertiesBuilder properties, { String prefix: '' }) {
    super.debugFillProperties(properties);
465 466 467 468 469
    final List<DiagnosticsNode> styles = <DiagnosticsNode>[];
    styles.add(new DiagnosticsProperty<Color>('${prefix}color', color, defaultValue: null));
    styles.add(new StringProperty('${prefix}family', fontFamily, defaultValue: null, quoted: false));
    styles.add(new DoubleProperty('${prefix}size', fontSize, defaultValue: null));
    String weightDescription;
Hixie's avatar
Hixie committed
470 471 472
    if (fontWeight != null) {
      switch (fontWeight) {
        case FontWeight.w100:
473
          weightDescription = '100';
Hixie's avatar
Hixie committed
474 475
          break;
        case FontWeight.w200:
476
          weightDescription = '200';
Hixie's avatar
Hixie committed
477 478
          break;
        case FontWeight.w300:
479
          weightDescription = '300';
Hixie's avatar
Hixie committed
480 481
          break;
        case FontWeight.w400:
482
          weightDescription = '400';
Hixie's avatar
Hixie committed
483 484
          break;
        case FontWeight.w500:
485
          weightDescription = '500';
Hixie's avatar
Hixie committed
486 487
          break;
        case FontWeight.w600:
488
          weightDescription = '600';
Hixie's avatar
Hixie committed
489 490
          break;
        case FontWeight.w700:
491
          weightDescription = '700';
Hixie's avatar
Hixie committed
492 493
          break;
        case FontWeight.w800:
494
          weightDescription = '800';
Hixie's avatar
Hixie committed
495 496
          break;
        case FontWeight.w900:
497
          weightDescription = '900';
Hixie's avatar
Hixie committed
498 499 500
          break;
      }
    }
501 502 503 504 505 506 507 508 509 510 511 512 513 514
    // TODO(jacobr): switch this to use enumProperty which will either cause the
    // weight description to change to w600 from 600 or require existing
    // enumProperty to handle this special case.
    styles.add(new DiagnosticsProperty<FontWeight>(
      '${prefix}weight',
      fontWeight,
      description: weightDescription,
      defaultValue: null,
    ));
    styles.add(new EnumProperty<FontStyle>('${prefix}style', fontStyle, defaultValue: null));
    styles.add(new DoubleProperty('${prefix}letterSpacing', letterSpacing, unit: 'x', defaultValue: null));
    styles.add(new DoubleProperty('${prefix}wordSpacing', wordSpacing, unit: 'x', defaultValue: null));
    styles.add(new EnumProperty<TextBaseline>('${prefix}baseline', textBaseline, defaultValue: null));
    styles.add(new DoubleProperty('${prefix}height', height, unit: 'x', defaultValue: null));
Hixie's avatar
Hixie committed
515
    if (decoration != null || decorationColor != null || decorationStyle != null) {
516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534
      final List<String> decorationDescription = <String>[];
      if (decorationStyle != null)
        decorationDescription.add(describeEnum((decorationStyle)));

      // Hide decorationColor from the default text view as it is shown in the
      // terse decoration summary as well.
      styles.add(new DiagnosticsProperty<Color>('${prefix}decorationColor', decorationColor, defaultValue: null, hidden: true));

      if (decorationColor != null)
        decorationDescription.add('$decorationColor');

      // Intentionally collide with the property 'decoration' added below.
      // Tools that show hidden properties could choose the first property
      // matching the name to disambiguate.
      styles.add(new DiagnosticsProperty<TextDecoration>('${prefix}decoration', decoration, defaultValue: null, hidden: true));
      if (decoration != null)
        decorationDescription.add('$decoration');
      assert(decorationDescription.isNotEmpty);
      styles.add(new MessageProperty('${prefix}decoration', decorationDescription.join(' ')));
Hixie's avatar
Hixie committed
535
    }
536 537 538

    final bool styleSpecified = styles.any((DiagnosticsNode n) => !n.hidden);
    properties.add(new DiagnosticsProperty<bool>('${prefix}inherit', inherit, hidden: !styleSpecified && inherit));
539 540 541
    for (DiagnosticsNode style in styles)
      properties.add(style);

542 543
    if (!styleSpecified)
      properties.add(new FlagProperty('inherit', value: inherit, ifTrue: '$prefix<all styles inherited>', ifFalse: '$prefix<no style specified>'));
544 545
  }
}