-
-
Notifications
You must be signed in to change notification settings - Fork 905
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: Structured text and text styles (#1830)
This PR introduces the notions of structured text, and text styles, to support rendering of rich text bodies. Specifically, we recognize that sometimes in games one needs to render pieces of text that are larger than a single word or even a single paragraph. These pieces may include: books, quest descriptions, mission objectives, tutorials, in-game help system, dialogues, etc. Rendering such a piece of text is non-trivial, however. In order to tackle this problem, I break into the following parts: Text structure, represented as a tree of Nodes. The nodes describe the logical structure of the text, for example the document may contain a header, and then several paragraphs, and a list, where the list contains some list items, some of which having possibly several paragraphs, etc. This structure is similar to how in HTML the text is marked up with HTML tags. Text styles are struct-like classes that contain properties describing how the text is to be styled: font size, font renderer, borders, backgrounds, margins, padding, etc. This representation is also tree-like, so that for example text inside paragraphs can have different style than text within headers, and paragraphs within lists can have different margins. A text style is similar to a stylesheet in HTML. Text elements are the result of applying the document style to a document node: they are the "prepared" and laid out pieces, ready to be rendered. Elements are a bit like mini-components, or perhaps text "particles" in a particle system.
- Loading branch information
Showing
26 changed files
with
754 additions
and
28 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
import 'package:flame/components.dart'; | ||
import 'package:flame/game.dart'; | ||
import 'package:flame/text.dart'; | ||
import 'package:flutter/painting.dart'; | ||
|
||
class RichTextExample extends FlameGame { | ||
static String description = ''; | ||
|
||
@override | ||
Color backgroundColor() => const Color(0xFF888888); | ||
|
||
@override | ||
Future<void> onLoad() async { | ||
add(MyTextComponent()..position = Vector2.all(100)); | ||
} | ||
} | ||
|
||
class MyTextComponent extends PositionComponent { | ||
late final Element element; | ||
|
||
@override | ||
Future<void> onLoad() async { | ||
final style = DocumentStyle( | ||
width: 400, | ||
height: 200, | ||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 14), | ||
background: BackgroundStyle( | ||
color: const Color(0xFFFFFFEE), | ||
borderColor: const Color(0xFF000000), | ||
borderWidth: 2.0, | ||
), | ||
paragraphStyle: BlockStyle( | ||
margin: const EdgeInsets.symmetric(vertical: 6), | ||
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 6), | ||
background: BackgroundStyle( | ||
color: const Color(0xFFFFF0CB), | ||
borderColor: const Color(0xFFAAAAAA), | ||
), | ||
), | ||
); | ||
final document = DocumentNode([ | ||
ParagraphNode.simple( | ||
'Anything could be true. The so-called laws of nature were nonsense.', | ||
), | ||
ParagraphNode.simple( | ||
'The law of gravity was nonsense. "If I wished," O\'Brien had said, ' | ||
'"I could float off this floor like a soap bubble." Winston worked it ' | ||
'out. "If he thinks he floats off the floor, and I simultaneously ' | ||
'think I can see him do it, then the thing happens."', | ||
), | ||
ParagraphNode.simple( | ||
'Suddenly, like a lump of submerged wreckage breaking the surface of ' | ||
'water, the thought burst into his mind: "It doesn\'t really happen. ' | ||
'We imagine it. It is hallucination."', | ||
), | ||
ParagraphNode.simple( | ||
'He pushed the thought under instantly. The fallacy was obvious. It ' | ||
'presupposed that somewhere or other, outside oneself, there was a ' | ||
'"real" world where "real" things happened. But how could there be ' | ||
'such a world? What knowledge have we of anything, save through our ' | ||
'own minds? All happenings are in the mind. Whatever happens in all ' | ||
'minds, truly happens.', | ||
), | ||
]); | ||
element = document.format(style); | ||
} | ||
|
||
@override | ||
void render(Canvas canvas) { | ||
element.render(canvas); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import 'dart:math'; | ||
|
||
import 'package:meta/meta.dart'; | ||
|
||
@internal | ||
double collapseMargin(double margin1, double margin2) { | ||
if (margin1 >= 0) { | ||
return (margin2 < 0) ? margin1 + margin2 : max(margin1, margin2); | ||
} else { | ||
return (margin2 < 0) ? min(margin1, margin2) : margin1 + margin2; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import 'package:flame/src/text/elements/element.dart'; | ||
|
||
/// [BlockElement] is the base class for [Element]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 { | ||
BlockElement(this.width, this.height); | ||
double width; | ||
double height; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
import 'dart:ui'; | ||
|
||
/// An [Element] is a basic building block of a rich-text document. | ||
/// | ||
/// Elements 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 (which are descriptors for how | ||
/// arbitrary pieces of text ought to be rendered). | ||
/// | ||
/// Elements are at the final stage of the text rendering pipeline, they are | ||
/// created during the layout step. | ||
abstract class Element { | ||
void translate(double dx, double dy); | ||
|
||
/// Renders the element on the [canvas], at coordinates determined during the | ||
/// layout. | ||
/// | ||
/// In order to render the element at a different location, consider either | ||
/// calling the [translate] method, or applying a translation transform to the | ||
/// canvas beforehand. | ||
void render(Canvas canvas); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import 'dart:ui'; | ||
|
||
import 'package:flame/src/text/elements/block_element.dart'; | ||
import 'package:flame/src/text/elements/element.dart'; | ||
|
||
class GroupElement extends BlockElement { | ||
GroupElement(super.width, super.height, this.children); | ||
|
||
final List<Element> children; | ||
|
||
@override | ||
void translate(double dx, double dy) { | ||
children.forEach((child) => child.translate(dx, dy)); | ||
} | ||
|
||
@override | ||
void render(Canvas canvas) { | ||
children.forEach((child) => child.render(canvas)); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
import 'dart:ui'; | ||
|
||
import 'package:flame/src/text/elements/element.dart'; | ||
|
||
class RectElement extends Element { | ||
RectElement(double width, double height, this._paint) | ||
: _rect = Rect.fromLTWH(0, 0, width, height); | ||
|
||
Rect _rect; | ||
final Paint _paint; | ||
|
||
@override | ||
void translate(double dx, double dy) { | ||
_rect = _rect.translate(dx, dy); | ||
} | ||
|
||
@override | ||
void render(Canvas canvas) { | ||
canvas.drawRect(_rect, _paint); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
import 'dart:ui'; | ||
|
||
import 'package:flame/src/text/elements/element.dart'; | ||
|
||
class RRectElement extends Element { | ||
RRectElement( | ||
double width, | ||
double height, | ||
double radius, | ||
this._paint, | ||
) : _rrect = RRect.fromLTRBR(0, 0, width, height, Radius.circular(radius)); | ||
|
||
RRect _rrect; | ||
final Paint _paint; | ||
|
||
@override | ||
void translate(double dx, double dy) { | ||
_rrect = _rrect.shift(Offset(dx, dy)); | ||
} | ||
|
||
@override | ||
void render(Canvas canvas) { | ||
canvas.drawRRect(_rrect, _paint); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,12 @@ | ||
import 'package:flame/src/text/common/text_line.dart'; | ||
import 'package:flame/src/text/elements/element.dart'; | ||
|
||
/// [TextElement] is the base class describing a span of text that has *inline* | ||
/// placement rules. | ||
/// | ||
/// Concrete implementations of this class must know how to perform own layout | ||
/// (i.e. determine the exact placement and size of each internal piece), and | ||
/// then render on a canvas afterwards. | ||
abstract class TextElement extends Element { | ||
TextLine get lastLine; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file was deleted.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import 'package:flame/src/text/nodes/block_node.dart'; | ||
|
||
class GroupBlockNode extends BlockNode { | ||
GroupBlockNode(this.children); | ||
|
||
final List<BlockNode> children; | ||
} | ||
|
||
class BlockquoteNode extends GroupBlockNode { | ||
BlockquoteNode(super.children); | ||
} | ||
|
||
abstract class TextNode {} | ||
|
||
class PlainTextNode extends TextNode { | ||
PlainTextNode(this.text); | ||
|
||
final String text; | ||
} | ||
|
||
class GroupTextNode extends TextNode { | ||
GroupTextNode(this.children); | ||
|
||
final List<TextNode> children; | ||
} | ||
|
||
class BoldTextNode extends GroupTextNode { | ||
BoldTextNode(super.children); | ||
} | ||
|
||
class ItalicTextNode extends GroupTextNode { | ||
ItalicTextNode(super.children); | ||
} | ||
|
||
class StrikethroughTextNode extends GroupTextNode { | ||
StrikethroughTextNode(super.children); | ||
} | ||
|
||
class HighlightedTextNode extends GroupTextNode { | ||
HighlightedTextNode(super.children); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
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'; | ||
|
||
/// An abstract base class for all entities with "block" placement rules. | ||
abstract class BlockNode { | ||
Element? makeBackground(BackgroundStyle? style, double width, double height) { | ||
if (style == null) { | ||
return null; | ||
} | ||
final out = <Element>[]; | ||
final backgroundPaint = style.backgroundPaint; | ||
final borderPaint = style.borderPaint; | ||
final borders = style.borderWidths; | ||
final radius = style.borderRadius; | ||
|
||
if (backgroundPaint != null) { | ||
if (radius == 0) { | ||
out.add(RectElement(width, height, backgroundPaint)); | ||
} else { | ||
out.add(RRectElement(width, height, radius, backgroundPaint)); | ||
} | ||
} | ||
if (borderPaint != null) { | ||
if (radius == 0) { | ||
out.add( | ||
RectElement( | ||
width - borders.horizontal / 2, | ||
height - borders.vertical / 2, | ||
borderPaint, | ||
)..translate(borders.left / 2, borders.top / 2), | ||
); | ||
} else { | ||
out.add( | ||
RRectElement( | ||
width - borders.horizontal / 2, | ||
height - borders.vertical / 2, | ||
radius, | ||
borderPaint, | ||
)..translate(borders.left / 2, borders.top / 2), | ||
); | ||
} | ||
} | ||
if (out.isEmpty) { | ||
return null; | ||
} | ||
if (out.length == 1) { | ||
return out.first; | ||
} else { | ||
return GroupElement(width, height, out); | ||
} | ||
} | ||
} |
Oops, something went wrong.