# The Breakdown: Flutter Text

## Why this series?

While there is quite a lot of content on usage and customisations for Flutter Widgets, there is relatively little content about how the Widget actually achieves the functionality it is designed for. Even a fairly simple Widget has a lot of simplification under the hood even excluding its main purpose. This series breaks down various Widgets to their fundamental level which usually goes down to the canvas level.

## Understanding the function of Text

```dart
Text(
  "Hello World!",
  style: TextStyle(
    fontSize: 24.0,
  ),
)
```

The `Text` Widget is probably the simplest Widget in Flutter to explain to a beginner - it is primarily to render some text. However, the simple act of painting the text on the screen is only a fraction of what the Widget does. Primarily, `Text` isn't just a UI component, it's a *Flutter Widget*. What I am implying here is that - not only does using `Text` create text on the screen, but it also integrates the app preferences into the Widget such as text direction, text styles, semantics, and more.

But here's the catch: the `Text` Widget is not responsible for rendering text, it is responsible for integrating aspects of the Flutter app into the text displayed. This is the reason that the `Text` Widget does not expect a `TextStyle` or other parameters to be provided. It inherits the default text style from the `BuildContext` and builds the text accordingly. As you go deeper into how the Widget paints the text on the screen, you will find more components that are more and more *pure* - meaning they do not inherit any information from the Flutter context and rather need all the information to be passed down to them.

Here is the simplified code for the `Text` Widget build method. As you can see, most of the method incorporates existing styles, adds a mouse region, and adds semantics. The rest of the method delegates the rendering to a `RichText` Widget.

```dart
  // The build() method of the Text Widget
  Widget build(BuildContext context) {

    // Set the effective text style
    final defaultTextStyle = DefaultTextStyle.of(context);
    TextStyle? effectiveTextStyle = style;
    if (style == null || style!.inherit) {
      effectiveTextStyle = defaultTextStyle.style.merge(style);
    }
    if (MediaQuery.boldTextOverride(context)) {
      effectiveTextStyle = effectiveTextStyle!.merge(const TextStyle(fontWeight: FontWeight.bold));
    }
    final registrar = SelectionContainer.maybeOf(context);

    // Parameters passed down to RichText
    Widget result = RichText(...);

    // Create a mouse region
    if (registrar != null) {
      result = MouseRegion(...);
    }

    // Set any semantics for the text
    if (semanticsLabel != null) {
      result = Semantics(...);
    }
    return result;
  }
```

Underneath the Text Widget itself is another Widget: `RichText`.

## Under the hood: RichText

The `RichText` Widget is used to display formatted text with varying styles within a single paragraph or sentence. It allows developers to create complex text layouts with different fonts, sizes, colours, and styles.

The `RichText` Widget accepts a `TextSpan` object to define the text and styles that are displayed on the screen. The `TextSpan` class allows developers to set various text formatting options such as font family, size, weight, style, colour, and background colour. It also allows for text decorations such as underlining and strikethrough. Additionally, `TextSpan` can also contain child `TextSpan` objects that allow nested text with different styles and formatting for each span.

Here's an example of how to use the `RichText` Widget to display some simple text with different styles:

```dart
RichText(
  text: TextSpan(
    text: 'Hello ',
    style: TextStyle(
      fontSize: 20.0,
      fontWeight: FontWeight.bold,
      color: Colors.black,
    ),
    children: <TextSpan>[
      TextSpan(
        text: 'World',
        style: TextStyle(
          fontSize: 24.0,
          fontWeight: FontWeight.normal,
          color: Colors.blue,
        ),
      ),
      TextSpan(
        text: '!',
        style: TextStyle(
          fontSize: 20.0,
          fontWeight: FontWeight.bold,
          color: Colors.black,
        ),
      ),
    ],
  ),
),
```

The Widget is made by extending `MultiChildRenderObjectWidget` with the multiple children here being the text spans provided to it. The `RichText` Widget gets most of its properties from the parameters and gets far fewer properties from Flutter. The only ones it inherits are text direction and locale.

Here is a simplified version of the `createRenderObject()` method from `RichText`:

```dart
@override
RenderParagraph createRenderObject(BuildContext context) {
  return RenderParagraph(text,
    textAlign: textAlign,
    // Defaults to inherited directionality
    textDirection: textDirection ?? Directionality.of(context),
    softWrap: softWrap,
    overflow: overflow,
    textScaleFactor: textScaleFactor,
    maxLines: maxLines,
    strutStyle: strutStyle,
    textWidthBasis: textWidthBasis,
    textHeightBehavior: textHeightBehavior,
    // Defaults to inherited locale
    locale: locale ?? Localizations.maybeLocaleOf(context),
    registrar: selectionRegistrar,
    selectionColor: selectionColor,
  );
}
```

The `RichText` Widget creates a `RenderObject` named `RenderParagraph` which gets us closer to finally rendering the text on the screen.

## Diving deeper: RenderParagraph

The `RenderParagraph` class is a `RenderObject` responsible for rendering a paragraph of text on the screen. It handles text layout, line breaking, and text selection features. It is used as the rendering component behind the `Text` and `TextField` Widget in Flutter. `RenderParagraph` provides text selection features, allowing the user to select a range of text with a mouse or touch gesture.

`RenderParagraph` is focused on rendering text, meaning it does not inherit any Flutter information and needs all parameters defined - either by users or as defaults. Since it is a `RenderObject`, it deals with layout of the text. However, while usual `RenderObjects` would only deal with the box size, the `RenderParagraph` also has to deal with the internal layout of the text to be displayed - taking into account the line height, text direction, wrapping, and more.

There are various methods that deal with this layout procedure. The following are some methods that deal with sizing the `RenderBox`:

```dart
@override
double computeMinIntrinsicWidth(double height) {...}

@override
double computeMaxIntrinsicWidth(double height) {...}

@override
double computeMinIntrinsicHeight(double width) {...}

@override
double computeMaxIntrinsicHeight(double width) {...}

@override
double computeDistanceToActualBaseline(TextBaseline baseline) {...}

void _computeChildrenWidthWithMaxIntrinsics(double height) {...}

void _computeChildrenWidthWithMinIntrinsics(double height) {...}

void _computeChildrenHeightWithMinIntrinsics(double width) {...}
```

For the text layout, an important method is the `performLayout()` method which handles text overflow and clipping.

Here is a simplified version of the method:

```dart
@override
void performLayout() {
  // Calculate size of placeholders
  final BoxConstraints constraints = this.constraints;
  _placeholderDimensions = _layoutChildren(constraints);
  _layoutTextWithConstraints(constraints);
  _setParentData();

  final Size textSize = _textPainter.size;
  final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
  size = constraints.constrain(textSize);

  // Check if text overflows any boundary
  final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
  final bool didOverflowWidth = size.width < textSize.width;
  final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;

  if (hasVisualOverflow) {
    switch (_overflow) {
      case TextOverflow.visible:
        _needsClipping = false;
        _overflowShader = null;
        break;
      case TextOverflow.clip:
      case TextOverflow.ellipsis:
        _needsClipping = true;
        _overflowShader = null;
        break;
      case TextOverflow.fade:
        _needsClipping = true;
        final fadeSizePainter = TextPainter(...)..layout();
        if (didOverflowWidth) {
          double fadeEnd, fadeStart;
          switch (textDirection) {
            ... // Set the direction of fade
          }
          _overflowShader = ui.Gradient.linear(...);
        } else {
          final double fadeEnd = size.height;
          final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
          _overflowShader = ui.Gradient.linear(...);
        }
        break;
    }
  } else {
    ... // No overflow
  }
}
```

The `performLayout()` method calculates if the text overflows based on a dry run and also handles overflow appropriately - like by creating a fade painter to fade text going to the end.

However, this still doesn't answer what actually paints the text on the screen.

So far, we've:

1) Collected everything required for the text: styles, direction, semantics, selections, etc.

2) Created text spans based on the data.

3) Performed layout for the text and added considerations for overflow.

Since Flutter apps are just giant paintings on a canvas, the final bit is the `TextPainter` class that does the text painting for us.

## Bedrock: Canvas and TextPainter

To paint the text on the screen, there are a few steps to follow.

First, we pass the tree of `TextSpans` created by the Widgets before to the `TextPainter`. The `RenderParagraph` also creates an instance of `TextPainter` inside it.

Then, we call the `layout()` method inside the `TextPainter` class which sizes the text height and width, and also clears any caret cache (since the class is also used for the `TextField` Widget).

Here is the simplified code for the `layout()` method:

```dart
void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {

  if (_paragraph != null && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth) {
    return;
  }

  if (_rebuildParagraphForPaint || _paragraph == null) {
    _createParagraph();
  }
  _lastMinWidth = minWidth;
  _lastMaxWidth = maxWidth;
  // A change in layout invalidates the cached caret and line metrics as well.
  _lineMetricsCache = null;
  _previousCaretPosition = null;
  _previousCaretPrototype = null;
  _layoutParagraph(minWidth, maxWidth);
  _inlinePlaceholderBoxes = _paragraph!.getBoxesForPlaceholders();
}
```

Finally, to actually *actually* paint text on the screen, we call the `paint()` method of the `TextPainter` and pass along the canvas from the `RenderParagraph`. This calls the `canvas.drawParagraph()` which paints the text on the screen:

```dart
void paint(Canvas canvas, Offset offset) {
  final double? minWidth = _lastMinWidth;
  final double? maxWidth = _lastMaxWidth;

  if (_rebuildParagraphForPaint) {
    _createParagraph();
    _layoutParagraph(minWidth, maxWidth);
  }
  canvas.drawParagraph(_paragraph!, offset);
}
```

If you wanted to do this yourself, you can also use a `CustomPaint` with a `CustomPainter` and draw a paragraph to the canvas yourself. That process will make you realise the number of arguments that really go into making a simple `Text` widget.

## Conclusion

It's very easy to say "It's just a Text Widget" but there is amazing customisation and complexity hidden behind even the simplest of Widgets. Text rendering in particular has always been a complex thing to handle and I hope this article helps you appreciate the complexity that is hidden away from you to allow you to create your best ideas with the least effort.
