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: Aligned text in the TextBoxComponent #1620

Merged
merged 40 commits into from
Jun 3, 2022
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
98f2b42
Added aligned text rendering
st-pasha May 12, 2022
3d3405d
docs
st-pasha May 12, 2022
dc20318
update example in docs
st-pasha May 12, 2022
c089d13
minor
st-pasha May 13, 2022
041a3cd
fix: Bug in "tty" TextBoxComponent
st-pasha May 13, 2022
d46b343
_fixedSize property
st-pasha May 13, 2022
fa92c4c
Added a test and an example
st-pasha May 13, 2022
37ea340
update docs
st-pasha May 13, 2022
7e37444
format
st-pasha May 13, 2022
fc1fe84
revert cache.dispose
st-pasha May 13, 2022
eaaeb5b
golden file
st-pasha May 13, 2022
80ce5d9
Merge branch 'ps/issue-1618' into ps/aligned-text
st-pasha May 13, 2022
fd71ca9
Merge branch 'main' into ps/aligned-text
spydon May 13, 2022
4186ecb
analysis issue
st-pasha May 13, 2022
8c9370c
clean up example
st-pasha May 13, 2022
3da2655
Merge branch 'main' into ps/aligned-text
st-pasha May 16, 2022
210a01f
Use testGolden()
st-pasha May 16, 2022
d3dc79a
Merge branch 'main' into ps/aligned-text
spydon May 19, 2022
dd6007f
Merge branch 'main' into ps/aligned-text
spydon May 24, 2022
887b915
chore: Move T-Rex to its own package (#1652)
spydon May 24, 2022
499f096
docs: Add Padracing game to the examples (#1651)
spydon May 26, 2022
cb03d4b
feat: Add non_constant_identifier_names rule (#1656)
spydon May 26, 2022
c6f2246
docs: Release procedure documentation (#1649)
spydon May 27, 2022
7e94aa5
fix: Dead links and header formatting (#1663)
spydon May 27, 2022
a5fadbb
docs: Fix remaining broken and redirected links (#1664)
st-pasha May 27, 2022
005ca7b
feat: Add ability to add/remove multiple overlays at once (#1657)
Hwan-seok May 27, 2022
b52d115
fix: Subscription for events when game changes in GameWidget (#1659)
st-pasha May 27, 2022
2750553
feat: Allow changing parent from null parent (#1662)
spydon May 27, 2022
29dd46c
docs: Fix the broken example links (#1666)
spydon May 28, 2022
70471a7
refactor: Deprecate ComponentSet.createDefault() (#1676)
st-pasha May 28, 2022
512c6c6
feat: Keep stacktrace when rethrowing an error from GameWidget (#1675)
st-pasha May 28, 2022
94a5717
refactor: Replace some usages of fold<> with .sum (#1670)
st-pasha May 29, 2022
4c59026
refactor: Simplify Component.firstChild, .lastChild, and .findParent …
st-pasha May 29, 2022
7c1f6c4
try to update a test
st-pasha May 29, 2022
4746ea4
Merge branch 'main' into ps/aligned-text
st-pasha May 29, 2022
855ede1
remove half-pixels
st-pasha Jun 3, 2022
02da3a9
Merge branch 'main' into ps/aligned-text
st-pasha Jun 3, 2022
b05ec8a
mark test as skipped
st-pasha Jun 3, 2022
1e5c28d
Merge branch 'main' into ps/aligned-text
spydon Jun 3, 2022
150de2c
skip
st-pasha Jun 3, 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
24 changes: 15 additions & 9 deletions doc/flame/rendering/text.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,25 +93,31 @@ class MyGame extends FlameGame {
text inside a bounding box, creating line breaks according to the provided box size.

You can decide if the box should grow as the text is written or if it should be static by the
`growingBox` variable in the `TextBoxConfig`.
`growingBox` variable in the `TextBoxConfig`. A static box could either have a fixed size (setting
the `size` property of the `TextBoxComponent`), or to automatically shrink to fit the text content.

In addition, the `align` property allows you to control the the horizontal and vertical alignment
of the text content. For example, setting `align` to `Anchor.center` will center the text within
its bounding box both vertically and horizontally.

If you want to change the margins of the box use the `margins` variable in the `TextBoxConfig`.

Example usage:

```dart
class MyTextBox extends TextBoxComponent {
MyTextBox(String text) : super(text: text, textRenderer: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05));
MyTextBox(String text)
: super(text: text, textRenderer: tiny, boxConfig: TextBoxConfig(timePerChar: 0.05));

final bgPaint = Paint()..color = Color(0xFFFF00FF);
final borderPaint = Paint()..color = Color(0xFF000000)..style = PaintingStyle.stroke;

@override
void drawBackground(Canvas c) {
void render(Canvas canvas) {
Rect rect = Rect.fromLTWH(0, 0, width, height);
c.drawRect(rect, Paint()..color = Color(0xFFFF00FF));
c.drawRect(
rect.deflate(boxConfig.margin),
BasicPalette.black.Paint()
..style = PaintingStyle.stroke,
);
canvas.drawRect(rect, bgPaint);
canvas.drawRect(rect.deflate(boxConfig.margin), borderPaint);
super.render(canvas);
}
}
```
Expand Down
52 changes: 32 additions & 20 deletions examples/lib/stories/rendering/text_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,20 @@ class TextExample extends FlameGame {

@override
Future<void> onLoad() async {
add(
addAll([
TextComponent(text: 'Hello, Flame', textRenderer: _regular)
..anchor = Anchor.topCenter
..x = size.x / 2
..y = 32.0,
);

add(
TextComponent(text: 'Text with shade', textRenderer: _shaded)
..anchor = Anchor.topRight
..position = size - Vector2.all(100),
);

add(
TextComponent(text: 'center', textRenderer: _tiny)
..anchor = Anchor.center
..position.setFrom(size / 2),
);

add(
TextComponent(text: 'bottomRight', textRenderer: _tiny)
..anchor = Anchor.bottomRight
..position.setFrom(size),
);

add(
MyTextBox(
'"This is our world now. The world of the electron and the switch; '
'the beauty of the baud. We exist without nationality, skin color, '
Expand All @@ -45,7 +33,23 @@ class TextExample extends FlameGame {
)
..anchor = Anchor.bottomLeft
..y = size.y,
);
MyTextBox(
'Let A be a finitely generated torsion-free abelian group. Then '
'A is free.',
align: Anchor.center,
size: Vector2(300, 200),
timePerChar: 0,
margins: 10,
)..position = Vector2(10, 50),
MyTextBox(
'Let A be a torsion abelian group. Then A is the direct sum of its '
'subgroups A(p) for all primes p such that A(p) ≠ 0.',
align: Anchor.bottomRight,
size: Vector2(300, 200),
timePerChar: 0,
margins: 10,
)..position = Vector2(10, 260),
]);
}
}

Expand All @@ -72,21 +76,29 @@ final _shaded = TextPaint(
);

class MyTextBox extends TextBoxComponent {
MyTextBox(String text)
: super(
MyTextBox(
String text, {
Anchor? align,
Vector2? size,
double? timePerChar,
double? margins,
}) : super(
text: text,
textRenderer: _box,
align: align,
size: size,
boxConfig: TextBoxConfig(
maxWidth: 400,
timePerChar: 0.05,
timePerChar: timePerChar ?? 0.05,
growingBox: true,
margins: const EdgeInsets.all(25),
margins: EdgeInsets.all(margins ?? 25),
),
);

@override
void drawBackground(Canvas c) {
void render(Canvas canvas) {
final rect = Rect.fromLTWH(0, 0, width, height);
c.drawRect(rect, Paint()..color = Colors.white10);
canvas.drawRect(rect, Paint()..color = Colors.white10);
st-pasha marked this conversation as resolved.
Show resolved Hide resolved
super.render(canvas);
}
}
83 changes: 63 additions & 20 deletions packages/flame/lib/src/components/text_box_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -65,26 +65,52 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
String? text,
T? textRenderer,
TextBoxConfig? boxConfig,
Anchor? align,
double? pixelRatio,
Vector2? position,
Vector2? size,
Vector2? scale,
double? angle,
Anchor? anchor,
Iterable<Component>? children,
int? priority,
}) : _boxConfig = boxConfig ?? TextBoxConfig(),
_fixedSize = size != null,
align = align ?? Anchor.topLeft,
pixelRatio = pixelRatio ?? window.devicePixelRatio,
super(
text: text,
textRenderer: textRenderer,
position: position,
scale: scale,
size: size,
angle: angle,
anchor: anchor,
children: children,
priority: priority,
);

/// Alignment of the text within its bounding box.
///
/// This property combines both the horizontal and vertical alignment. For
/// example, setting this property to `Align.center` will make the text
/// centered inside its box. Similarly, `Align.bottomRight` will render the
/// text that's aligned to the right and to the bottom of the box.
///
/// Custom alignment anchors are supported too. For example, if this property
/// is set to `Anchor(0.1, 0)`, then the text would be positioned such that
/// its every line will have 10% of whitespace on the left, and 90% on the
/// right. You can use an `AnchorEffect` to make the text gradually transition
/// between different alignment values.
Anchor align;
spydon marked this conversation as resolved.
Show resolved Hide resolved

/// If true, the size of the component will remain fixed. If false, the size
/// will expand or shrink to the fit the text.
///
/// This property is set to true if the user has explicitly specified [size]
/// in the constructor.
final bool _fixedSize;

@override
set text(String value) {
if (text != value) {
Expand All @@ -96,9 +122,8 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {

@override
@mustCallSuper
Future<void> onLoad() async {
await super.onLoad();
await redraw();
Future<void> onLoad() {
return redraw();
}

@override
Expand All @@ -114,12 +139,13 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
void updateBounds() {
_lines.clear();
double? lineHeight;
final maxBoxWidth = _fixedSize ? width : _boxConfig.maxWidth;
text.split(' ').forEach((word) {
final possibleLine = _lines.isEmpty ? word : '${_lines.last} $word';
lineHeight ??= textRenderer.measureTextHeight(possibleLine);

final textWidth = textRenderer.measureTextWidth(possibleLine);
if (textWidth <= _boxConfig.maxWidth - _boxConfig.margins.horizontal) {
if (textWidth <= maxBoxWidth - _boxConfig.margins.horizontal) {
if (_lines.isNotEmpty) {
_lines.last = possibleLine;
} else {
Expand Down Expand Up @@ -173,7 +199,9 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
}

Vector2 _recomputeSize() {
if (_boxConfig.growingBox) {
if (_fixedSize) {
return size;
} else if (_boxConfig.growingBox) {
var i = 0;
var totalCharCount = 0;
final _currentChar = currentChar;
Expand Down Expand Up @@ -222,28 +250,43 @@ class TextBoxComponent<T extends TextRenderer> extends TextComponent {
/// Override this method to provide a custom background to the text box.
void drawBackground(Canvas c) {}

void _fullRender(Canvas c) {
drawBackground(c);
void _fullRender(Canvas canvas) {
drawBackground(canvas);

final _currentLine = currentLine;
final nLines = currentLine + 1;
final boxWidth = size.x - boxConfig.margins.horizontal;
final boxHeight = size.y - boxConfig.margins.vertical;
var charCount = 0;
var dy = _boxConfig.margins.top;
for (var line = 0; line < _currentLine; line++) {
charCount += _lines[line].length;
_drawLine(c, _lines[line], dy);
dy += _lineHeight;
for (var i = 0; i < nLines; i++) {
var line = _lines[i];
if (i == nLines - 1) {
final nChars = math.min(currentChar - charCount, line.length);
line = line.substring(0, nChars);
}
textRenderer.render(
canvas,
line,
Vector2(
boxConfig.margins.left +
(boxWidth - textRenderer.measureTextWidth(line)) * align.x,
boxConfig.margins.top +
(boxHeight - nLines * _lineHeight) * align.y +
i * _lineHeight,
),
);
charCount += _lines[i].length;
}
final max = math.min(currentChar - charCount, _lines[_currentLine].length);
_drawLine(c, _lines[_currentLine].substring(0, max), dy);
}

void _drawLine(Canvas c, String line, double dy) {
textRenderer.render(c, line, Vector2(_boxConfig.margins.left, dy));
}

Future<void> redraw() async {
final newSize = _recomputeSize();
cache?.dispose();
final cachedImage = cache;
if (cachedImage != null) {
// Do not dispose of the cached image immediately, since it may have been
// sent into the rendering pipeline where it is still pending to be used.
// See issue #1618 for details.
Future.delayed(const Duration(milliseconds: 100), cachedImage.dispose);
}
cache = await _fullRenderAsImage(newSize);
size = newSize;
}
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
75 changes: 73 additions & 2 deletions packages/flame/test/components/text_box_component_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import 'package:canvas_test/canvas_test.dart';
import 'package:flame/components.dart';
import 'package:flame/palette.dart';
import 'package:flame_test/flame_test.dart';
import 'package:test/test.dart';
import 'package:flutter_test/flutter_test.dart';

void main() {
group('TextBoxComponent', () {
Expand All @@ -20,7 +20,7 @@ void main() {
expect(c.size.y, greaterThan(1));
});

flameGame.test('onLoad waits for cache to be done', (game) async {
testWithFlameGame('onLoad waits for cache to be done', (game) async {
final c = TextBoxComponent(text: 'foo bar');

await game.ensureAdd(c);
Expand Down Expand Up @@ -72,5 +72,76 @@ void main() {
expect(c.cache!.debugDisposed, isFalse);
},
);

testGolden(
'Alignment options',
(game) async {
game.addAll([
_FramedTextBox(
text: 'I strike quickly, being moved.',
position: Vector2(10, 10),
size: Vector2(390, 100),
align: Anchor.topLeft,
),
_FramedTextBox(
text: 'But thou art not quickly moved to strike.',
position: Vector2(10, 120),
size: Vector2(390, 115),
align: Anchor.topCenter,
),
_FramedTextBox(
text: 'A dog of the house of Montague moves me.',
position: Vector2(10, 245),
size: Vector2(390, 115),
align: Anchor.topRight,
),
_FramedTextBox(
text: 'To move is to stir, and to be valiant is to stand. '
'Therefore, if thou art moved, thou runn‘st away.',
position: Vector2(10, 370),
size: Vector2(390, 225),
align: Anchor.bottomRight,
),
_FramedTextBox(
text: 'A dog of that house shall move me to stand. I will take '
'the wall of any man or maid of Montague‘s.',
position: Vector2(410, 10),
size: Vector2(380, 300),
align: Anchor.center,
),
_FramedTextBox(
text: 'That shows thee a weak slave; for the weakest goes to the '
'wall.',
position: Vector2(410, 320),
size: Vector2(380, 270),
align: Anchor.centerRight,
),
]);
},
goldenFile: '../_goldens/text_box_component_test_1.png',
);
});
}

class _FramedTextBox extends TextBoxComponent {
_FramedTextBox({
required String text,
Anchor? align,
Vector2? position,
Vector2? size,
}) : super(text: text, align: align, position: position, size: size);

final Paint _borderPaint = Paint()
..style = PaintingStyle.stroke
..strokeWidth = 2
..color = const Color(0xff00ff00);

@override
void render(Canvas canvas) {
canvas.drawRRect(
RRect.fromRectAndRadius(size.toRect(), const Radius.circular(5)),
_borderPaint,
);
super.render(canvas);
}
}