Skip to content

Commit

Permalink
feat: Add TextElementComponent (#2694)
Browse files Browse the repository at this point in the history
Add TextElementComponent
  • Loading branch information
luanpotter committed Sep 2, 2023
1 parent 6dbcd0b commit 10fb65f
Show file tree
Hide file tree
Showing 6 changed files with 213 additions and 20 deletions.
2 changes: 1 addition & 1 deletion doc/flame/diagrams/component.md
Expand Up @@ -51,5 +51,5 @@ graph TD
PositionComponent --> Sprites
PositionComponent --> HudMarginComponent
PositionComponent --> OtherPositionComponents
HudMarginComponent --> HudComponents
HudMarginComponent --> HudComponents
```
67 changes: 62 additions & 5 deletions doc/flame/rendering/text_rendering.md
Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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("
<big><strong>TextNode</strong></big>
Expand Down Expand Up @@ -436,7 +493,7 @@ classDiagram
note for FlameTextStyle "Root for all styles.
Not to be confused with Flutter's TextStyle."
class DocumentStyle {
<<for the entire Document Root>>
size
Expand All @@ -451,7 +508,7 @@ classDiagram
background [BackgroundStyle]
text [InlineTextStyle]
}
class BackgroundStyle {
<<for Block or Document>>
color
Expand Down
21 changes: 7 additions & 14 deletions examples/lib/stories/rendering/rich_text_example.dart
Expand Up @@ -10,15 +10,6 @@ class RichTextExample extends FlameGame {
@override
Color backgroundColor() => const Color(0xFF888888);

@override
Future<void> onLoad() async {
add(MyTextComponent()..position = Vector2(100, 50));
}
}

class MyTextComponent extends PositionComponent {
late final TextElement element;

@override
Future<void> onLoad() async {
final style = DocumentStyle(
Expand Down Expand Up @@ -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),
),
);
}
}
1 change: 1 addition & 0 deletions packages/flame/lib/components.dart
Expand Up @@ -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';
Expand Down
78 changes: 78 additions & 0 deletions 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<Component>? 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);
}
}
64 changes: 64 additions & 0 deletions 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.';
}),
),
);
});
});
}

0 comments on commit 10fb65f

Please sign in to comment.