From c44272be45eadfabc8f03ef250eb663e59ef2aab Mon Sep 17 00:00:00 2001 From: Pasha Stetsenko Date: Mon, 27 Jun 2022 11:06:14 -0700 Subject: [PATCH] feat: Added TextFormatter classes (#1720) A TextFormatter is a class that can take a string of text and convert it into a TextElement. A TextElement is an object that encapsulates a text string prepared for rendering. This object knows its own dimensions (layout), and is able to render itself on a canvas. A TextFormatter is different from the current TextRenderer in one crucial respect: whereas TextRenderer takes a string of text and draws it onto the canvas directly (performing the layout and measurement internally), the TextFormatter creates an object that encapsulates the information necessary for rendering. Thus, TextFormatter's output can be used to prepare the text once, and then render multiple times. In addition, since each TextElement knows its own layout, -- these individual layouts can be collected, manipulated, and arranged into a larger text body necessary for rich text rendering. In addition, this PR: Implements a debug version of the standard text renderer, which draws rectangles instead of text; Re-enables the skipped golden test for the TextBoxComponent (hopefully it'll work this time); Refactors existing TextRenderers to use the new TextFormatters under the hood. This PR is a WIP for the Rich Text functionality (#1627). --- .../flame/lib/src/text/common/glyph_data.dart | 19 +++ .../flame/lib/src/text/common/glyph_info.dart | 14 ++ .../lib/src/text/common/line_metrics.dart | 98 ++++++++++++ .../flame/lib/src/text/common/text_line.dart | 15 ++ .../lib/src/text/formatter_text_renderer.dart | 35 +++++ .../sprite_font_text_formatter.dart | 83 ++++++++++ .../src/text/formatters/text_formatter.dart | 7 + .../text_painter_text_formatter.dart | 38 +++++ .../debug_text_painter_text_element.dart | 21 +++ .../text/inline/sprite_font_text_element.dart | 42 +++++ .../lib/src/text/inline/text_element.dart | 22 +++ .../inline/text_painter_text_element.dart | 33 ++++ .../lib/src/text/sprite_font_renderer.dart | 145 +++--------------- packages/flame/lib/src/text/text_paint.dart | 47 ++---- packages/flame/lib/text.dart | 3 +- .../_goldens/text_box_component_test_1.png | Bin 36900 -> 28670 bytes .../components/text_box_component_test.dart | 3 +- .../test/text/common/line_metrics_test.dart | 122 +++++++++++++++ .../test/text/sprite_font_renderer_test.dart | 13 +- 19 files changed, 596 insertions(+), 164 deletions(-) create mode 100644 packages/flame/lib/src/text/common/glyph_data.dart create mode 100644 packages/flame/lib/src/text/common/glyph_info.dart create mode 100644 packages/flame/lib/src/text/common/line_metrics.dart create mode 100644 packages/flame/lib/src/text/common/text_line.dart create mode 100644 packages/flame/lib/src/text/formatter_text_renderer.dart create mode 100644 packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart create mode 100644 packages/flame/lib/src/text/formatters/text_formatter.dart create mode 100644 packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart create mode 100644 packages/flame/lib/src/text/inline/debug_text_painter_text_element.dart create mode 100644 packages/flame/lib/src/text/inline/sprite_font_text_element.dart create mode 100644 packages/flame/lib/src/text/inline/text_element.dart create mode 100644 packages/flame/lib/src/text/inline/text_painter_text_element.dart create mode 100644 packages/flame/test/text/common/line_metrics_test.dart diff --git a/packages/flame/lib/src/text/common/glyph_data.dart b/packages/flame/lib/src/text/common/glyph_data.dart new file mode 100644 index 000000000..2dcdd9546 --- /dev/null +++ b/packages/flame/lib/src/text/common/glyph_data.dart @@ -0,0 +1,19 @@ +class GlyphData { + const GlyphData({ + required this.left, + required this.top, + this.right, + this.bottom, + }); + + const GlyphData.fromLTWH(this.left, this.top, double width, double height) + : right = left + width, + bottom = top + height; + + const GlyphData.fromLTRB(this.left, this.top, this.right, this.bottom); + + final double left; + final double top; + final double? right; + final double? bottom; +} diff --git a/packages/flame/lib/src/text/common/glyph_info.dart b/packages/flame/lib/src/text/common/glyph_info.dart new file mode 100644 index 000000000..6f836805c --- /dev/null +++ b/packages/flame/lib/src/text/common/glyph_info.dart @@ -0,0 +1,14 @@ +/// Helper class that stores dimensions of a single glyph for a spritesheet- +/// based font. +class GlyphInfo { + double srcLeft = 0; + double srcTop = 0; + double srcRight = 0; + double srcBottom = 0; + double rstSCos = 1; + double rstSSin = 0; + double rstTx = 0; + double rstTy = 0; + double width = 0; + double height = 0; +} diff --git a/packages/flame/lib/src/text/common/line_metrics.dart b/packages/flame/lib/src/text/common/line_metrics.dart new file mode 100644 index 000000000..1d2e6c752 --- /dev/null +++ b/packages/flame/lib/src/text/common/line_metrics.dart @@ -0,0 +1,98 @@ +import 'dart:ui'; + +import 'package:flame/src/text/common/text_line.dart'; + +/// The [LineMetrics] object contains measurements of a [TextLine]. +/// +/// A line of text can be thought of as surrounded by a box (rect) that outlines +/// the boundaries of the text, plus there is a [baseline] inside the box which +/// is the line on top of which the text is placed. +/// +/// The [LineMetrics] box surrounding a piece of text is not necessarily tight: +/// there's usually some amount of space above and below the text glyphs to +/// improve legibility of multi-line text. +class LineMetrics { + LineMetrics({ + double left = 0, + double baseline = 0, + double width = 0, + double? ascent, + double? descent, + double? height, + }) : _left = left, + _baseline = baseline, + _width = width, + _ascent = ascent ?? (height == null ? 0 : height - (descent ?? 0)), + _descent = + descent ?? (height == null ? 0 : height - (ascent ?? height)); + + /// X-coordinate of the left edge of the box. + double get left => _left; + double _left; + + /// Y-coordinate of the baseline of the box. When several line fragments are + /// placed next to each other, their baselines will match. + double get baseline => _baseline; + double _baseline; + + /// The total width of the box. + double get width => _width; + double _width; + + /// The distance from the baseline to the top of the box. + double get ascent => _ascent; + double _ascent; + + /// The distance from the baseline to the bottom of the box. + double get descent => _descent; + double _descent; + + double get right => left + width; + double get top => baseline - ascent; + double get bottom => baseline + descent; + double get height => ascent + descent; + + /// Moves the [LineMetrics] box by the specified offset [dx], [dy] leaving its + /// width and height unmodified. + void translate(double dx, double dy) { + _left += dx; + _baseline += dy; + } + + /// Moves this [LineMetrics] box to the origin, setting [left] and [baseline] + /// to 0. + void moveToOrigin() { + _left = 0; + _baseline = 0; + } + + /// Sets the position of the left edge of this [LineMetrics] box, leaving the + /// [right] edge in place. + void setLeftEdge(double x) { + _width = right - x; + _left = x; + } + + /// Appends another [LineMetrics] box that is adjacent to the current and on + /// the same baseline. The current object will be modified to encompass the + /// [other] box. + void append(LineMetrics other) { + assert( + baseline == other.baseline, + 'Baselines do not match: $baseline vs ${other.baseline}', + ); + _width = other.right - left; + if (_ascent < other.ascent) { + _ascent = other.ascent; + } + if (_descent < other.descent) { + _descent = other.descent; + } + } + + Rect toRect() => Rect.fromLTWH(left, top, width, height); + + @override + String toString() => 'LineMetrics(left: $left, baseline: $baseline, ' + 'width: $width, ascent: $ascent, descent: $descent)'; +} diff --git a/packages/flame/lib/src/text/common/text_line.dart b/packages/flame/lib/src/text/common/text_line.dart new file mode 100644 index 000000000..632df774b --- /dev/null +++ b/packages/flame/lib/src/text/common/text_line.dart @@ -0,0 +1,15 @@ +import 'package:flame/src/text/common/line_metrics.dart'; +import 'package:flame/src/text/inline/text_element.dart'; + +/// [TextLine] is an abstract class describing a single line (or a fragment of +/// a line) of a laid-out text. +/// +/// More specifically, after any [TextElement] has been laid out, its layout +/// will be described by one or more [TextLine]s. +abstract class TextLine { + /// The dimensions of this line. + LineMetrics get metrics; + + /// Move the text within this [TextLine] by the specified offsets [dx], [dy]. + void translate(double dx, double dy); +} diff --git a/packages/flame/lib/src/text/formatter_text_renderer.dart b/packages/flame/lib/src/text/formatter_text_renderer.dart new file mode 100644 index 000000000..bc1650329 --- /dev/null +++ b/packages/flame/lib/src/text/formatter_text_renderer.dart @@ -0,0 +1,35 @@ +import 'dart:ui'; + +import 'package:flame/src/anchor.dart'; +import 'package:flame/src/text/formatters/text_formatter.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).lastLine.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.lastLine.metrics; + txt.lastLine.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/formatters/sprite_font_text_formatter.dart b/packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart new file mode 100644 index 000000000..8f43ae7ee --- /dev/null +++ b/packages/flame/lib/src/text/formatters/sprite_font_text_formatter.dart @@ -0,0 +1,83 @@ +import 'dart:typed_data'; +import 'dart:ui' hide LineMetrics; + +import 'package:flame/src/text/common/glyph_data.dart'; +import 'package:flame/src/text/common/glyph_info.dart'; +import 'package:flame/src/text/common/line_metrics.dart'; +import 'package:flame/src/text/formatters/text_formatter.dart'; +import 'package:flame/src/text/inline/sprite_font_text_element.dart'; + +class SpriteFontTextFormatter extends TextFormatter { + SpriteFontTextFormatter({ + required this.source, + required double charWidth, + required double charHeight, + required Map glyphs, + this.scale = 1, + this.letterSpacing = 0, + }) : scaledCharWidth = charWidth * scale, + scaledCharHeight = charHeight * scale, + _glyphs = glyphs.map((char, rect) { + assert( + char.length == 1, + 'A glyph must have a single character: "$char"', + ); + final info = GlyphInfo(); + info.srcLeft = rect.left; + info.srcTop = rect.top; + info.srcRight = rect.right ?? rect.left + charWidth; + info.srcBottom = rect.bottom ?? rect.top + charHeight; + info.rstSCos = scale; + info.rstTy = (charHeight - (info.srcBottom - info.srcTop)) * scale; + info.width = charWidth * scale; + info.height = charHeight * scale; + return MapEntry(char.codeUnitAt(0), info); + }); + + final Image source; + final paint = Paint()..color = const Color(0xFFFFFFFF); + final double letterSpacing; + final double scale; + final double scaledCharWidth; + final double scaledCharHeight; + final Map _glyphs; + + @override + SpriteFontTextElement format(String text) { + final rstTransforms = Float32List(4 * text.length); + final rects = Float32List(4 * text.length); + var j = 0; + var x0 = 0.0; + final y0 = -scaledCharHeight; + for (final glyph in _textToGlyphs(text)) { + rects[j + 0] = glyph.srcLeft; + rects[j + 1] = glyph.srcTop; + rects[j + 2] = glyph.srcRight; + rects[j + 3] = glyph.srcBottom; + rstTransforms[j + 0] = glyph.rstSCos; + rstTransforms[j + 1] = glyph.rstSSin; + rstTransforms[j + 2] = x0 + glyph.rstTx; + rstTransforms[j + 3] = y0 + glyph.rstTy; + x0 += glyph.width + letterSpacing; + j += 4; + } + return SpriteFontTextElement( + source: source, + transforms: rstTransforms, + rects: rects, + paint: paint, + metrics: LineMetrics(width: x0, height: scaledCharHeight, descent: 0), + ); + } + + Iterable _textToGlyphs(String text) { + return text.codeUnits.map((int i) { + final glyph = _glyphs[i]; + assert( + glyph != null, + 'No glyph for character "${String.fromCharCode(i)}"', + ); + return glyph!; + }); + } +} diff --git a/packages/flame/lib/src/text/formatters/text_formatter.dart b/packages/flame/lib/src/text/formatters/text_formatter.dart new file mode 100644 index 000000000..46b30f890 --- /dev/null +++ b/packages/flame/lib/src/text/formatters/text_formatter.dart @@ -0,0 +1,7 @@ +import 'package:flame/src/text/inline/text_element.dart'; + +/// [TextFormatter] is an abstract interface for a class that can convert an +/// arbitrary string of text into a renderable [TextElement]. +abstract class TextFormatter { + TextElement format(String text); +} diff --git a/packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart b/packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart new file mode 100644 index 000000000..88daa3745 --- /dev/null +++ b/packages/flame/lib/src/text/formatters/text_painter_text_formatter.dart @@ -0,0 +1,38 @@ +import 'package:flame/src/text/formatters/text_formatter.dart'; +import 'package:flame/src/text/inline/debug_text_painter_text_element.dart'; +import 'package:flame/src/text/inline/text_painter_text_element.dart'; +import 'package:flutter/rendering.dart'; + +/// [TextPainterTextFormatter] applies a Flutter [TextStyle] to a string of +/// text, creating a [TextPainterTextElement]. +/// +/// If the [debugMode] is true, this formatter will wrap the text with a +/// [DebugTextPainterTextElement] instead. This mode is mostly useful for tests. +class TextPainterTextFormatter extends TextFormatter { + TextPainterTextFormatter({ + required this.style, + this.textDirection = TextDirection.ltr, + this.debugMode = false, + }); + + final TextStyle style; + final TextDirection textDirection; + final bool debugMode; + + @override + TextPainterTextElement format(String text) { + final tp = _textToTextPainter(text); + if (debugMode) { + return DebugTextPainterTextElement(tp); + } else { + return TextPainterTextElement(tp); + } + } + + TextPainter _textToTextPainter(String text) { + return TextPainter( + text: TextSpan(text: text, style: style), + textDirection: textDirection, + )..layout(); + } +} diff --git a/packages/flame/lib/src/text/inline/debug_text_painter_text_element.dart b/packages/flame/lib/src/text/inline/debug_text_painter_text_element.dart new file mode 100644 index 000000000..9d9c076db --- /dev/null +++ b/packages/flame/lib/src/text/inline/debug_text_painter_text_element.dart @@ -0,0 +1,21 @@ +import 'dart:ui'; + +import 'package:flame/src/text/inline/text_painter_text_element.dart'; + +/// Replacement class for [TextPainterTextElement] which draws solid rectangles +/// instead of regular text. +/// +/// This class is useful for testing purposes: different test environments may +/// have slightly different font definitions and mechanisms for anti-aliased +/// font rendering, which makes it impossible to create golden tests with +/// regular text painter. +class DebugTextPainterTextElement extends TextPainterTextElement { + DebugTextPainterTextElement(super.textPainter); + + final paint = Paint()..color = const Color(0xFFFFFFFF); + + @override + void render(Canvas canvas) { + canvas.drawRect(metrics.toRect(), paint); + } +} diff --git a/packages/flame/lib/src/text/inline/sprite_font_text_element.dart b/packages/flame/lib/src/text/inline/sprite_font_text_element.dart new file mode 100644 index 000000000..9eb0c7862 --- /dev/null +++ b/packages/flame/lib/src/text/inline/sprite_font_text_element.dart @@ -0,0 +1,42 @@ +import 'dart:typed_data'; +import 'dart:ui'; + +import 'package:flame/src/text/common/line_metrics.dart'; +import 'package:flame/src/text/common/text_line.dart'; +import 'package:flame/src/text/inline/text_element.dart'; + +class SpriteFontTextElement extends TextElement implements TextLine { + SpriteFontTextElement({ + required this.source, + required this.transforms, + required this.rects, + required this.paint, + required LineMetrics metrics, + }) : _box = metrics; + + final Image source; + final Float32List transforms; + final Float32List rects; + final Paint paint; + final LineMetrics _box; + + @override + TextLine get lastLine => this; + + @override + LineMetrics get metrics => _box; + + @override + void translate(double dx, double dy) { + _box.translate(dx, dy); + for (var i = 0; i < transforms.length; i += 4) { + transforms[i + 2] += dx; + transforms[i + 3] += dy; + } + } + + @override + void render(Canvas canvas) { + canvas.drawRawAtlas(source, transforms, rects, null, null, null, paint); + } +} diff --git a/packages/flame/lib/src/text/inline/text_element.dart b/packages/flame/lib/src/text/inline/text_element.dart new file mode 100644 index 000000000..3aa551ea1 --- /dev/null +++ b/packages/flame/lib/src/text/inline/text_element.dart @@ -0,0 +1,22 @@ +import 'dart:ui' hide LineMetrics; + +import 'package:flame/src/text/common/text_line.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 { + TextLine get lastLine; + + /// Renders the text on the [canvas], at positions determined during the + /// layout. + /// + /// This method should only be invoked after the text was laid out. + /// + /// In order to render the text at a different location, consider applying a + /// translation transform to the canvas. + void render(Canvas canvas); +} diff --git a/packages/flame/lib/src/text/inline/text_painter_text_element.dart b/packages/flame/lib/src/text/inline/text_painter_text_element.dart new file mode 100644 index 000000000..4d0b62aa2 --- /dev/null +++ b/packages/flame/lib/src/text/inline/text_painter_text_element.dart @@ -0,0 +1,33 @@ +import 'dart:ui'; + +import 'package:flame/src/text/common/line_metrics.dart'; +import 'package:flame/src/text/common/text_line.dart'; +import 'package:flame/src/text/inline/text_element.dart'; +import 'package:flutter/rendering.dart' show TextBaseline, TextPainter; + +class TextPainterTextElement extends TextElement implements TextLine { + TextPainterTextElement(this._textPainter) + : _box = LineMetrics( + ascent: _textPainter + .computeDistanceToActualBaseline(TextBaseline.alphabetic), + width: _textPainter.width, + height: _textPainter.height, + ); + + final TextPainter _textPainter; + final LineMetrics _box; + + @override + LineMetrics get metrics => _box; + + @override + TextLine get lastLine => this; + + @override + void translate(double dx, double dy) => _box.translate(dx, dy); + + @override + void render(Canvas canvas) { + _textPainter.paint(canvas, Offset(_box.left, _box.top)); + } +} diff --git a/packages/flame/lib/src/text/sprite_font_renderer.dart b/packages/flame/lib/src/text/sprite_font_renderer.dart index f8dbe098c..b05dd44c9 100644 --- a/packages/flame/lib/src/text/sprite_font_renderer.dart +++ b/packages/flame/lib/src/text/sprite_font_renderer.dart @@ -1,9 +1,9 @@ -import 'dart:typed_data'; import 'dart:ui'; -import 'package:flame/src/anchor.dart'; +import 'package:flame/src/text/common/glyph_data.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'; -import 'package:vector_math/vector_math_64.dart'; /// [TextRenderer] implementation that uses a spritesheet of various font glyphs /// to render text. @@ -14,134 +14,31 @@ import 'package:vector_math/vector_math_64.dart'; /// /// Currently, this class supports monospace fonts only -- the widths and the /// heights of all characters must be the same. -/// Extra space between letters can be added via the [letterSpacing] parameter +/// Extra space between letters can be added via the `letterSpacing` parameter /// (it can also be negative to "squish" characters together). Finally, the -/// [scale] parameter allows scaling the font to be bigger/smaller relative to +/// `scale` parameter allows scaling the font to be bigger/smaller relative to /// its size in the source image. /// -/// The [paint] parameter is used to composite the character images onto the +/// 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 TextRenderer { +class SpriteFontRenderer + extends FormatterTextRenderer { SpriteFontRenderer({ - required this.source, + required Image source, required double charWidth, required double charHeight, required Map glyphs, - this.scale = 1, - this.letterSpacing = 0, - }) : scaledCharWidth = charWidth * scale, - scaledCharHeight = charHeight * scale, - _glyphs = glyphs.map((char, rect) { - assert( - char.length == 1, - 'A glyph must have a single character: "$char"', - ); - final info = _GlyphInfo(); - info.srcLeft = rect.left; - info.srcTop = rect.top; - info.srcRight = rect.right ?? rect.left + charWidth; - info.srcBottom = rect.bottom ?? rect.top + charHeight; - info.rstSCos = scale; - info.rstTy = (charHeight - (info.srcBottom - info.srcTop)) * scale; - info.width = charWidth * scale; - info.height = charHeight * scale; - return MapEntry(char.codeUnitAt(0), info); - }); - - final Image source; - final Map _glyphs; - final double letterSpacing; - final double scale; - final double scaledCharWidth; - final double scaledCharHeight; - bool get isMonospace => true; - - Paint paint = Paint()..color = const Color(0xFFFFFFFF); - - @override - double measureTextHeight(String text) => scaledCharHeight; - - @override - double measureTextWidth(String text) { - final n = text.length; - return n > 0 ? scaledCharWidth * n + letterSpacing * (n - 1) : 0; - } - - @override - Vector2 measureText(String text) { - return Vector2(measureTextWidth(text), measureTextHeight(text)); - } - - @override - void render( - Canvas canvas, - String text, - Vector2 position, { - Anchor anchor = Anchor.topLeft, - }) { - final rstTransforms = Float32List(4 * text.length); - final rects = Float32List(4 * text.length); - var j = 0; - var x0 = position.x; - final y0 = position.y; - for (final glyph in _textToGlyphs(text)) { - rects[j + 0] = glyph.srcLeft; - rects[j + 1] = glyph.srcTop; - rects[j + 2] = glyph.srcRight; - rects[j + 3] = glyph.srcBottom; - rstTransforms[j + 0] = glyph.rstSCos; - rstTransforms[j + 1] = glyph.rstSSin; - rstTransforms[j + 2] = x0 + glyph.rstTx; - rstTransforms[j + 3] = y0 + glyph.rstTy; - x0 += glyph.width + letterSpacing; - j += 4; - } - canvas.drawRawAtlas(source, rstTransforms, rects, null, null, null, paint); - } - - Iterable<_GlyphInfo> _textToGlyphs(String text) { - return text.codeUnits.map(_getGlyphFromCodeUnit); - } - - _GlyphInfo _getGlyphFromCodeUnit(int i) { - final glyph = _glyphs[i]; - if (glyph == null) { - throw ArgumentError('No glyph for character "${String.fromCharCode(i)}"'); - } - return glyph; - } -} - -class GlyphData { - const GlyphData({ - required this.left, - required this.top, - this.right, - this.bottom, - }); - - const GlyphData.fromLTWH(this.left, this.top, double width, double height) - : right = left + width, - bottom = top + height; - - const GlyphData.fromLTRB(this.left, this.top, this.right, this.bottom); - - final double left; - final double top; - final double? right; - final double? bottom; -} - -class _GlyphInfo { - double srcLeft = 0; - double srcTop = 0; - double srcRight = 0; - double srcBottom = 0; - double rstSCos = 1; - double rstSSin = 0; - double rstTx = 0; - double rstTy = 0; - double width = 0; - double height = 0; + double scale = 1, + double letterSpacing = 0, + }) : super( + SpriteFontTextFormatter( + source: source, + charWidth: charWidth, + charHeight: charHeight, + glyphs: glyphs, + scale: scale, + letterSpacing: letterSpacing, + ), + ); } diff --git a/packages/flame/lib/src/text/text_paint.dart b/packages/flame/lib/src/text/text_paint.dart index 6a690a96e..a51e24cd0 100644 --- a/packages/flame/lib/src/text/text_paint.dart +++ b/packages/flame/lib/src/text/text_paint.dart @@ -1,6 +1,6 @@ -import 'package:flame/src/anchor.dart'; import 'package:flame/src/cache/memory_cache.dart'; -import 'package:flame/src/extensions/vector2.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,14 +10,19 @@ 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 TextRenderer { - TextPaint({TextStyle? style, TextDirection? textDirection}) - : style = style ?? defaultTextStyle, - textDirection = textDirection ?? TextDirection.ltr; +class TextPaint extends FormatterTextRenderer { + TextPaint({TextStyle? style, TextDirection? textDirection, bool? debugMode}) + : super( + TextPainterTextFormatter( + style: style ?? defaultTextStyle, + textDirection: textDirection ?? TextDirection.ltr, + debugMode: debugMode ?? false, + ), + ); - final TextStyle style; + TextStyle get style => formatter.style; - final TextDirection textDirection; + TextDirection get textDirection => formatter.textDirection; final MemoryCache _textPainterCache = MemoryCache(); @@ -27,27 +32,6 @@ class TextPaint extends TextRenderer { fontSize: 24, ); - @override - void render( - Canvas canvas, - String text, - Vector2 p, { - Anchor anchor = Anchor.topLeft, - }) { - final tp = toTextPainter(text); - final translatedPosition = Offset( - p.x - tp.width * anchor.x, - p.y - tp.height * anchor.y, - ); - tp.paint(canvas, translatedPosition); - } - - @override - Vector2 measureText(String text) { - final tp = toTextPainter(text); - return Vector2(tp.width, tp.height); - } - /// Returns a [TextPainter] that allows for text rendering and size /// measuring. /// @@ -79,6 +63,9 @@ class TextPaint extends TextRenderer { TextStyle Function(TextStyle) transform, { TextDirection? textDirection, }) { - return TextPaint(style: transform(style), textDirection: textDirection); + return TextPaint( + style: transform(formatter.style), + textDirection: textDirection ?? formatter.textDirection, + ); } } diff --git a/packages/flame/lib/text.dart b/packages/flame/lib/text.dart index a06be9325..2af56f3d7 100644 --- a/packages/flame/lib/text.dart +++ b/packages/flame/lib/text.dart @@ -1,3 +1,4 @@ -export 'src/text/sprite_font_renderer.dart' show SpriteFontRenderer, GlyphData; +export 'src/text/common/glyph_data.dart' show GlyphData; +export 'src/text/sprite_font_renderer.dart' show SpriteFontRenderer; export 'src/text/text_paint.dart' show TextPaint; export 'src/text/text_renderer.dart' show TextRenderer; diff --git a/packages/flame/test/_goldens/text_box_component_test_1.png b/packages/flame/test/_goldens/text_box_component_test_1.png index 800c7f40abcb24c239527c1983ae8edd48406b28..16ad6651b2386cb041583b4eaaafc4b7628310cc 100644 GIT binary patch literal 28670 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 box1.append(LineMetrics()), + failsAssert('Baselines do not match: 17.0 vs 0.0'), + ); + }); + + test('toString', () { + final box = LineMetrics(left: 33, baseline: 78, width: 101, height: 11); + expect( + box.toString(), + 'LineMetrics(left: 33.0, baseline: 78.0, width: 101.0, ascent: 11.0, ' + 'descent: 0.0)', + ); + }); + }); +} diff --git a/packages/flame/test/text/sprite_font_renderer_test.dart b/packages/flame/test/text/sprite_font_renderer_test.dart index 408746077..6a8b19e3a 100644 --- a/packages/flame/test/text/sprite_font_renderer_test.dart +++ b/packages/flame/test/text/sprite_font_renderer_test.dart @@ -12,15 +12,14 @@ void main() { group('SpriteFontRenderer', () { test('creating SpriteFontRenderer', () async { final renderer = await createRenderer(); - expect(renderer.source, isA()); - expect(renderer.scaledCharWidth, 6); - expect(renderer.scaledCharHeight, 6); - expect(renderer.letterSpacing, 0); - expect(renderer.isMonospace, true); + expect(renderer.formatter.source, isA()); + expect(renderer.formatter.scaledCharWidth, 6); + expect(renderer.formatter.scaledCharHeight, 6); + expect(renderer.formatter.letterSpacing, 0); expect( () => renderer.render(MockCanvas(), 'Ї', Vector2.zero()), - throwsArgumentError, + failsAssert('No glyph for character "Ї"'), ); }); @@ -43,7 +42,7 @@ void main() { TextComponent( text: 'FLAME', textRenderer: (await createRenderer(scale: 25)) - ..paint.color = const Color(0x44000000), + ..formatter.paint.color = const Color(0x44000000), position: Vector2(400, 500), anchor: Anchor.center, ),