diff --git a/examples/lib/stories/components/components.dart b/examples/lib/stories/components/components.dart index 1b7aad0ae..ef93fc4e8 100644 --- a/examples/lib/stories/components/components.dart +++ b/examples/lib/stories/components/components.dart @@ -10,6 +10,7 @@ import 'package:examples/stories/components/keys_example.dart'; import 'package:examples/stories/components/look_at_example.dart'; import 'package:examples/stories/components/look_at_smooth_example.dart'; import 'package:examples/stories/components/priority_example.dart'; +import 'package:examples/stories/components/skip_text_box_component_example.dart'; import 'package:examples/stories/components/spawn_component_example.dart'; import 'package:examples/stories/components/time_scale_example.dart'; import 'package:flame/game.dart'; @@ -92,5 +93,11 @@ void addComponentsStories(Dashbook dashbook) { (_) => GameWidget(game: HasVisibilityExample()), codeLink: baseLink('components/has_visibility_example.dart'), info: HasVisibilityExample.description, + ) + ..add( + 'Skip TextBoxComponent', + (_) => GameWidget(game: SkipTextBoxComponentExample()), + codeLink: baseLink('components/skip_text_box_component_example.dart'), + info: SkipTextBoxComponentExample.description, ); } diff --git a/examples/lib/stories/components/skip_text_box_component_example.dart b/examples/lib/stories/components/skip_text_box_component_example.dart new file mode 100644 index 000000000..21cfca57e --- /dev/null +++ b/examples/lib/stories/components/skip_text_box_component_example.dart @@ -0,0 +1,45 @@ +import 'dart:async'; + +import 'package:flame/components.dart'; +import 'package:flame/game.dart'; +import 'package:flame/input.dart'; + +class SkipTextBoxComponentExample extends FlameGame { + static const String description = ''' + On this example, click on the "Skip" button to display all the text at once. + '''; + + @override + FutureOr onLoad() { + final textBoxComponent = TextBoxComponent( + text: samplePassage, + position: Vector2(48, 48 * 2), + boxConfig: const TextBoxConfig( + maxWidth: 480, + timePerChar: 0.01, + ), + ); + addAll([ + ButtonComponent( + position: Vector2(48, 48), + button: TextComponent(text: 'Skip'), + onReleased: textBoxComponent.skip, + ), + textBoxComponent, + ]); + } + + static const String samplePassage = ''' +Look again at that dot. That's here. That's home. That's us. On it everyone you love, everyone you know, everyone you ever heard of, every human being who ever was, lived out their lives. The aggregate of our joy and suffering, thousands of confident religions, ideologies, and economic doctrines, every hunter and forager, every hero and coward, every creator and destroyer of civilization, every king and peasant, every young couple in love, every mother and father, hopeful child, inventor and explorer, every teacher of morals, every corrupt politician, every "superstar," every "supreme leader," every saint and sinner in the history of our species lived there--on a mote of dust suspended in a sunbeam. + +The Earth is a very small stage in a vast cosmic arena. Think of the rivers of blood spilled by all those generals and emperors so that, in glory and triumph, they could become the momentary masters of a fraction of a dot. Think of the endless cruelties visited by the inhabitants of one corner of this pixel on the scarcely distinguishable inhabitants of some other corner, how frequent their misunderstandings, how eager they are to kill one another, how fervent their hatreds. + +Our posturings, our imagined self-importance, the delusion that we have some privileged position in the Universe, are challenged by this point of pale light. Our planet is a lonely speck in the great enveloping cosmic dark. In our obscurity, in all this vastness, there is no hint that help will come from elsewhere to save us from ourselves. + +The Earth is the only world known so far to harbor life. There is nowhere else, at least in the near future, to which our species could migrate. Visit, yes. Settle, not yet. Like it or not, for the moment the Earth is where we make our stand. + +It has been said that astronomy is a humbling and character-building experience. There is perhaps no better demonstration of the folly of human conceits than this distant image of our tiny world. To me, it underscores our responsibility to deal more kindly with one another, and to preserve and cherish the pale blue dot, the only home we've ever known. + +— Carl Sagan, Pale Blue Dot, 1994 +'''; +} diff --git a/packages/flame/lib/src/components/text_box_component.dart b/packages/flame/lib/src/components/text_box_component.dart index dd4c1f230..c117c9cb6 100644 --- a/packages/flame/lib/src/components/text_box_component.dart +++ b/packages/flame/lib/src/components/text_box_component.dart @@ -65,7 +65,7 @@ class TextBoxConfig { class TextBoxComponent extends TextComponent { static final Paint _imagePaint = BasicPalette.white.paint() ..filterQuality = FilterQuality.medium; - final TextBoxConfig _boxConfig; + TextBoxConfig boxConfig; final double pixelRatio; @visibleForTesting @@ -94,7 +94,6 @@ class TextBoxComponent extends TextComponent { /// Callback function to be executed after all text is displayed. void Function()? onComplete; - TextBoxConfig get boxConfig => _boxConfig; double get lineHeight => _lineHeight; TextBoxComponent({ @@ -112,7 +111,7 @@ class TextBoxComponent extends TextComponent { super.priority, this.onComplete, super.key, - }) : _boxConfig = boxConfig ?? const TextBoxConfig(), + }) : boxConfig = boxConfig ?? const TextBoxConfig(), _fixedSize = size != null, align = align ?? Anchor.topLeft, pixelRatio = pixelRatio ?? @@ -167,7 +166,7 @@ class TextBoxComponent extends TextComponent { void updateBounds() { lines.clear(); var lineHeight = 0.0; - final maxBoxWidth = _fixedSize ? width : _boxConfig.maxWidth; + final maxBoxWidth = _fixedSize ? width : boxConfig.maxWidth; for (final word in text.split(' ')) { final wordLines = word.split('\n'); final possibleLine = @@ -177,7 +176,7 @@ class TextBoxComponent extends TextComponent { _updateMaxWidth(metrics.width); final bool canAppend; - if (metrics.width <= maxBoxWidth - _boxConfig.margins.horizontal) { + if (metrics.width <= maxBoxWidth - boxConfig.margins.horizontal) { canAppend = lines.isNotEmpty; } else { canAppend = lines.isNotEmpty && lines.last == ''; @@ -204,18 +203,18 @@ class TextBoxComponent extends TextComponent { } } - double get totalCharTime => text.length * _boxConfig.timePerChar; + double get totalCharTime => text.length * boxConfig.timePerChar; bool get finished => - _lifeTime >= totalCharTime + (_boxConfig.dismissDelay ?? 0); + _lifeTime >= totalCharTime + (boxConfig.dismissDelay ?? 0); int get _actualTextLength { return lines.map((e) => e.length).sum; } - int get currentChar => _boxConfig.timePerChar == 0.0 + int get currentChar => boxConfig.timePerChar == 0.0 ? _actualTextLength - : math.min(_lifeTime ~/ _boxConfig.timePerChar, _actualTextLength); + : math.min(_lifeTime ~/ boxConfig.timePerChar, _actualTextLength); int get currentLine { var totalCharCount = 0; @@ -240,7 +239,7 @@ class TextBoxComponent extends TextComponent { Vector2 _recomputeSize() { if (_fixedSize) { return size; - } else if (_boxConfig.growingBox) { + } else if (boxConfig.growingBox) { var i = 0; var totalCharCount = 0; final cachedCurrentChar = currentChar; @@ -254,13 +253,13 @@ class TextBoxComponent extends TextComponent { return getLineWidth(line, charCount); }).reduce(math.max); return Vector2( - textWidth + _boxConfig.margins.horizontal, - _lineHeight * lines.length + _boxConfig.margins.vertical, + textWidth + boxConfig.margins.horizontal, + _lineHeight * lines.length + boxConfig.margins.vertical, ); } else { return Vector2( - _boxConfig.maxWidth + _boxConfig.margins.horizontal, - _lineHeight * _totalLines + _boxConfig.margins.vertical, + boxConfig.maxWidth + boxConfig.margins.horizontal, + _lineHeight * _totalLines + boxConfig.margins.vertical, ); } } @@ -358,7 +357,7 @@ class TextBoxComponent extends TextComponent { _isOnCompleteExecuted = true; onComplete?.call(); } - if (_boxConfig.dismissDelay != null) { + if (boxConfig.dismissDelay != null) { removeFromParent(); } } @@ -371,4 +370,17 @@ class TextBoxComponent extends TextComponent { cache?.dispose(); cache = null; } + + /// Force [TextBoxComponent] to display [text] in its entirety. + /// + /// It is possible that in the future, one may want to revert timePerChar or + /// even the old [boxConfig] value to its previous value once [onComplete] + /// is called. Such a case might be when a new value of [text] is set. + /// However, this is non-trivial task, so this implementation is intentionally + /// kept simple. + /// If this behavior is needed, the user can simply add the code for setting + /// [boxConfig] by themselves in [onComplete]. + void skip() { + boxConfig = boxConfig.copyWith(timePerChar: 0); + } } diff --git a/packages/flame/test/components/text_box_component_test.dart b/packages/flame/test/components/text_box_component_test.dart index 2fe583d75..5c99ea3bc 100644 --- a/packages/flame/test/components/text_box_component_test.dart +++ b/packages/flame/test/components/text_box_component_test.dart @@ -46,6 +46,40 @@ void main() { ); }); + test('boxConfig gets set', () { + const firstConfig = TextBoxConfig(maxWidth: 400, timePerChar: 0.1); + const secondConfig = TextBoxConfig(maxWidth: 300, timePerChar: 0.2); + final c = TextBoxComponent( + text: 'The quick brown fox jumps over the lazy dog.', + boxConfig: firstConfig, + ); + expect( + c.boxConfig, + firstConfig, + ); + c.boxConfig = secondConfig; + expect( + c.boxConfig, + secondConfig, + ); + }); + + test('skip method sets boxConfig timePerChar to 0', () { + const firstConfig = TextBoxConfig(maxWidth: 400, timePerChar: 0.1); + final c = TextBoxComponent( + text: 'The quick brown fox jumps over the lazy dog.', + boxConfig: firstConfig, + ); + expect( + c.boxConfig, + firstConfig, + ); + c.skip(); + expect(c.boxConfig.timePerChar, 0); + // other props are preserved + expect(c.boxConfig.maxWidth, 400); + }); + testWithFlameGame( 'setting dismissDelay removes component when finished', (game) async { @@ -142,6 +176,35 @@ lines.''', expect(lineSize, greaterThan(0)); }); + testWithFlameGame('TextBoxComponent skips to the end of text', + (game) async { + final textBoxComponent1 = TextBoxComponent( + text: 'aaa', + boxConfig: const TextBoxConfig(timePerChar: 1.0), + ); + await game.ensureAdd(textBoxComponent1); + // forward time by 2.5 seconds + game.update(2.5); + expect(textBoxComponent1.finished, false); + // flush + game.update(0.6); + expect(textBoxComponent1.finished, true); + + // reset + await game.ensureRemove(textBoxComponent1); + + final textBoxComponent2 = TextBoxComponent( + text: 'aaa', + boxConfig: const TextBoxConfig(timePerChar: 1.0), + ); + await game.ensureAdd(textBoxComponent2); + expect(textBoxComponent2.finished, false); + // Simulate running 0.5 seconds before skipping + game.update(0.5); + textBoxComponent2.skip(); + expect(textBoxComponent2.finished, true); + }); + testGolden( 'Alignment options', (game) async {