diff --git a/doc/flame/diagrams/component.md b/doc/flame/diagrams/component.md
index 0720871f5..a808e50f4 100644
--- a/doc/flame/diagrams/component.md
+++ b/doc/flame/diagrams/component.md
@@ -51,5 +51,5 @@ graph TD
PositionComponent --> Sprites
PositionComponent --> HudMarginComponent
PositionComponent --> OtherPositionComponents
- HudMarginComponent --> HudComponents
+ HudMarginComponent --> HudComponents
```
diff --git a/doc/flame/rendering/text_rendering.md b/doc/flame/rendering/text_rendering.md
index 52000712a..60cdff6a4 100644
--- a/doc/flame/rendering/text_rendering.md
+++ b/doc/flame/rendering/text_rendering.md
@@ -113,6 +113,63 @@ You can find all the options under [TextBoxComponent's
API](https://pub.dev/documentation/flame/latest/components/TextBoxComponent-class.html).
+### TextElementComponent
+
+If you want to render an arbitrary TextElement, ranging from a single InlineTextElement to a
+formatted DocumentRoot, you can use the `TextElementComponent`.
+
+A simple example is to create a DocumentRoot to render a sequence of block elements (think of an
+HTML "div") containing rich text:
+
+```dart
+ final document = DocumentRoot([
+ HeaderNode.simple('1984', level: 1),
+ ParagraphNode.simple(
+ 'Anything could be true. The so-called laws of nature were nonsense.',
+ ),
+ // ...
+ ]);
+ final element = TextElementComponent.fromDocument(
+ document: document,
+ position: Vector2(100, 50),
+ size: Vector2(400, 200),
+ );
+```
+
+Note that the size can be specified in two ways; either via:
+
+- the size property common to all `PositionComponents`; or
+- the width/height included within the `DocumentStyle` applied.
+
+An example applying a style to the document (which can include the size but other parameters as
+well):
+
+```dart
+ final style = DocumentStyle(
+ width: 400,
+ height: 200,
+ padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14),
+ background: BackgroundStyle(
+ color: const Color(0xFF4E322E),
+ borderColor: const Color(0xFF000000),
+ borderWidth: 2.0,
+ ),
+ );
+ final document = DocumentRoot([ ... ]);
+ final element = TextElementComponent.fromDocument(
+ document: document,
+ style: style,
+ position: Vector2(100, 50),
+ );
+```
+
+For a more elaborate example of rich-text, formatted text blocks rendering, check [this
+example](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/rendering/rich_text_example.dart).
+
+For more details about the underlying mechanics of the text rendering pipeline, see "Text Elements,
+Text Nodes, and Text Styles" below.
+
+
## Infrastructure
If you are not using the Flame Component System, want to understand the infrastructure behind text
@@ -322,8 +379,8 @@ element is that it exposes a LineMetrics that can be used for advanced rendering
elements only expose a simpler `draw` method which is unaware of sizing and positioning.
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.
+to create an entire document (multiple blocks or paragraphs), enriched with formatted text. In order
+to render an arbitrary TextElement, you can alternatively use the `TextElementComponent` (see above).
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).
@@ -354,7 +411,7 @@ The actual nodes all inherit from `TextNode` and are broken down by the followin
graph TD
%% Config %%
classDef default fill:#282828,stroke:#F6BE00;
-
+
%% Nodes %%
TextNode("
TextNode
@@ -436,7 +493,7 @@ classDiagram
note for FlameTextStyle "Root for all styles.
Not to be confused with Flutter's TextStyle."
-
+
class DocumentStyle {
<>
size
@@ -451,7 +508,7 @@ classDiagram
background [BackgroundStyle]
text [InlineTextStyle]
}
-
+
class BackgroundStyle {
<>
color
diff --git a/examples/lib/stories/rendering/rich_text_example.dart b/examples/lib/stories/rendering/rich_text_example.dart
index 47f75fcff..e5b25eacd 100644
--- a/examples/lib/stories/rendering/rich_text_example.dart
+++ b/examples/lib/stories/rendering/rich_text_example.dart
@@ -10,15 +10,6 @@ class RichTextExample extends FlameGame {
@override
Color backgroundColor() => const Color(0xFF888888);
- @override
- Future onLoad() async {
- add(MyTextComponent()..position = Vector2(100, 50));
- }
-}
-
-class MyTextComponent extends PositionComponent {
- late final TextElement element;
-
@override
Future onLoad() async {
final style = DocumentStyle(
@@ -68,11 +59,13 @@ class MyTextComponent extends PositionComponent {
'minds, truly happens.',
),
]);
- element = document.format(style);
- }
- @override
- void render(Canvas canvas) {
- element.draw(canvas);
+ add(
+ TextElementComponent.fromDocument(
+ document: document,
+ style: style,
+ position: Vector2(100, 50),
+ ),
+ );
}
}
diff --git a/packages/flame/lib/components.dart b/packages/flame/lib/components.dart
index 1b5952260..ecd77d16a 100644
--- a/packages/flame/lib/components.dart
+++ b/packages/flame/lib/components.dart
@@ -42,6 +42,7 @@ export 'src/components/sprite_component.dart';
export 'src/components/sprite_group_component.dart';
export 'src/components/text_box_component.dart';
export 'src/components/text_component.dart';
+export 'src/components/text_element_component.dart';
export 'src/components/timer_component.dart';
export 'src/extensions/vector2.dart';
export 'src/geometry/circle_component.dart';
diff --git a/packages/flame/lib/src/components/text_element_component.dart b/packages/flame/lib/src/components/text_element_component.dart
new file mode 100644
index 000000000..6e57e1fe9
--- /dev/null
+++ b/packages/flame/lib/src/components/text_element_component.dart
@@ -0,0 +1,78 @@
+import 'dart:ui';
+
+import 'package:flame/components.dart';
+import 'package:flame/text.dart';
+
+class TextElementComponent extends PositionComponent {
+ TextElement element;
+
+ TextElementComponent({
+ required this.element,
+ super.position,
+ super.size,
+ super.scale,
+ super.angle,
+ super.anchor,
+ super.children,
+ super.priority,
+ super.key,
+ });
+
+ factory TextElementComponent.fromDocument({
+ required DocumentRoot document,
+ DocumentStyle? style,
+ Vector2? position,
+ Vector2? size,
+ Vector2? scale,
+ double? angle,
+ Anchor? anchor,
+ List? children,
+ int priority = 0,
+ ComponentKey? key,
+ }) {
+ final effectiveStyle = style ?? DocumentStyle();
+ final effectiveSize = _coalesceSize(effectiveStyle, size);
+ final element = document.format(
+ effectiveStyle,
+ width: effectiveSize.x,
+ height: effectiveSize.y,
+ );
+ return TextElementComponent(
+ element: element,
+ position: position,
+ size: effectiveSize,
+ scale: scale,
+ angle: angle,
+ anchor: anchor,
+ children: children,
+ priority: priority,
+ key: key,
+ );
+ }
+
+ @override
+ void render(Canvas canvas) {
+ element.draw(canvas);
+ }
+
+ static Vector2 _coalesceSize(DocumentStyle style, Vector2? size) {
+ final width = style.width ?? size?.x;
+ final height = style.height ?? size?.y;
+ if (width == null || height == null) {
+ throw ArgumentError('Either style.width or size.x must be provided.');
+ }
+ if ((style.width != null && style.width != width) ||
+ (size?.x != null && size?.x != width)) {
+ throw ArgumentError(
+ 'style.width and size.x, if both provided, must match.',
+ );
+ }
+ if ((style.height != null && style.height != height) ||
+ (size?.y != null && size?.y != height)) {
+ throw ArgumentError(
+ 'style.height and size.y, if both provided, must match.',
+ );
+ }
+ return Vector2(width, height);
+ }
+}
diff --git a/packages/flame/test/components/element_component_test.dart b/packages/flame/test/components/element_component_test.dart
new file mode 100644
index 000000000..370072670
--- /dev/null
+++ b/packages/flame/test/components/element_component_test.dart
@@ -0,0 +1,64 @@
+import 'package:flame/components.dart';
+import 'package:flame/text.dart';
+import 'package:test/test.dart';
+
+void main() {
+ group('ElementComponent', () {
+ test('size can be specified via the size parameter', () {
+ final c = TextElementComponent.fromDocument(
+ document: DocumentRoot([]),
+ size: Vector2(100, 200),
+ );
+ expect(c.size, equals(Vector2(100, 200)));
+ });
+ test('size can be specified via the style', () {
+ final c = TextElementComponent.fromDocument(
+ document: DocumentRoot([]),
+ style: DocumentStyle(width: 100, height: 200),
+ );
+ expect(c.size, equals(Vector2(100, 200)));
+ });
+ test('size can be super-specified if matching', () {
+ final c = TextElementComponent.fromDocument(
+ document: DocumentRoot([]),
+ style: DocumentStyle(width: 100, height: 200),
+ size: Vector2(100, 200),
+ );
+ expect(c.size, equals(Vector2(100, 200)));
+ });
+ test('size must be specified', () {
+ expect(
+ () {
+ TextElementComponent.fromDocument(
+ document: DocumentRoot([]),
+ style: DocumentStyle(),
+ );
+ },
+ throwsA(
+ predicate((e) {
+ return e is ArgumentError &&
+ e.message == 'Either style.width or size.x must be provided.';
+ }),
+ ),
+ );
+ });
+ test('size cannot be over-specified if mismatched', () {
+ expect(
+ () {
+ TextElementComponent.fromDocument(
+ document: DocumentRoot([]),
+ style: DocumentStyle(width: 100, height: 200),
+ size: Vector2(100, 300),
+ );
+ },
+ throwsA(
+ predicate((e) {
+ return e is ArgumentError &&
+ e.message ==
+ 'style.height and size.y, if both provided, must match.';
+ }),
+ ),
+ );
+ });
+ });
+}