From 34f69b953c137fbf0168aebec3860c6abc888594 Mon Sep 17 00:00:00 2001 From: Luan Nico Date: Thu, 24 Aug 2023 00:58:52 -0700 Subject: [PATCH] refactor!: Simplify text rendering pipeline (#2663) While studying the existing text rendering pipeline and infrastructure, in order to better document it and add some easier user-facing ways to render rich text, I found it to be on a slightly overcomplicated state that seems to me to be an artifact of a migration. Let me first briefly describe how it works with some diagrams (I am writing docs for everything and will re-use this content but wanted to propose this PR first as it will change how things look like). We have 3 hierarchies related to rendering: renderers: children of TextRenderer, these define how to render text (style). do not include text formatters: children of TextFormatter, this also define how to render text (style), but a bit more higher level than TextRenderer. elements: these are laid-out, styled and ready-render pieces of text (style + string), created by formatters Note that the renderers and formatters kinda serve the same purpose, but the renderers are a more low-level API. Here is how it it looks like today: Not only that but all of our TextRenderer are actually FormatterTextRenderer, which just use a formatter to "comply" with the "renderer" interface. There are no "raw" TextRenderers anymore. This current structure seems to clearly derive from an unfinished transition into the new, higher level and more flexible "formatter" structure while still supporting the "renderers" while we transition. That transition, though, seems to have completed. The current state makes things more complicated as our current versions of components (TextComponent and TextBoxComponent), specifically the former, have branching code to support both the raw TextRenderer and the specific FormatterTextRenderer implementation, which breaks the inheritance encapsulation and voids its purpose in the first place. The purpose of this PR is to simplify this class structure by: remove the base TextRenderer rename FormatterTextRenderer to TextRenderer: all renderers are formatter-based now update all references simplify the code of the components by only dealing with (former FormatterTextRenderer) TextRenderers This is what it looks like now: Note that while this is a breaking change, it should not affect most users as they should be using much more user-facing classes, like the aforementioned components, instead of the backing implementations. In fact even if they use the renderers, they probably use one of the concrete implementations anyway, TextPaint or SpriteFontRenderer. Notwithstanding, I added migration instructions below. This is a small step towards a world where we completely combine the converts of renderers and formatters, as, in my eyes, they do the same thing (carry the style information w/o the text). Possible future (but definitely breaking) changes: remove renderers entirely and use formatters directly consider renaming formatters to renderers (or not) rename TextPaint as the name does not make sense in any hierarchical model we use (what I am really interested in): allow the creation of text_ and text_box_ components directly from renderers + elements --- .../src/components/text_box_component.dart | 44 +++++++------ .../lib/src/components/text_component.dart | 23 ++----- .../lib/src/text/common/line_metrics.dart | 15 ++++- .../lib/src/text/formatter_text_renderer.dart | 34 ---------- .../lib/src/text/sprite_font_renderer.dart | 4 +- packages/flame/lib/src/text/text_paint.dart | 3 +- .../flame/lib/src/text/text_renderer.dart | 63 +++++++++++-------- .../components/text_box_component_test.dart | 4 +- .../flame/test/text/text_renderer_test.dart | 25 +++++--- .../example/lib/system/kawabunga_system.dart | 2 +- 10 files changed, 102 insertions(+), 115 deletions(-) delete mode 100644 packages/flame/lib/src/text/formatter_text_renderer.dart diff --git a/packages/flame/lib/src/components/text_box_component.dart b/packages/flame/lib/src/components/text_box_component.dart index a6dc19a91d..ceeaadbe8b 100644 --- a/packages/flame/lib/src/components/text_box_component.dart +++ b/packages/flame/lib/src/components/text_box_component.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:math' as math; +import 'dart:math'; import 'dart:ui'; import 'package:collection/collection.dart'; @@ -133,18 +134,18 @@ class TextBoxComponent extends TextComponent { @internal void updateBounds() { lines.clear(); - double? lineHeight; + var lineHeight = 0.0; final maxBoxWidth = _fixedSize ? width : _boxConfig.maxWidth; text.split(' ').forEach((word) { final wordLines = word.split('\n'); final possibleLine = lines.isEmpty ? wordLines[0] : '${lines.last} ${wordLines[0]}'; - lineHeight ??= textRenderer.measureTextHeight(possibleLine); + final metrics = textRenderer.getLineMetrics(possibleLine); + lineHeight = max(lineHeight, metrics.height); - final textWidth = textRenderer.measureTextWidth(possibleLine); - _updateMaxWidth(textWidth); - var canAppend = false; - if (textWidth <= maxBoxWidth - _boxConfig.margins.horizontal) { + _updateMaxWidth(metrics.width); + final bool canAppend; + if (metrics.width <= maxBoxWidth - _boxConfig.margins.horizontal) { canAppend = lines.isNotEmpty; } else { canAppend = lines.isNotEmpty && lines.last == ''; @@ -161,7 +162,7 @@ class TextBoxComponent extends TextComponent { } }); _totalLines = lines.length; - _lineHeight = lineHeight ?? 0.0; + _lineHeight = lineHeight; size = _recomputeSize(); } @@ -197,9 +198,11 @@ class TextBoxComponent extends TextComponent { } double getLineWidth(String line, int charCount) { - return textRenderer.measureTextWidth( - line.substring(0, math.min(charCount, line.length)), - ); + return textRenderer + .getLineMetrics( + line.substring(0, math.min(charCount, line.length)), + ) + .width; } Vector2 _recomputeSize() { @@ -269,17 +272,18 @@ class TextBoxComponent extends TextComponent { final nChars = math.min(currentChar - charCount, line.length); line = line.substring(0, nChars); } - textRenderer.render( - canvas, - line, - Vector2( - boxConfig.margins.left + - (boxWidth - textRenderer.measureTextWidth(line)) * align.x, - boxConfig.margins.top + - (boxHeight - nLines * _lineHeight) * align.y + - i * _lineHeight, - ), + + final textElement = textRenderer.formatter.format(line); + final metrics = textElement.metrics; + + final position = Vector2( + boxConfig.margins.left + (boxWidth - metrics.width) * align.x, + boxConfig.margins.top + + (boxHeight - nLines * _lineHeight) * align.y + + i * _lineHeight, ); + textRenderer.renderElement(canvas, textElement, position, anchor: anchor); + charCount += lines[i].length; } } diff --git a/packages/flame/lib/src/components/text_component.dart b/packages/flame/lib/src/components/text_component.dart index a1e20ae851..05ff2465e6 100644 --- a/packages/flame/lib/src/components/text_component.dart +++ b/packages/flame/lib/src/components/text_component.dart @@ -2,7 +2,6 @@ import 'dart:ui'; import 'package:flame/components.dart'; import 'package:flame/src/text/elements/text_element.dart'; -import 'package:flame/src/text/formatter_text_renderer.dart'; import 'package:flame/src/text/text_renderer.dart'; import 'package:flutter/painting.dart'; import 'package:meta/meta.dart'; @@ -40,28 +39,18 @@ class TextComponent extends PositionComponent { updateBounds(); } - TextElement? _textElement; + late TextElement _textElement; @internal void updateBounds() { - if (_textRenderer is FormatterTextRenderer) { - _textElement = - (_textRenderer as FormatterTextRenderer).formatter.format(_text); - final measurements = _textElement!.metrics; - _textElement!.translate(0, measurements.ascent); - size.setValues(measurements.width, measurements.height); - } else { - final expectedSize = textRenderer.measureText(_text); - size.setValues(expectedSize.x, expectedSize.y); - } + _textElement = _textRenderer.formatter.format(_text); + final measurements = _textElement.metrics; + _textElement.translate(0, measurements.ascent); + size.setValues(measurements.width, measurements.height); } @override void render(Canvas canvas) { - if (_textElement != null) { - _textElement!.render(canvas); - } else { - _textRenderer.render(canvas, text, Vector2.zero()); - } + _textElement.render(canvas); } } diff --git a/packages/flame/lib/src/text/common/line_metrics.dart b/packages/flame/lib/src/text/common/line_metrics.dart index 58e68a30fc..948f514d21 100644 --- a/packages/flame/lib/src/text/common/line_metrics.dart +++ b/packages/flame/lib/src/text/common/line_metrics.dart @@ -1,4 +1,4 @@ -import 'dart:ui'; +import 'package:flame/extensions.dart'; /// The [LineMetrics] object contains measurements of a text line. /// @@ -22,7 +22,9 @@ class LineMetrics { _width = width, _ascent = ascent ?? (height == null ? 0 : height - (descent ?? 0)), _descent = - descent ?? (height == null ? 0 : height - (ascent ?? height)); + descent ?? (height == null ? 0 : height - (ascent ?? height)) { + _updateSize(); + } /// X-coordinate of the left edge of the box. double get left => _left; @@ -50,6 +52,9 @@ class LineMetrics { double get bottom => baseline + descent; double get height => ascent + descent; + final Vector2 _size = Vector2.zero(); + Vector2 get size => _size.clone(); + /// Moves the [LineMetrics] box by the specified offset [dx], [dy] leaving its /// width and height unmodified. void translate(double dx, double dy) { @@ -69,6 +74,7 @@ class LineMetrics { void setLeftEdge(double x) { _width = right - x; _left = x; + _updateSize(); } /// Appends another [LineMetrics] box that is adjacent to the current and on @@ -86,6 +92,11 @@ class LineMetrics { if (_descent < other.descent) { _descent = other.descent; } + _updateSize(); + } + + void _updateSize() { + _size.setValues(width, height); } Rect toRect() => Rect.fromLTWH(left, top, width, height); diff --git a/packages/flame/lib/src/text/formatter_text_renderer.dart b/packages/flame/lib/src/text/formatter_text_renderer.dart deleted file mode 100644 index bb0ce4073f..0000000000 --- a/packages/flame/lib/src/text/formatter_text_renderer.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'dart:ui'; - -import 'package:flame/src/anchor.dart'; -import 'package:flame/text.dart'; -import 'package:vector_math/vector_math_64.dart'; - -/// Helper class that implements a [TextRenderer] using a [TextFormatter]. -class FormatterTextRenderer extends TextRenderer { - FormatterTextRenderer(this.formatter); - - final T formatter; - - @override - Vector2 measureText(String text) { - final box = formatter.format(text).metrics; - return Vector2(box.width, box.height); - } - - @override - void render( - Canvas canvas, - String text, - Vector2 position, { - Anchor anchor = Anchor.topLeft, - }) { - final txt = formatter.format(text); - final box = txt.metrics; - txt.translate( - position.x - box.width * anchor.x, - position.y - box.height * anchor.y - box.top, - ); - txt.render(canvas); - } -} diff --git a/packages/flame/lib/src/text/sprite_font_renderer.dart b/packages/flame/lib/src/text/sprite_font_renderer.dart index c7d1cab4cc..a67a78d95c 100644 --- a/packages/flame/lib/src/text/sprite_font_renderer.dart +++ b/packages/flame/lib/src/text/sprite_font_renderer.dart @@ -1,7 +1,6 @@ import 'dart:ui'; import 'package:flame/src/text/common/sprite_font.dart'; -import 'package:flame/src/text/formatter_text_renderer.dart'; import 'package:flame/src/text/formatters/sprite_font_text_formatter.dart'; import 'package:flame/src/text/text_renderer.dart'; @@ -22,8 +21,7 @@ import 'package:flame/src/text/text_renderer.dart'; /// The `paint` parameter is used to composite the character images onto the /// canvas. Its default value will draw the character images as-is. Changing /// the opacity of the paint's color will make the text semi-transparent. -class SpriteFontRenderer - extends FormatterTextRenderer { +class SpriteFontRenderer extends TextRenderer { SpriteFontRenderer.fromFont( SpriteFont font, { Color? color, diff --git a/packages/flame/lib/src/text/text_paint.dart b/packages/flame/lib/src/text/text_paint.dart index 5fad6414d7..799c03d39d 100644 --- a/packages/flame/lib/src/text/text_paint.dart +++ b/packages/flame/lib/src/text/text_paint.dart @@ -1,5 +1,4 @@ import 'package:flame/src/cache/memory_cache.dart'; -import 'package:flame/src/text/formatter_text_renderer.dart'; import 'package:flame/src/text/formatters/text_painter_text_formatter.dart'; import 'package:flame/src/text/text_renderer.dart'; import 'package:flutter/rendering.dart'; @@ -10,7 +9,7 @@ import 'package:flutter/rendering.dart'; /// modified dynamically, if you need to change any attribute of the text at /// runtime, such as color, then create a new [TextPaint] object using /// [copyWith]. -class TextPaint extends FormatterTextRenderer { +class TextPaint extends TextRenderer { TextPaint({ TextStyle? style, TextDirection? textDirection, diff --git a/packages/flame/lib/src/text/text_renderer.dart b/packages/flame/lib/src/text/text_renderer.dart index c47393de29..494aa137a7 100644 --- a/packages/flame/lib/src/text/text_renderer.dart +++ b/packages/flame/lib/src/text/text_renderer.dart @@ -1,50 +1,61 @@ import 'dart:ui'; import 'package:flame/src/anchor.dart'; -import 'package:flame/src/components/text_box_component.dart'; -import 'package:flame/src/components/text_component.dart'; -import 'package:flame/src/text/sprite_font_renderer.dart'; -import 'package:flame/src/text/text_paint.dart'; +import 'package:flame/text.dart'; import 'package:vector_math/vector_math_64.dart'; -/// [TextRenderer] is the abstract API for drawing text. +/// [TextRenderer] is the most basic API for drawing text. /// -/// A text renderer usually embodies a particular style for rendering text, such -/// as font-family, color, size, and so on. At the same time, a text renderer -/// is not tied to a specific string -- it can render any text fragment that -/// you give it. +/// A text renderer contains a [formatter] that embodies a particular style +/// for rendering text, such as font-family, color, size, and so on. +/// At the same time, nor the text renderer or the [formatter] are tied to a +/// specific string -- it can render any text fragment that you give it. /// /// A text renderer object has two functions: to measure the size of a text /// string that it will have when rendered, and to actually render that string /// onto a canvas. /// /// [TextRenderer] is a low-level API that may be somewhat inconvenient to use -/// directly. Instead, consider using components such as [TextComponent] or -/// [TextBoxComponent]. +/// directly. Instead, consider using components such as TextComponent or +/// TextBoxComponent. /// -/// The following text renderers are available in Flame: -/// - [TextPaint] which uses the standard Flutter's `TextPainter`; -/// - [SpriteFontRenderer] which uses a spritesheet as a font file; -abstract class TextRenderer { - /// Compute the dimensions of [text] when rendered. - Vector2 measureText(String text); +/// See [TextFormatter] for more information about existing options. +class TextRenderer { + TextRenderer(this.formatter); - /// Compute the width of [text] when rendered. - double measureTextWidth(String text) => measureText(text).x; + final T formatter; - /// Compute the height of [text] when rendered. - double measureTextHeight(String text) => measureText(text).y; + TextElement format(String text) { + return formatter.format(text); + } + + LineMetrics getLineMetrics(String text) { + return format(text).metrics; + } - /// Renders [text] on the [canvas] at a given [position]. - /// - /// For example, if [Anchor.center] is specified, it's going to be drawn - /// centered around [position]. void render( Canvas canvas, String text, Vector2 position, { Anchor anchor = Anchor.topLeft, - }); + }) { + final element = format(text); + renderElement(canvas, element, position, anchor: anchor); + } + + void renderElement( + Canvas canvas, + TextElement element, + Vector2 position, { + Anchor anchor = Anchor.topLeft, + }) { + final box = element.metrics; + element.translate( + position.x - box.width * anchor.x, + position.y - box.height * anchor.y - box.top, + ); + element.render(canvas); + } /// A registry containing default providers for every [TextRenderer] subclass; /// used by [createDefault] to create default parameter values. diff --git a/packages/flame/test/components/text_box_component_test.dart b/packages/flame/test/components/text_box_component_test.dart index 912fc978fe..ceaac33ca2 100644 --- a/packages/flame/test/components/text_box_component_test.dart +++ b/packages/flame/test/components/text_box_component_test.dart @@ -3,7 +3,7 @@ import 'dart:ui' hide TextStyle; import 'package:canvas_test/canvas_test.dart'; import 'package:flame/components.dart'; import 'package:flame/palette.dart'; -import 'package:flame/src/text/formatter_text_renderer.dart'; +import 'package:flame/text.dart'; import 'package:flame_test/flame_test.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -193,7 +193,7 @@ class _FramedTextBox extends TextBoxComponent { super.position, super.size, }) : super( - textRenderer: FormatterTextRenderer(DebugTextFormatter(fontSize: 22)), + textRenderer: TextRenderer(DebugTextFormatter(fontSize: 22)), ); final Paint _borderPaint = Paint() diff --git a/packages/flame/test/text/text_renderer_test.dart b/packages/flame/test/text/text_renderer_test.dart index 9ed5235a45..750027c14d 100644 --- a/packages/flame/test/text/text_renderer_test.dart +++ b/packages/flame/test/text/text_renderer_test.dart @@ -32,15 +32,24 @@ void main() { }); } -class _CustomTextRenderer extends TextRenderer { +class _CustomTextRenderer extends TextRenderer<_CustomTextFormatter> { + _CustomTextRenderer() : super(_CustomTextFormatter()); +} + +class _CustomTextFormatter extends TextFormatter { + @override + TextElement format(String text) { + return CustomTextElement(); + } +} + +class CustomTextElement extends TextElement { + @override + LineMetrics get metrics => LineMetrics(); + @override - Vector2 measureText(String text) => Vector2.zero(); + void render(Canvas canvas) {} @override - void render( - Canvas canvas, - String text, - Vector2 position, { - Anchor anchor = Anchor.topLeft, - }) {} + void translate(double dx, double dy) {} } diff --git a/packages/flame_oxygen/example/lib/system/kawabunga_system.dart b/packages/flame_oxygen/example/lib/system/kawabunga_system.dart index e96c06b8ec..1173c703f3 100644 --- a/packages/flame_oxygen/example/lib/system/kawabunga_system.dart +++ b/packages/flame_oxygen/example/lib/system/kawabunga_system.dart @@ -33,7 +33,7 @@ class KawabungaSystem extends BaseSystem with UpdateSystem { final textComponent = entity.get()!; final size = entity.get()!.size; final textRenderer = TextPaint(style: textComponent.style); - size.setFrom(textRenderer.measureText(textComponent.text)); + size.setFrom(textRenderer.getLineMetrics(textComponent.text).size); final timer = entity.get()!; timer.timePassed = timer.timePassed + delta;