Skip to content

Commit

Permalink
refactor!: Rename (Text) Elements, Nodes and Styles for clarity, add …
Browse files Browse the repository at this point in the history
…docs (#2700)

This occurred to me after a discussion on the [new FCS component
PR](#2694 (comment)).
As per usual, @spydon has opened my eyes to the ultimate truth:

We should rename loads of files, and it shall affect almost no one.

The idea is to (1) add a "Text" prefix to all text-rendering-related
classes and (2) rename the existing `Text*` to `InlineText*` (which is
what they are).

This PR is a bit big, but the changes should hopefully be simple to
review, and can be broken down into:

* Add a proper base class for the node inheritance chain, call it
TextNode (while working on Flame Markdown I realized the value this will
have to me)
* Rename the old TextNode to InlineTextNode
* Rename DocumentNode to DocumentRoot because it is not a node
* Rename Element to TextElement
* Rename the old TextElement to InlineTextElement
* Rename Style to FlameTextStyle (note: we could consider dropping the
Flame here)
* Rename the old FlameTextStyle to InlineTextStyle
* Update the docs accordingly
* Add some more diagrams and explanations to the docs, following the new
nomenclature
* I also updated our "internal" imports to use the text module to make
life so much easier (this could arguably be done in a separate PR, but I
honestly think it's easier to review together, please lmk if you prefer
me to split).

These are all breaking changes but likely won't actually affect most
users (see below).

While this is breaking, it should hopefully not affect most users,
because these are all infrastructure classes that most people aren't
using directly. If you are using the FCS components, or the renderers
`TextPaint` or `SpriteFontRenderer` directly, this should have zero
effect to you.

If you are using the Nodes, Stlyes or Elements directly, or have a
custom TextRenderer, see below.

Migrating should be a simple matter of renaming your type references:

* from TextNode to InlineTextNode
* from TextElement to InlineTextElement
* from Element to TextElement
* from FlameTextStyle to InlineTextStyle
* from Style to FlameTextStyle

Make sure to do it in the appropriate order not to cause any
double-replace issues.

If you are importing via the module `package:flame/text.dart`, which we
highly encourage, you should not have to change any import statements
whatsoever.
  • Loading branch information
luanpotter committed Sep 2, 2023
1 parent 99a1016 commit 4b420b7
Show file tree
Hide file tree
Showing 40 changed files with 494 additions and 361 deletions.
224 changes: 200 additions & 24 deletions doc/flame/rendering/text_rendering.md
Expand Up @@ -127,6 +127,7 @@ this section is for you.
The following diagram showcases the class and inheritance structure of the text rendering pipeline:

```mermaid
%%{init: { 'theme': 'dark' } }%%
classDiagram
%% renderers
note for TextRenderer "This just the style (how).
Expand Down Expand Up @@ -265,21 +266,21 @@ it possible to test the layout, positioning and sizing of the elements without h
font-based rendering.


## Text Elements
## Inline Text Elements

Text Elements are "pre-compiled", formatted and laid-out pieces of text with a specific styling
A `TextElement` is a "pre-compiled", formatted and laid-out piece of text with a specific styling
applied, ready to be rendered at any given position.

`TextElement` implements the `Element` interface and must implement their two methods, one that
teaches how to translate it around and another on how to draw it to the canvas:
A `InlineTextElement` implements the `TextElement` interface and must implement their two methods,
one that teaches how to translate it around and another on how to draw it to the canvas:

```dart
void translate(double dx, double dy);
void draw(Canvas canvas);
```

These methods are intended to be overwritten by the implementations of `TextElement` but probably
will not be called directly by users; because a convenient `render` method is provided:
These methods are intended to be overwritten by the implementations of `InlineTextElement`, and
probably will not be called directly by users; because a convenient `render` method is provided:

```dart
void render(
Expand All @@ -291,37 +292,212 @@ will not be called directly by users; because a convenient `render` method is pr

That allows the element to be rendered at a specific position, using a given anchor.

The interface also mandates (and provides) a getter for the LineMetrics object associated with that
`TextElement`, which allows you (and the `render` implementation) to access sizing information
related to the element (width, height, ascend, etc).
The interface also mandates (and provides) a getter for the `LineMetrics` object associated with
that `InlineTextElement`, which allows you (and the `render` implementation) to access sizing
information related to the element (width, height, ascend, etc).

```dart
LineMetrics get metrics;
```


## Elements, Nodes, and Styles
## Text Elements, Text Nodes, and Text Styles

While normal renderers always work with TextElements directly, there is a bigger underlying
While normal renderers always work with a `InlineTextElement` directly, there is a bigger underlying
infrastructure that can be used to render more rich or formatter text.

Elements are a superset of TextElements that represent an arbitrary rendering block within a
rich-text document. Essentially, they are concrete and "physical": they are objects that are ready
to be rendered on a canvas.
Text Elements are a superset of Inline Text Elements that represent an arbitrary rendering block
within a rich-text document. Essentially, they are concrete and "physical": they are objects that
are ready to be rendered on a canvas.

This property distinguishes them from Nodes, which are structured pieces of text, and from Styles,
This property distinguishes them from Text Nodes, which are structured pieces of text, and from Text
Styles (called `FlameTextStyle` in code to make it easier to work alongside Flutter's `TextStyle`),
which are descriptors for how arbitrary pieces of text ought to be rendered.

So a user would use Node to describe a desired document of rich text; define Styles to apply to it;
and use that to generate an Element. Depending on the type of rendering, the Element generated will
be a TextElement, which brings us back to the normal flow of the rendering pipeline. The unique
property of the Text-type element is that it exposes a LineMetrics that can be used for advanced
rendering; while the other elements only expose a simpler `draw` method which is unaware of sizing
and positioning.
So, in the most general case, a user would use a `TextNode` to describe a desired piece of rich
text; define a `FlameTextStyle` to apply to it; and use that to generate a `TextElement`. Depending
on the type of rendering, the `TextElement` generated will be an `InlineTextElement`, which brings
us back to the normal flow of the rendering pipeline. The unique property of the Inline-Text-type
element is that it exposes a LineMetrics that can be used for advanced rendering; while the other
elements only expose a simpler `draw` method which is unaware of sizing and positioning.

However the other types of Elements, Nodes and Style must be used if the intent is to create an
entire Document, enriched with formatted text. Currently these extra features of the system are not
exposed through FCS, but can be used directly.
However, the other types of Text Elements, Text Nodes, and Text Styles must be used if the intent is
to create an entire document (multiple blocks or paragraphs), enriched with formatted text.
Currently, these extra features of the system are not exposed through FCS; but can be used directly.

An example of such usages can be seen in [this
example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/rich_text_example.dart).


### Text Nodes and the Document Root

A `DocumentRoot` is not a `TextNode` (inheritance-wise) in itself but represents a grouping of
`BlockNodes` that layout a "page" or "document" of rich text laid out in multiple blocks or
paragraphs. It represents the entire document and can receive a global Style.

The first step to define your rich-text document is to create a Node, which will likely be a
`DocumentRoot`.

It will first contain the top-most list of Block Nodes that can define headers, paragraphs or
columns.

Then each of those blocks can contain other blocks or the Inline Text Nodes, either Plain Text Nodes
or some rich-text with specific formatting.

Note that the hierarchy defined by the node structure is also used for styling purposes as per
defined in the `FlameTextStyle` class.

The actual nodes all inherit from `TextNode` and are broken down by the following diagram:

```mermaid
%%{init: { 'theme': 'dark' } }%%
graph TD
%% Config %%
classDef default fill:#282828,stroke:#F6BE00;
%% Nodes %%
TextNode("
<big><strong>TextNode</strong></big>
Can be thought of as an HTML DOM node;
each subclass can be thought of as a specific tag.
")
BlockNode("
<big><strong>BlockNode</strong></big>
#quot;div#quot;
")
InlineTextNode("
<big><strong>InlineTextNode</strong></big>
#quot;span#quot;
")
ColumnNode("
<big><strong>ColumnNode</strong></big>
column-arranged group of other Block Nodes
")
TextBlockNode("
<big><strong>TextBlockNode</strong></big>
a #quot;div#quot; with an InlineTextNode as a direct child
")
HeaderNode("
<big><strong>HeaderNode</strong></big>
#quot;h1#quot; / #quot;h2#quot; / etc
")
ParagraphNode("
<big><strong>ParagraphNode</strong></big>
#quot;p#quot;
")
GroupTextNode("
<big><strong>GroupTextNode</strong></big>
groups other TextNodes in a single line
")
PlainTextNode("
<big><strong>PlainTextNode</strong></big>
just plain text, unformatted
")
ItalicTextNode("
<big><strong>ItalicTextNode</strong></big>
#quot;i#quot; / #quot;em#quot;
")
BoldTextNode("
<big><strong>BoldTextNode</strong></big>
#quot;b#quot; / #quot;strong#quot;
")
TextNode ----> BlockNode
TextNode --------> InlineTextNode
BlockNode --> ColumnNode
BlockNode --> TextBlockNode
TextBlockNode --> HeaderNode
TextBlockNode --> ParagraphNode
InlineTextNode --> GroupTextNode
InlineTextNode --> PlainTextNode
InlineTextNode --> BoldTextNode
InlineTextNode --> ItalicTextNode
```


### (Flame) Text Styles

Text Styles can be applied to nodes to generate elements. They all inherit from `FlameTextStyle`
abstract class (which is named as is to avoid confusion with Flutter's `TextStyle`).

They follow a tree-like structure, always having `DocumentStyle` as the root; this structure is
leveraged to apply cascading style to the analogous Node structure. In fact, they are pretty similar
to, and can be thought of as, CSS definitions.

The full inheritance chain can be seen on the following diagram:

```mermaid
%%{init: { 'theme': 'dark' } }%%
classDiagram
%% Nodes %%
class FlameTextStyle {
copyWith()
merge()
}
note for FlameTextStyle "Root for all styles.
Not to be confused with Flutter's TextStyle."
class DocumentStyle {
<<for the entire Document Root>>
size
padding
background [BackgroundStyle]
specific styles [for blocks & inline]
}
class BlockStyle {
<<for Block Nodes>>
margin, padding
background [BackgroundStyle]
text [InlineTextStyle]
}
class BackgroundStyle {
<<for Block or Document>>
color
border
}
class InlineTextStyle {
<<for any nodes>>
font, color
}
FlameTextStyle <|-- DocumentStyle
FlameTextStyle <|-- BlockStyle
FlameTextStyle <|-- BackgroundStyle
FlameTextStyle <|-- InlineTextStyle
```


### Text Elements

Finally, we have the elements, that represent a combination of a node ("what") with a style ("how"),
and therefore represent a pre-compiled, laid-out piece of rich text to be rendered on the Canvas.

Inline Text Elements specifically can alternatively be thought of as a combination of a
`TextRenderer` (simplified "how") and a string (single line of "what").

That is because an `InlineTextStyle` can be converted to a specific `TextRenderer` via the
`asTextRenderer` method, which is then used to lay out each line of text into a unique
`InlineTextElement`.

When using the renderer directly, the entire layout process is skipped, and a single
`TextPainterTextElement` or `SpriteFontTextElement` is returned.

As you can see, both definitions of an Element are, essentially, equivalent, all things considered.
But it still leaves us with two paths for rendering text. Which one to pick? How to solve this
conundrum?

When in doubt, the following guidelines can help you picking the best path for you:

- for the simplest way to render text, use `TextPaint` (basic renderer implementation)
- you can use the FCS provided component `TextComponent` for that.
- for rendering Sprite Fonts, you must use `SpriteFontRenderer` (a renderer implementation that
accepts a `SpriteFont`);
- for rendering multiple lines of text, with automatic line breaks, you have two options:
- use the FCS `TextBoxComponent`, which uses any text renderer to draw each line of text as an
Element, and does its own layout and line breaking;
- use the Text Node & Style system to create your pre-laid-out Elements. Note: there is no current
FCS component for it.
- finally, in order to have formatted (or rich) text, you must use Text Nodes & Styles.
2 changes: 1 addition & 1 deletion examples/lib/stories/input/hardware_keyboard_example.dart
Expand Up @@ -219,7 +219,7 @@ class KeyboardKey extends PositionComponent {
}

final String text;
late final TextElement textElement;
late final InlineTextElement textElement;
late final RRect rect;

/// The RawKeyEvents may occur very fast, and out of sync with the game loop.
Expand Down
4 changes: 2 additions & 2 deletions examples/lib/stories/rendering/rich_text_example.dart
Expand Up @@ -17,7 +17,7 @@ class RichTextExample extends FlameGame {
}

class MyTextComponent extends PositionComponent {
late final Element element;
late final TextElement element;

@override
Future<void> onLoad() async {
Expand All @@ -38,7 +38,7 @@ class MyTextComponent extends PositionComponent {
),
),
);
final document = DocumentNode([
final document = DocumentRoot([
HeaderNode.simple('1984', level: 1),
ParagraphNode.simple(
'Anything could be true. The so-called laws of nature were nonsense.',
Expand Down
2 changes: 1 addition & 1 deletion packages/flame/lib/src/components/text_component.dart
Expand Up @@ -38,7 +38,7 @@ class TextComponent<T extends TextRenderer> extends PositionComponent {
updateBounds();
}

late TextElement _textElement;
late InlineTextElement _textElement;

@internal
void updateBounds() {
Expand Down
13 changes: 7 additions & 6 deletions packages/flame/lib/src/text/common/utils.dart
@@ -1,10 +1,7 @@
import 'dart:math';

import 'package:flame/src/text/elements/element.dart';
import 'package:flame/src/text/elements/group_element.dart';
import 'package:flame/src/text/elements/rect_element.dart';
import 'package:flame/src/text/elements/rrect_element.dart';
import 'package:flame/src/text/styles/background_style.dart';
import 'package:flame/text.dart';
import 'package:meta/meta.dart';

@internal
Expand All @@ -17,11 +14,15 @@ double collapseMargin(double margin1, double margin2) {
}

@internal
Element? makeBackground(BackgroundStyle? style, double width, double height) {
TextElement? makeBackground(
BackgroundStyle? style,
double width,
double height,
) {
if (style == null) {
return null;
}
final out = <Element>[];
final out = <TextElement>[];
final backgroundPaint = style.backgroundPaint;
final borderPaint = style.borderPaint;
final borders = style.borderWidths;
Expand Down
8 changes: 4 additions & 4 deletions packages/flame/lib/src/text/elements/block_element.dart
@@ -1,11 +1,11 @@
import 'package:flame/src/text/elements/element.dart';
import 'package:flame/text.dart';

/// [BlockElement] is the base class for [Element]s with rectangular shape and
/// "block" placement rules.
/// [BlockElement] is the base class for [TextElement]s with rectangular shape
/// and "block" placement rules.
///
/// Within HTML, this corresponds to elements with `display: block` property,
/// such as `<div>` or `<blockquote>`.
abstract class BlockElement extends Element {
abstract class BlockElement extends TextElement {
BlockElement(this.width, this.height);

final double width;
Expand Down
24 changes: 0 additions & 24 deletions packages/flame/lib/src/text/elements/element.dart

This file was deleted.

5 changes: 2 additions & 3 deletions packages/flame/lib/src/text/elements/group_element.dart
@@ -1,5 +1,4 @@
import 'package:flame/src/text/elements/block_element.dart';
import 'package:flame/src/text/elements/element.dart';
import 'package:flame/text.dart';
import 'package:flutter/rendering.dart' hide TextStyle;

class GroupElement extends BlockElement {
Expand All @@ -9,7 +8,7 @@ class GroupElement extends BlockElement {
required this.children,
}) : super(width, height);

final List<Element> children;
final List<TextElement> children;

@override
void translate(double dx, double dy) {
Expand Down

0 comments on commit 4b420b7

Please sign in to comment.