feat: Add TextElementComponent (#2694)

Add TextElementComponent
This commit is contained in:
Luan Nico
2023-09-02 12:36:58 -07:00
committed by GitHub
parent 6dbcd0bd40
commit 10fb65f66c
6 changed files with 213 additions and 20 deletions

View File

@ -51,5 +51,5 @@ graph TD
PositionComponent --> Sprites PositionComponent --> Sprites
PositionComponent --> HudMarginComponent PositionComponent --> HudMarginComponent
PositionComponent --> OtherPositionComponents PositionComponent --> OtherPositionComponents
HudMarginComponent --> HudComponents HudMarginComponent --> HudComponents
``` ```

View File

@ -113,6 +113,63 @@ You can find all the options under [TextBoxComponent's
API](https://pub.dev/documentation/flame/latest/components/TextBoxComponent-class.html). 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 ## Infrastructure
If you are not using the Flame Component System, want to understand the infrastructure behind text 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. 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 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. to create an entire document (multiple blocks or paragraphs), enriched with formatted text. In order
Currently, these extra features of the system are not exposed through FCS; but can be used directly. to render an arbitrary TextElement, you can alternatively use the `TextElementComponent` (see above).
An example of such usages can be seen in [this 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). 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 graph TD
%% Config %% %% Config %%
classDef default fill:#282828,stroke:#F6BE00; classDef default fill:#282828,stroke:#F6BE00;
%% Nodes %% %% Nodes %%
TextNode(" TextNode("
<big><strong>TextNode</strong></big> <big><strong>TextNode</strong></big>
@ -436,7 +493,7 @@ classDiagram
note for FlameTextStyle "Root for all styles. note for FlameTextStyle "Root for all styles.
Not to be confused with Flutter's TextStyle." Not to be confused with Flutter's TextStyle."
class DocumentStyle { class DocumentStyle {
<<for the entire Document Root>> <<for the entire Document Root>>
size size
@ -451,7 +508,7 @@ classDiagram
background [BackgroundStyle] background [BackgroundStyle]
text [InlineTextStyle] text [InlineTextStyle]
} }
class BackgroundStyle { class BackgroundStyle {
<<for Block or Document>> <<for Block or Document>>
color color

View File

@ -10,15 +10,6 @@ class RichTextExample extends FlameGame {
@override @override
Color backgroundColor() => const Color(0xFF888888); 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 @override
Future<void> onLoad() async { Future<void> onLoad() async {
final style = DocumentStyle( final style = DocumentStyle(
@ -68,11 +59,13 @@ class MyTextComponent extends PositionComponent {
'minds, truly happens.', 'minds, truly happens.',
), ),
]); ]);
element = document.format(style);
}
@override add(
void render(Canvas canvas) { TextElementComponent.fromDocument(
element.draw(canvas); document: document,
style: style,
position: Vector2(100, 50),
),
);
} }
} }

View File

@ -42,6 +42,7 @@ export 'src/components/sprite_component.dart';
export 'src/components/sprite_group_component.dart'; export 'src/components/sprite_group_component.dart';
export 'src/components/text_box_component.dart'; export 'src/components/text_box_component.dart';
export 'src/components/text_component.dart'; export 'src/components/text_component.dart';
export 'src/components/text_element_component.dart';
export 'src/components/timer_component.dart'; export 'src/components/timer_component.dart';
export 'src/extensions/vector2.dart'; export 'src/extensions/vector2.dart';
export 'src/geometry/circle_component.dart'; export 'src/geometry/circle_component.dart';

View File

@ -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);
}
}

View File

@ -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.';
}),
),
);
});
});
}