Abstracting text API to enable custom renderers (#772)

* Abstracting text API to enable custom renderers

* Addressing comments

* Lint

* Update doc/text.md

Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>

* Adding dartdoc about TextRenderer

Co-authored-by: Lukas Klingsbo <lukas.klingsbo@gmail.com>
This commit is contained in:
Erick
2021-04-29 09:24:17 -03:00
committed by GitHub
parent 91864d0b88
commit d9e0acd3ff
16 changed files with 266 additions and 175 deletions

View File

@ -2,15 +2,28 @@
Flame has some dedicated classes to help you render text. Flame has some dedicated classes to help you render text.
## TextConfig ## TextRenderer
A Text Config contains all typographical information required to render text; i.e., font size and `TextRenderer` is the abstract class used by Flame to render text. Flame provides one
color, family, etc. implementation for this called `TextPaint` but anyone can implement this abstraction
and create a custom way to render text.
## TextPaint
A Text Paint is the built in implementation of text rendering on Flame, it is based on top of
Flutter's `TextPainter` class (hence the name), it can be configured by its config class
`TextPaintConfig` which contains all typographical information required to render text; i.e., font
size and color, family, etc.
Example usage: Example usage:
```dart ```dart
const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font'); const TextPaint textPaint = TextPaint(
config: TextPaintConfig(
fontSize: 48.0,
fontFamily: 'Awesome Font',
),
);
``` ```
- `fontFamily`: a commonly available font, like Arial (default), or a custom font added in your - `fontFamily`: a commonly available font, like Arial (default), or a custom font added in your
@ -22,17 +35,17 @@ const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font')
For more information regarding colors and how to create then, see the For more information regarding colors and how to create then, see the
[Colors and the Palette](palette.md) guide. [Colors and the Palette](palette.md) guide.
After the creation of the config you can use its `render` method to draw some string on a canvas: After the creation of the text paint you can use its `render` method to draw some string on a canvas:
```dart ```dart
config.render(canvas, "Flame is awesome", Position(10, 10)); textPaint.render(canvas, "Flame is awesome", Vector2(10, 10));
``` ```
If you want to set the anchor of the text you can also do that in the render call, with the optional If you want to set the anchor of the text you can also do that in the render call, with the optional
`anchor` parameter: `anchor` parameter:
```dart ```dart
config.render(canvas, 'Flame is awesome', Vector2(10, 10), anchor: Anchor.topCenter); textPaint.render(canvas, 'Flame is awesome', Vector2(10, 10), anchor: Anchor.topCenter);
``` ```
## Text Components ## Text Components
@ -47,12 +60,12 @@ Flame provides two text components that make it even easier to render text in yo
Example usage: Example usage:
```dart ```dart
TextConfig regular = TextConfig(color: BasicPalette.white.color); TextPaint regular = TextPaint(color: BasicPalette.white.color);
class MyGame extends BaseGame { class MyGame extends BaseGame {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
add(TextComponent('Hello, Flame', config: regular) add(TextComponent('Hello, Flame', textRenderer: regular)
..anchor = Anchor.topCenter ..anchor = Anchor.topCenter
..x = size.width / 2 // size is a property from game ..x = size.width / 2 // size is a property from game
..y = 32.0); ..y = 32.0);
@ -74,15 +87,15 @@ Example usage:
```dart ```dart
class MyTextBox extends TextBoxComponent { class MyTextBox extends TextBoxComponent {
MyTextBox(String text) : super(text, config: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05)); MyTextBox(String text) : super(text, textRenderer: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05));
@override @override
void drawBackground(Canvas c) { void drawBackground(Canvas c) {
Rect rect = Rect.fromLTWH(0, 0, width, height); Rect rect = Rect.fromLTWH(0, 0, width, height);
c.drawRect(rect, new Paint()..color = Color(0xFFFF00FF)); c.drawRect(rect, Paint()..color = Color(0xFFFF00FF));
c.drawRect( c.drawRect(
rect.deflate(boxConfig.margin), rect.deflate(boxConfig.margin),
new Paint() Paint()
..color = BasicPalette.black.color ..color = BasicPalette.black.color
..style = PaintingStyle.stroke); ..style = PaintingStyle.stroke);
} }

View File

@ -14,7 +14,11 @@ final R = Random();
class MovableSquare extends SquareComponent class MovableSquare extends SquareComponent
with Hitbox, Collidable, HasGameRef<CameraAndViewportGame> { with Hitbox, Collidable, HasGameRef<CameraAndViewportGame> {
static const double speed = 300; static const double speed = 300;
static final TextConfig config = TextConfig(fontSize: 12); static final TextPaint textRenderer = TextPaint(
config: const TextPaintConfig(
fontSize: 12,
),
);
final Vector2 velocity = Vector2.zero(); final Vector2 velocity = Vector2.zero();
late Timer timer; late Timer timer;
@ -41,7 +45,7 @@ class MovableSquare extends SquareComponent
void render(Canvas c) { void render(Canvas c) {
super.render(c); super.render(c);
final text = '(${x.toInt()}, ${y.toInt()})'; final text = '(${x.toInt()}, ${y.toInt()})';
config.render(c, text, size / 2, anchor: Anchor.center); textRenderer.render(c, text, size / 2, anchor: Anchor.center);
} }
@override @override

View File

@ -167,8 +167,10 @@ class CollidableSnowman extends MyCollidable {
class MultipleShapes extends BaseGame class MultipleShapes extends BaseGame
with HasCollidables, HasDraggableComponents { with HasCollidables, HasDraggableComponents {
final TextConfig fpsTextConfig = TextConfig( final TextPaint fpsTextPaint = TextPaint(
config: TextPaintConfig(
color: BasicPalette.white.color, color: BasicPalette.white.color,
),
); );
@override @override
@ -229,7 +231,7 @@ class MultipleShapes extends BaseGame
@override @override
void render(Canvas canvas) { void render(Canvas canvas) {
super.render(canvas); super.render(canvas);
fpsTextConfig.render( fpsTextPaint.render(
canvas, canvas,
'${fps(120).toStringAsFixed(2)}fps', '${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24), Vector2(0, size.y - 24),

View File

@ -34,7 +34,11 @@ class LogoCompomnent extends SpriteComponent with HasGameRef<DebugGame> {
} }
class DebugGame extends BaseGame { class DebugGame extends BaseGame {
static final fpsTextConfig = TextConfig(color: const Color(0xFFFFFFFF)); static final fpsTextPaint = TextPaint(
config: const TextPaintConfig(
color: Color(0xFFFFFFFF),
),
);
@override @override
bool debugMode = true; bool debugMode = true;
@ -67,7 +71,7 @@ class DebugGame extends BaseGame {
super.render(canvas); super.render(canvas);
if (debugMode) { if (debugMode) {
fpsTextConfig.render(canvas, fps(120).toString(), Vector2(0, 50)); fpsTextPaint.render(canvas, fps(120).toString(), Vector2(0, 50));
} }
} }
} }

View File

@ -5,8 +5,9 @@ import 'package:flame/game.dart';
import 'package:flame/palette.dart'; import 'package:flame/palette.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
final _regular = TextConfig(color: BasicPalette.white.color); final _regularTextConfig = TextPaintConfig(color: BasicPalette.white.color);
final _tiny = _regular.withFontSize(12.0); final _regular = TextPaint(config: _regularTextConfig);
final _tiny = TextPaint(config: _regularTextConfig.withFontSize(12.0));
final _white = Paint() final _white = Paint()
..color = BasicPalette.white.color ..color = BasicPalette.white.color
@ -16,7 +17,7 @@ class MyTextBox extends TextBoxComponent {
MyTextBox(String text) MyTextBox(String text)
: super( : super(
text, text,
config: _tiny, textRenderer: _tiny,
boxConfig: TextBoxConfig( boxConfig: TextBoxConfig(
timePerChar: 0.05, timePerChar: 0.05,
growingBox: true, growingBox: true,
@ -43,20 +44,20 @@ class TextGame extends BaseGame {
@override @override
Future<void> onLoad() async { Future<void> onLoad() async {
add( add(
TextComponent('Hello, Flame', config: _regular) TextComponent('Hello, Flame', textRenderer: _regular)
..anchor = Anchor.topCenter ..anchor = Anchor.topCenter
..x = size.x / 2 ..x = size.x / 2
..y = 32.0, ..y = 32.0,
); );
add( add(
TextComponent('center', config: _tiny) TextComponent('center', textRenderer: _tiny)
..anchor = Anchor.center ..anchor = Anchor.center
..position.setFrom(size / 2), ..position.setFrom(size / 2),
); );
add( add(
TextComponent('bottomRight', config: _tiny) TextComponent('bottomRight', textRenderer: _tiny)
..anchor = Anchor.bottomRight ..anchor = Anchor.bottomRight
..position.setFrom(size), ..position.setFrom(size),
); );

View File

@ -21,8 +21,10 @@ class ParticlesGame extends BaseGame {
final Random rnd = Random(); final Random rnd = Random();
final StepTween steppedTween = StepTween(begin: 0, end: 5); final StepTween steppedTween = StepTween(begin: 0, end: 5);
final trafficLight = TrafficLightComponent(); final trafficLight = TrafficLightComponent();
final TextConfig fpsTextConfig = TextConfig( final TextPaint fpsTextPaint = TextPaint(
color: const Color(0xFFFFFFFF), config: const TextPaintConfig(
color: Color(0xFFFFFFFF),
),
); );
/// Defines the lifespan of all the particles in these examples /// Defines the lifespan of all the particles in these examples
@ -474,7 +476,7 @@ class ParticlesGame extends BaseGame {
super.render(canvas); super.render(canvas);
if (debugMode) { if (debugMode) {
fpsTextConfig.render( fpsTextPaint.render(
canvas, canvas,
'${fps(120).toStringAsFixed(2)}fps', '${fps(120).toStringAsFixed(2)}fps',
Vector2(0, size.y - 24), Vector2(0, size.y - 24),

View File

@ -4,7 +4,11 @@ import 'package:flame/timer.dart';
import 'package:flame/gestures.dart'; import 'package:flame/gestures.dart';
class TimerGame extends Game with TapDetector { class TimerGame extends Game with TapDetector {
final TextConfig textConfig = TextConfig(color: const Color(0xFFFFFFFF)); final TextPaint textConfig = TextPaint(
config: const TextPaintConfig(
color: Color(0xFFFFFFFF),
),
);
late Timer countdown; late Timer countdown;
late Timer interval; late Timer interval;

View File

@ -4,13 +4,17 @@ import 'package:flame/timer.dart';
import 'package:flame/gestures.dart'; import 'package:flame/gestures.dart';
class RenderedTimeComponent extends TimerComponent { class RenderedTimeComponent extends TimerComponent {
final TextConfig textConfig = TextConfig(color: const Color(0xFFFFFFFF)); final TextPaint textPaint = TextPaint(
config: const TextPaintConfig(
color: Color(0xFFFFFFFF),
),
);
RenderedTimeComponent(Timer timer) : super(timer); RenderedTimeComponent(Timer timer) : super(timer);
@override @override
void render(Canvas canvas) { void render(Canvas canvas) {
textConfig.render( textPaint.render(
canvas, canvas,
'Elapsed time: ${timer.current}', 'Elapsed time: ${timer.current}',
Vector2(10, 150), Vector2(10, 150),

View File

@ -9,6 +9,7 @@
- Add a new renderRect method to Sprite - Add a new renderRect method to Sprite
- Addresses the TODO to change the camera public APIs to take Anchors for relativePositions - Addresses the TODO to change the camera public APIs to take Anchors for relativePositions
- Adds methods to support moving the camera relative to its current position - Adds methods to support moving the camera relative to its current position
- Abstracting the text api to allow custom text renderers on the framework
## [1.0.0-rc9] ## [1.0.0-rc9]
- Fix input bug with other anchors than center - Fix input bug with other anchors than center

View File

@ -21,5 +21,5 @@ export 'src/components/sprite_component.dart';
export 'src/components/text_box_component.dart'; export 'src/components/text_box_component.dart';
export 'src/components/text_component.dart'; export 'src/components/text_component.dart';
export 'src/extensions/vector2.dart'; export 'src/extensions/vector2.dart';
export 'src/text_config.dart'; export 'src/text.dart';
export 'src/timer.dart'; export 'src/timer.dart';

View File

@ -6,4 +6,4 @@ export 'src/game/game.dart';
export 'src/game/game_widget/game_widget.dart'; export 'src/game/game_widget/game_widget.dart';
export 'src/game/projector.dart'; export 'src/game/projector.dart';
export 'src/game/viewport.dart'; export 'src/game/viewport.dart';
export 'src/text_config.dart'; export 'src/text.dart';

View File

@ -9,7 +9,7 @@ import '../../game.dart';
import '../effects/effects.dart'; import '../effects/effects.dart';
import '../effects/effects_handler.dart'; import '../effects/effects_handler.dart';
import '../extensions/vector2.dart'; import '../extensions/vector2.dart';
import '../text_config.dart'; import '../text.dart';
import 'component.dart'; import 'component.dart';
import 'mixins/has_game_ref.dart'; import 'mixins/has_game_ref.dart';
@ -50,7 +50,12 @@ abstract class BaseComponent extends Component {
..strokeWidth = 1 ..strokeWidth = 1
..style = PaintingStyle.stroke; ..style = PaintingStyle.stroke;
TextConfig get debugTextConfig => TextConfig(color: debugColor, fontSize: 12); TextPaint get debugTextPaint => TextPaint(
config: TextPaintConfig(
color: debugColor,
fontSize: 12,
),
);
/// This method is called periodically by the game engine to request that your component updates itself. /// This method is called periodically by the game engine to request that your component updates itself.
/// ///

View File

@ -156,7 +156,7 @@ abstract class PositionComponent extends BaseComponent {
(this as Hitbox).renderShapes(canvas); (this as Hitbox).renderShapes(canvas);
} }
canvas.drawRect(size.toRect(), debugPaint); canvas.drawRect(size.toRect(), debugPaint);
debugTextConfig.render( debugTextPaint.render(
canvas, canvas,
'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}', 'x: ${x.toStringAsFixed(2)} y:${y.toStringAsFixed(2)}',
Vector2(-50, -15), Vector2(-50, -15),
@ -165,7 +165,7 @@ abstract class PositionComponent extends BaseComponent {
final rect = toRect(); final rect = toRect();
final dx = rect.right; final dx = rect.right;
final dy = rect.bottom; final dy = rect.bottom;
debugTextConfig.render( debugTextPaint.render(
canvas, canvas,
'x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}', 'x:${dx.toStringAsFixed(2)} y:${dy.toStringAsFixed(2)}',
Vector2(width - 50, height), Vector2(width - 50, height),

View File

@ -6,7 +6,7 @@ import 'package:flutter/widgets.dart' hide Image;
import '../extensions/vector2.dart'; import '../extensions/vector2.dart';
import '../palette.dart'; import '../palette.dart';
import '../text_config.dart'; import '../text.dart';
import 'position_component.dart'; import 'position_component.dart';
class TextBoxConfig { class TextBoxConfig {
@ -31,7 +31,7 @@ class TextBoxComponent extends PositionComponent {
Vector2 _gameSize = Vector2.zero(); Vector2 _gameSize = Vector2.zero();
final String _text; final String _text;
final TextConfig _config; final TextRenderer _textRenderer;
final TextBoxConfig _boxConfig; final TextBoxConfig _boxConfig;
late List<String> _lines; late List<String> _lines;
@ -45,37 +45,37 @@ class TextBoxComponent extends PositionComponent {
String get text => _text; String get text => _text;
TextConfig get config => _config; TextRenderer get renderer => _textRenderer;
TextBoxConfig get boxConfig => _boxConfig; TextBoxConfig get boxConfig => _boxConfig;
TextBoxComponent( TextBoxComponent(
String text, { String text, {
TextConfig? config, TextRenderer? textRenderer,
TextBoxConfig? boxConfig, TextBoxConfig? boxConfig,
Vector2? position, Vector2? position,
Vector2? size, Vector2? size,
}) : _text = text, }) : _text = text,
_boxConfig = boxConfig ?? TextBoxConfig(), _boxConfig = boxConfig ?? TextBoxConfig(),
_config = config ?? TextConfig(), _textRenderer = textRenderer ?? TextPaint(),
super(position: position, size: size) { super(position: position, size: size) {
_lines = []; _lines = [];
double? lineHeight; double? lineHeight;
text.split(' ').forEach((word) { text.split(' ').forEach((word) {
final possibleLine = _lines.isEmpty ? word : '${_lines.last} $word'; final possibleLine = _lines.isEmpty ? word : '${_lines.last} $word';
final painter = _config.toTextPainter(possibleLine); lineHeight ??= _textRenderer.measureTextHeight(possibleLine);
lineHeight ??= painter.height;
if (painter.width <= final textWidth = _textRenderer.measureTextWidth(possibleLine);
_boxConfig.maxWidth - _boxConfig.margins.horizontal) { if (textWidth <= _boxConfig.maxWidth - _boxConfig.margins.horizontal) {
if (_lines.isNotEmpty) { if (_lines.isNotEmpty) {
_lines.last = possibleLine; _lines.last = possibleLine;
} else { } else {
_lines.add(possibleLine); _lines.add(possibleLine);
} }
_updateMaxWidth(painter.width); _updateMaxWidth(textWidth);
} else { } else {
_lines.add(word); _lines.add(word);
_updateMaxWidth(_config.toTextPainter(word).width); _updateMaxWidth(textWidth);
} }
}); });
_totalLines = _lines.length; _totalLines = _lines.length;
@ -112,9 +112,12 @@ class TextBoxComponent extends PositionComponent {
Vector2 get size => Vector2(width, height); Vector2 get size => Vector2(width, height);
double getLineWidth(String line, int charCount) { double getLineWidth(String line, int charCount) {
return _config return _textRenderer.measureTextWidth(
.toTextPainter(line.substring(0, math.min(charCount, line.length))) line.substring(
.width; 0,
math.min(charCount, line.length),
),
);
} }
double? _cachedWidth; double? _cachedWidth;
@ -192,7 +195,7 @@ class TextBoxComponent extends PositionComponent {
} }
void _drawLine(Canvas c, String line, double dy) { void _drawLine(Canvas c, String line, double dy) {
_config.toTextPainter(line).paint(c, Offset(_boxConfig.margins.left, dy)); _textRenderer.render(c, line, Vector2(_boxConfig.margins.left, dy));
} }
void redrawLater() async { void redrawLater() async {

View File

@ -4,14 +4,12 @@ import 'package:flutter/painting.dart';
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import '../extensions/vector2.dart'; import '../extensions/vector2.dart';
import '../text_config.dart'; import '../text.dart';
import 'position_component.dart'; import 'position_component.dart';
class TextComponent extends PositionComponent { class TextComponent extends PositionComponent {
String _text; String _text;
TextConfig _config; TextRenderer _textRenderer;
late TextPainter _tp;
String get text => _text; String get text => _text;
@ -22,32 +20,32 @@ class TextComponent extends PositionComponent {
} }
} }
TextConfig get config => _config; TextRenderer get textRenderer => _textRenderer;
set config(TextConfig config) { set textRenderer(TextRenderer textRenderer) {
_config = config; _textRenderer = textRenderer;
_updateBox(); _updateBox();
} }
TextComponent( TextComponent(
this._text, { this._text, {
TextConfig? config, TextRenderer? textRenderer,
Vector2? position, Vector2? position,
Vector2? size, Vector2? size,
}) : _config = config ?? TextConfig(), }) : _textRenderer = textRenderer ?? TextPaint(),
super(position: position, size: size) { super(position: position, size: size) {
_updateBox(); _updateBox();
} }
void _updateBox() { void _updateBox() {
_tp = config.toTextPainter(_text); final size = textRenderer.measureText(_text);
size.setValues(_tp.width, _tp.height); size.setValues(size.x, size.y);
} }
@mustCallSuper @mustCallSuper
@override @override
void render(Canvas c) { void render(Canvas canvas) {
super.render(c); super.render(canvas);
_tp.paint(c, Offset.zero); _textRenderer.render(canvas, text, Vector2.zero());
} }
} }

View File

@ -8,22 +8,76 @@ import 'extensions/size.dart';
import 'extensions/vector2.dart'; import 'extensions/vector2.dart';
import 'memory_cache.dart'; import 'memory_cache.dart';
/// A Text Config contains all typographical information required to render texts; i.e., font size and color, family, etc. /// [TextRenderer] is the abstract API that Flame uses for rendering text in its features
/// this class can be extended to provide an implementation of text rendering in the engine.
/// ///
/// It does not hold information regarding the position of the text to be render neither the text itself (the string). /// See [TextPaint] for the default implementation offered by Flame
/// To hold all those information, use the Text component. abstract class TextRenderer<T extends BaseTextConfig> {
final T config;
TextRenderer({required this.config});
/// Renders a given [text] in a given position [position] using the provided [canvas] and [anchor].
/// ///
/// It is used by [TextComponent]. /// Renders it in the given position, considering the [anchor] specified.
class TextConfig { /// For example, if [Anchor.center] is specified, it's going to be drawn centered around [position].
///
/// Example usage (Using TextPaint implementation):
///
/// const TextPaint config = TextPaint(fontSize: 48.0, fontFamily: 'Awesome Font');
/// config.render(canvas, Vector2(size.x - 10, size.y - 10, anchor: Anchor.bottomRight);
void render(
Canvas canvas,
String text,
Vector2 position, {
Anchor anchor = Anchor.topLeft,
});
/// Given a [text] String, returns the width of that [text].
double measureTextWidth(String text);
/// Given a [text] String, returns the height of that [text].
double measureTextHeight(String text);
/// Given a [text] String, returns a Vector2 with the size of that [text] has.
Vector2 measureText(String text) {
return Vector2(
measureTextWidth(text),
measureTextHeight(text),
);
}
}
/// A Text Config contains all typographical information required to render texts; i.e., font size, text direction, etc.
abstract class BaseTextConfig {
/// The font size to be used, in points. /// The font size to be used, in points.
final double fontSize; final double fontSize;
/// The direction to render this text (left to right or right to left).
///
/// Normally, leave this as is for most languages.
/// For proper fonts of languages like Hebrew or Arabic, replace this with [TextDirection.rtl].
final TextDirection textDirection;
/// The height of line, as a multiple of font size.
final double? lineHeight;
const BaseTextConfig({
this.fontSize = 24.0,
this.textDirection = TextDirection.ltr,
this.lineHeight,
});
}
/// An extension of the BaseTextConfig which includes more configs supported by
/// TextPaint
class TextPaintConfig extends BaseTextConfig {
/// The font color to be used. /// The font color to be used.
/// ///
/// Dart's [Color] class is just a plain wrapper on top of ARGB color (0xAARRGGBB). /// Dart's [Color] class is just a plain wrapper on top of ARGB color (0xAARRGGBB).
/// For example, /// For example,
/// ///
/// const TextConfig config = TextConfig(color: const Color(0xFF00FF00)); // green /// const TextPaint config = TextPaint(color: const Color(0xFF00FF00)); // green
/// ///
/// You can also use your Palette class to access colors used in your game. /// You can also use your Palette class to access colors used in your game.
final Color color; final Color color;
@ -44,45 +98,97 @@ class TextConfig {
/// The name you choose for the font family can be any name (it's not inside the TTF file and the filename doesn't need to match). /// The name you choose for the font family can be any name (it's not inside the TTF file and the filename doesn't need to match).
final String fontFamily; final String fontFamily;
/// The [TextAlign] to be used when creating the [material.TextPainter]. /// Creates a constant [TextPaint] with sensible defaults.
/// ///
/// Beware: it's recommended to leave this with the default value of [TextAlign.left]. /// Every parameter can be specified.
/// Use the anchor parameter to [render] to specify a proper relative position. const TextPaintConfig({
final TextAlign textAlign; this.color = const Color(0xFF000000),
this.fontFamily = 'Arial',
double fontSize = 24.0,
TextDirection textDirection = TextDirection.rtl,
double? lineHeight,
}) : super(
fontSize: fontSize,
textDirection: textDirection,
lineHeight: lineHeight,
);
/// The direction to render this text (left to right or right to left). /// Creates a new [TextPaintConfig] changing only the [fontSize].
/// ///
/// Normally, leave this as is for most languages. /// This does not change the original (as it's immutable).
/// For proper fonts of languages like Hebrew or Arabic, replace this with [TextDirection.rtl]. TextPaintConfig withFontSize(double fontSize) {
final TextDirection textDirection; return TextPaintConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textDirection: textDirection,
);
}
/// The height of line, as a multiple of font size. /// Creates a new [TextPaintConfig] changing only the [color].
final double? lineHeight; ///
/// This does not change the original (as it's immutable).
TextPaintConfig withColor(Color color) {
return TextPaintConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textDirection: textDirection,
);
}
/// Creates a new [TextPaintConfig] changing only the [fontFamily].
///
/// This does not change the original (as it's immutable).
TextPaintConfig withFontFamily(String fontFamily) {
return TextPaintConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textDirection: textDirection,
);
}
/// Creates a new [TextPaintConfig] changing only the [textAlign].
///
/// This does not change the original (as it's immutable).
TextPaintConfig withTextAlign(TextAlign textAlign) {
return TextPaintConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textDirection: textDirection,
);
}
/// Creates a new [TextPaintConfig] changing only the [textDirection].
///
/// This does not change the original (as it's immutable).
TextPaintConfig withTextDirection(TextDirection textDirection) {
return TextPaintConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textDirection: textDirection,
);
}
}
/// A Text Config contains all typographical information required to render texts; i.e., font size and color, family, etc.
///
/// It does not hold information regarding the position of the text to be render neither the text itself (the string).
/// To hold all those information, use the Text component.
///
/// It is used by [TextComponent].
class TextPaint extends TextRenderer<TextPaintConfig> {
final MemoryCache<String, material.TextPainter> _textPainterCache = final MemoryCache<String, material.TextPainter> _textPainterCache =
MemoryCache(); MemoryCache();
/// Creates a constant [TextConfig] with sensible defaults. TextPaint({
/// TextPaintConfig config = const TextPaintConfig(),
/// Every parameter can be specified. }) : super(config: config);
TextConfig({
this.fontSize = 24.0,
this.color = const Color(0xFF000000),
this.fontFamily = 'Arial',
this.textAlign = TextAlign.left,
this.textDirection = TextDirection.ltr,
this.lineHeight,
});
/// Renders a given [text] in a given position [p] using the provided [canvas] and [anchor]. @override
///
/// It creates a [material.TextPainter] instance using the [toTextPainter] method, and renders it in the given position, considering the [anchor] specified.
/// For example, if [Anchor.center] is specified, it's going to be drawn centered around [p].
///
/// Example usage:
///
/// const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font');
/// config.render(c, Offset(size.width - 10, size.height - 10, anchor: Anchor.bottomRight);
void render( void render(
Canvas canvas, Canvas canvas,
String text, String text,
@ -94,13 +200,23 @@ class TextConfig {
tp.paint(canvas, translatedPosition.toOffset()); tp.paint(canvas, translatedPosition.toOffset());
} }
@override
double measureTextWidth(String text) {
return toTextPainter(text).width;
}
@override
double measureTextHeight(String text) {
return toTextPainter(text).height;
}
/// Returns a [material.TextPainter] that allows for text rendering and size measuring. /// Returns a [material.TextPainter] that allows for text rendering and size measuring.
/// ///
/// A [material.TextPainter] has three important properties: paint, width and height (or size). /// A [material.TextPainter] has three important properties: paint, width and height (or size).
/// ///
/// Example usage: /// Example usage:
/// ///
/// const TextConfig config = TextConfig(fontSize: 48.0, fontFamily: 'Awesome Font'); /// const TextPaint config = TextPaint(fontSize: 48.0, fontFamily: 'Awesome Font');
/// final tp = config.toTextPainter('Score: $score'); /// final tp = config.toTextPainter('Score: $score');
/// tp.paint(c, Offset(size.width - p.width - 10, size.height - p.height - 10)); /// tp.paint(c, Offset(size.width - p.width - 10, size.height - p.height - 10));
/// ///
@ -109,10 +225,10 @@ class TextConfig {
material.TextPainter toTextPainter(String text) { material.TextPainter toTextPainter(String text) {
if (!_textPainterCache.containsKey(text)) { if (!_textPainterCache.containsKey(text)) {
final style = material.TextStyle( final style = material.TextStyle(
color: color, color: config.color,
fontSize: fontSize, fontSize: config.fontSize,
fontFamily: fontFamily, fontFamily: config.fontFamily,
height: lineHeight, height: config.lineHeight,
); );
final span = material.TextSpan( final span = material.TextSpan(
style: style, style: style,
@ -120,8 +236,7 @@ class TextConfig {
); );
final tp = material.TextPainter( final tp = material.TextPainter(
text: span, text: span,
textAlign: textAlign, textDirection: config.textDirection,
textDirection: textDirection,
); );
tp.layout(); tp.layout();
@ -129,69 +244,4 @@ class TextConfig {
} }
return _textPainterCache.getValue(text)!; return _textPainterCache.getValue(text)!;
} }
/// Creates a new [TextConfig] changing only the [fontSize].
///
/// This does not change the original (as it's immutable).
TextConfig withFontSize(double fontSize) {
return TextConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textAlign: textAlign,
textDirection: textDirection,
);
}
/// Creates a new [TextConfig] changing only the [color].
///
/// This does not change the original (as it's immutable).
TextConfig withColor(Color color) {
return TextConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textAlign: textAlign,
textDirection: textDirection,
);
}
/// Creates a new [TextConfig] changing only the [fontFamily].
///
/// This does not change the original (as it's immutable).
TextConfig withFontFamily(String fontFamily) {
return TextConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textAlign: textAlign,
textDirection: textDirection,
);
}
/// Creates a new [TextConfig] changing only the [textAlign].
///
/// This does not change the original (as it's immutable).
TextConfig withTextAlign(TextAlign textAlign) {
return TextConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textAlign: textAlign,
textDirection: textDirection,
);
}
/// Creates a new [TextConfig] changing only the [textDirection].
///
/// This does not change the original (as it's immutable).
TextConfig withTextDirection(TextDirection textDirection) {
return TextConfig(
fontSize: fontSize,
color: color,
fontFamily: fontFamily,
textAlign: textAlign,
textDirection: textDirection,
);
}
} }