From 10fb65f66ca1f1dbac04a138ef4a28b1ed5e5a23 Mon Sep 17 00:00:00 2001 From: Luan Nico Date: Sat, 2 Sep 2023 12:36:58 -0700 Subject: [PATCH] feat: Add TextElementComponent (#2694) Add TextElementComponent --- doc/flame/diagrams/component.md | 2 +- doc/flame/rendering/text_rendering.md | 67 ++++++++++++++-- .../stories/rendering/rich_text_example.dart | 21 ++--- packages/flame/lib/components.dart | 1 + .../components/text_element_component.dart | 78 +++++++++++++++++++ .../components/element_component_test.dart | 64 +++++++++++++++ 6 files changed, 213 insertions(+), 20 deletions(-) create mode 100644 packages/flame/lib/src/components/text_element_component.dart create mode 100644 packages/flame/test/components/element_component_test.dart 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.'; + }), + ), + ); + }); + }); +}