mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 16:36:57 +08:00
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).
This commit is contained in:
19
packages/flame/lib/src/text/common/glyph_data.dart
Normal file
19
packages/flame/lib/src/text/common/glyph_data.dart
Normal file
@ -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;
|
||||
}
|
||||
14
packages/flame/lib/src/text/common/glyph_info.dart
Normal file
14
packages/flame/lib/src/text/common/glyph_info.dart
Normal file
@ -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;
|
||||
}
|
||||
98
packages/flame/lib/src/text/common/line_metrics.dart
Normal file
98
packages/flame/lib/src/text/common/line_metrics.dart
Normal file
@ -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)';
|
||||
}
|
||||
15
packages/flame/lib/src/text/common/text_line.dart
Normal file
15
packages/flame/lib/src/text/common/text_line.dart
Normal file
@ -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);
|
||||
}
|
||||
35
packages/flame/lib/src/text/formatter_text_renderer.dart
Normal file
35
packages/flame/lib/src/text/formatter_text_renderer.dart
Normal file
@ -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<T extends TextFormatter> 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);
|
||||
}
|
||||
}
|
||||
@ -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<String, GlyphData> 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<int, GlyphInfo> _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<GlyphInfo> _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!;
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
22
packages/flame/lib/src/text/inline/text_element.dart
Normal file
22
packages/flame/lib/src/text/inline/text_element.dart
Normal file
@ -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);
|
||||
}
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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<SpriteFontTextFormatter> {
|
||||
SpriteFontRenderer({
|
||||
required this.source,
|
||||
required Image source,
|
||||
required double charWidth,
|
||||
required double charHeight,
|
||||
required Map<String, GlyphData> 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<int, _GlyphInfo> _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,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@ -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<TextPainterTextFormatter> {
|
||||
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<String, TextPainter> _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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 28 KiB |
@ -119,7 +119,6 @@ void main() {
|
||||
]);
|
||||
},
|
||||
goldenFile: '../_goldens/text_box_component_test_1.png',
|
||||
skip: true,
|
||||
);
|
||||
});
|
||||
}
|
||||
@ -130,7 +129,7 @@ class _FramedTextBox extends TextBoxComponent {
|
||||
super.align,
|
||||
super.position,
|
||||
super.size,
|
||||
});
|
||||
}) : super(textRenderer: TextPaint(debugMode: true));
|
||||
|
||||
final Paint _borderPaint = Paint()
|
||||
..style = PaintingStyle.stroke
|
||||
|
||||
122
packages/flame/test/text/common/line_metrics_test.dart
Normal file
122
packages/flame/test/text/common/line_metrics_test.dart
Normal file
@ -0,0 +1,122 @@
|
||||
import 'package:flame/src/text/common/line_metrics.dart';
|
||||
import 'package:flame_test/flame_test.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void main() {
|
||||
group('LineMetrics', () {
|
||||
test('default LineMetrics box', () {
|
||||
final box = LineMetrics(left: 10, baseline: 20);
|
||||
expect(box.left, 10);
|
||||
expect(box.right, 10);
|
||||
expect(box.width, 0);
|
||||
expect(box.baseline, 20);
|
||||
expect(box.top, 20);
|
||||
expect(box.bottom, 20);
|
||||
expect(box.ascent, 0);
|
||||
expect(box.descent, 0);
|
||||
expect(box.height, 0);
|
||||
});
|
||||
|
||||
test('with height only', () {
|
||||
final box = LineMetrics(baseline: 15, height: 10);
|
||||
expect(box.baseline, 15);
|
||||
expect(box.height, 10);
|
||||
expect(box.top, 15 - 10);
|
||||
expect(box.bottom, 15);
|
||||
expect(box.ascent, 10);
|
||||
expect(box.descent, 0);
|
||||
});
|
||||
|
||||
test('with height and ascent', () {
|
||||
final box = LineMetrics(baseline: 15, height: 10, ascent: 8);
|
||||
expect(box.baseline, 15);
|
||||
expect(box.height, 10);
|
||||
expect(box.top, 15 - 8);
|
||||
expect(box.bottom, 15 - 8 + 10);
|
||||
expect(box.ascent, 8);
|
||||
expect(box.descent, 10 - 8);
|
||||
});
|
||||
|
||||
test('with height and descent', () {
|
||||
final box = LineMetrics(baseline: 15, height: 10, descent: 3);
|
||||
expect(box.baseline, 15);
|
||||
expect(box.height, 10);
|
||||
expect(box.top, 15 + 3 - 10);
|
||||
expect(box.bottom, 15 + 3);
|
||||
expect(box.ascent, 10 - 3);
|
||||
expect(box.descent, 3);
|
||||
});
|
||||
|
||||
test('with ascent and descent', () {
|
||||
final box = LineMetrics(baseline: 15, ascent: 10, descent: 3);
|
||||
expect(box.baseline, 15);
|
||||
expect(box.height, 10 + 3);
|
||||
expect(box.top, 15 - 10);
|
||||
expect(box.bottom, 15 + 3);
|
||||
expect(box.ascent, 10);
|
||||
expect(box.descent, 3);
|
||||
});
|
||||
|
||||
test('translate', () {
|
||||
final box = LineMetrics(width: 40, descent: 2, ascent: 8);
|
||||
expect(box.left, 0);
|
||||
expect(box.baseline, 0);
|
||||
box.translate(5, 11);
|
||||
expect(box.left, 5);
|
||||
expect(box.baseline, 11);
|
||||
expect(box.width, 40);
|
||||
expect(box.height, 10);
|
||||
expect(box.ascent, 8);
|
||||
box.translate(-1, -1);
|
||||
expect(box.left, 4);
|
||||
expect(box.baseline, 10);
|
||||
});
|
||||
|
||||
test('moveToOrigin', () {
|
||||
final box = LineMetrics(left: 33, baseline: 78, ascent: 8, descent: 2);
|
||||
box.moveToOrigin();
|
||||
expect(box.left, 0);
|
||||
expect(box.baseline, 0);
|
||||
expect(box.top, -8);
|
||||
expect(box.bottom, 2);
|
||||
});
|
||||
|
||||
test('setLeftEdge', () {
|
||||
final box = LineMetrics(left: 33, baseline: 78, width: 101);
|
||||
expect(box.right, 33 + 101);
|
||||
box.setLeftEdge(49);
|
||||
expect(box.right, 33 + 101);
|
||||
expect(box.left, 49);
|
||||
});
|
||||
|
||||
test('append', () {
|
||||
final box1 = LineMetrics(left: 4, width: 10, baseline: 17, height: 6);
|
||||
final box2 = LineMetrics(
|
||||
left: 18,
|
||||
width: 40,
|
||||
baseline: 17,
|
||||
ascent: 8,
|
||||
descent: 2,
|
||||
);
|
||||
box1.append(box2);
|
||||
expect(box1.left, 4);
|
||||
expect(box1.right, 18 + 40);
|
||||
expect(box1.top, 17 - 8);
|
||||
expect(box1.bottom, 17 + 2);
|
||||
|
||||
expect(
|
||||
() => 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)',
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
@ -12,15 +12,14 @@ void main() {
|
||||
group('SpriteFontRenderer', () {
|
||||
test('creating SpriteFontRenderer', () async {
|
||||
final renderer = await createRenderer();
|
||||
expect(renderer.source, isA<Image>());
|
||||
expect(renderer.scaledCharWidth, 6);
|
||||
expect(renderer.scaledCharHeight, 6);
|
||||
expect(renderer.letterSpacing, 0);
|
||||
expect(renderer.isMonospace, true);
|
||||
expect(renderer.formatter.source, isA<Image>());
|
||||
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,
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user