Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Added TextFormatter classes #1720

Merged
merged 36 commits into from
Jun 27, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
d431d99
TextElement class
st-pasha Jun 11, 2022
92b4060
TextLine
st-pasha Jun 11, 2022
15847f2
LineMetrics class
st-pasha Jun 11, 2022
0e8eaf1
test for LineMetrics
st-pasha Jun 11, 2022
b347675
TextPainterTextElement
st-pasha Jun 11, 2022
a8f5340
TextFormatter class
st-pasha Jun 11, 2022
88e3e11
TextPainterTextFormatter
st-pasha Jun 11, 2022
18a99d1
DebugTextPainterTextElement
st-pasha Jun 11, 2022
950c4b7
refactor TextPaint
st-pasha Jun 11, 2022
2a00d2c
update golden test
st-pasha Jun 11, 2022
4f3e6ba
update doc-comment
st-pasha Jun 11, 2022
e562ef4
restore toTextPainter
st-pasha Jun 11, 2022
68ff5d5
deprecate toTextPainter
st-pasha Jun 11, 2022
1efddcc
SpriteFontTextFormatter and SpriteFontTextElement
st-pasha Jun 11, 2022
2b6953d
move out GlyphData and GlyphInfo
st-pasha Jun 11, 2022
42acb74
remove the isMonospace property
st-pasha Jun 11, 2022
b186cb2
Introduce FormatterTextRenderer
st-pasha Jun 11, 2022
991c942
Update packages/flame/lib/src/text/common/line_metrics.dart
st-pasha Jun 18, 2022
9da5837
fix: Merge basic and advanced gesture detectors (#1718)
st-pasha Jun 11, 2022
a0a0509
feat: add `HasAncestor` mixin (#1711)
wolfenrain Jun 12, 2022
7121b19
feat!: Update flame_audio to AP 1.0.0 (#1724)
luanpotter Jun 14, 2022
679906b
ci: Use pubspec_overrides.yaml (#1728)
spydon Jun 14, 2022
07ec8ff
fix: Add missing paint arguments on shapes (#1727)
spydon Jun 14, 2022
cb9c4d6
fix: tiled example size (#1729)
ConcealGeek Jun 14, 2022
03f02a9
fix: ButtonComponent behavior when the engine is paused (#1726)
st-pasha Jun 14, 2022
ffc062d
docs: Documenting how to write documentation (#1721)
st-pasha Jun 14, 2022
afec303
feat: Drag events that dispatch using componentsAtPoint (#1715)
st-pasha Jun 14, 2022
aeb14e0
chore: Publish flame_audio 1.2.0 (#1730)
spydon Jun 14, 2022
a4a9a2e
fix: Correct flutter constraint (#1731)
spydon Jun 14, 2022
2a26e67
feat: Adding bloc getter to FlameBlocListenable mixin (#1732)
AdrienDelgado Jun 15, 2022
9e70889
docs: Return Widget instead of void in the game_widget.md (#1736)
imaNNeo Jun 16, 2022
88d100e
Merge branch 'main' into ps/text-formatters
st-pasha Jun 18, 2022
e0ff623
Merge branch 'main' into ps/text-formatters
st-pasha Jun 19, 2022
4eb0ee4
remove deprecation note
st-pasha Jun 24, 2022
b0b5756
Merge branch 'main' into ps/text-formatters
st-pasha Jun 24, 2022
1235846
Merge branch 'main' into ps/text-formatters
spydon Jun 27, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions packages/flame/lib/src/text/common/glyph_data.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 14 additions & 0 deletions packages/flame/lib/src/text/common/glyph_info.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/// Helper class that stores dimensions of a single glyph for a spritesheet-
/// based font.
class GlyphInfo {
st-pasha marked this conversation as resolved.
Show resolved Hide resolved
double srcLeft = 0;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add some dart doc on each attribute? Some of them I can infer their meaning from the name, but not all

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rst* attributes are used in the rstTransforms argument for the canvas.drawRawAtlas function.

We do need to have proper documentation for them, but I was thinking of leaving that for later, since we still need to merge GlyphData and GlyphInfo classes, plus there would be some reorganization of this class to support non-monospace fonts.

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 changes: 98 additions & 0 deletions packages/flame/lib/src/text/common/line_metrics.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import 'dart:ui';

import 'package:flame/src/text/common/text_line.dart';

/// [LineMetrics] object contains measurements of a [TextLine].
st-pasha marked this conversation as resolved.
Show resolved Hide resolved
///
/// 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 changes: 15 additions & 0 deletions packages/flame/lib/src/text/common/text_line.dart
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions packages/flame/lib/src/text/formatter_text_renderer.dart
Original file line number Diff line number Diff line change
@@ -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 {
spydon marked this conversation as resolved.
Show resolved Hide resolved
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);
}
}
Original file line number Diff line number Diff line change
@@ -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!;
});
}
}
7 changes: 7 additions & 0 deletions packages/flame/lib/src/text/formatters/text_formatter.dart
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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);
}
}
42 changes: 42 additions & 0 deletions packages/flame/lib/src/text/inline/sprite_font_text_element.dart
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading