This is a high level overview of how we do text layout and rendering in Flutter Web:
Before we start measuring text, we first need to break it down into fragments. A fragment is the longest substring in the paragraph that satisfies the following:
- Contains no soft or hard line breaks.
- The entire fragment has the same text direction.
- The entire fragment has the same text style.
In a multi-line paragraph, we need to know where soft/hard line breaks are so we can correctly split the paragraph into lines that fit within a certain width.
Currently, we don't have an API from the browser to do this, so we wrote our own implementation of the unicode line breaking algorithm. We have a script that takes unicode line break properties and embeds them into our dart code as an encoded string.
This implementation has been doing okay but it's not complete. There are 2 main issues:
- Discrepancies: Chrome, for example, deviates from the line breaking rules listed in the unicode spec.
- South East Asian languages: These require more advanced line break detection using dictionaries and morphological analysis.
This is where a proposal like Intl.Segmenter v2
can help. It'll give us more correct line break detection and improve our code size and performance. It'll also keep us in sync with how the browser performs line break detection.
Fragments of text that have different directions have to be separated because they need to be positioned differently. There's no browser API to determine the direction of text, so we implemented something similar to the Google Closure library's LTR/RTL detection.
Fragments that have different styles need to be measured separately using the canvas2d measureText
API as explained below.
After the text is broken down into fragments, we now measure the width of each fragment using canvas's measureText
API. This allows us to position the fragments inside the paragraph, and determine which fragments go in each line.
We use a hidden DOM element to measure the height of text. But because this is an expensive operation, we cache the results using a cache key that combines:
font-family
font-size
line-height
We assume that these are the only styles that affect the text's height.
Note: In some browsers, the measureText
implementation provides information about the height of text too. We don't use this yet because not all browsers support it.
In flutter web, when using the html renderer (as opposed to the Canvaskit renderer), we have two modes for displaying text:
Simply create text <span>
s for the fragments, then insert them into a <p>
element. We also set the appropriate css styles on the <p>
root element and all the <span>
children.
Paint the text in a canvas2d context using the fillText
/strokeText
APIs.
These canvas2d text drawing APIs are limited, some things require custom code from us to implement:
- Text shadows: We draw the shadows separately before drawing the actual text.
- Background color: We calculate the bounding boxes of the text and draw the background as rects.
- Letter spacing:
- We have custom code to handle letter spacing in a very inefficient manner.
- We are unable to render Arabic with letter-spacing correctly: flutter/flutter#71220
- Word spacing
- Text decoration
- Text overflow
Some things simply can't be done in canvas2d:
Our text implementation in Flutter Web allows the querying of certain information after the paragraph has been laid out. These are useful when dealing with text editing for example. Some of the queries that we support:
- Highlighting a range of text: One may need to highlight a range of text, so they query about the bounding boxes that cover that range of text.
- Placing the cursor: When the user clicks with a mouse inside a piece of text, the app needs to know where to place the cursor, so it can query about that.
We also support having placeholders inline inside the paragraph of text. Each placeholder box has a size (width & height) and a few properties to control its vertical alignment with the text around it.
Placeholders are taken into account when laying out the paragraph, and their positions within the paragraph can be queried too.