Line data Source code
1 : import 'dart:async';
2 : import 'dart:math' as math;
3 : import 'dart:ui';
4 :
5 : import 'package:flutter/widgets.dart' hide Image;
6 :
7 : import '../extensions/vector2.dart';
8 : import '../palette.dart';
9 : import '../text.dart';
10 : import 'position_component.dart';
11 :
12 : /// A set of configurations for the [TextBoxComponent] itself (as opposed to
13 : /// the [TextRenderer], that contains the configuration for how to render the
14 : /// text only (font size, color, family, etc)).
15 : class TextBoxConfig {
16 : /// Max width this paragraph can take. Lines will be broken trying to respect
17 : /// word boundaries in as many lines as necessary.
18 : final double maxWidth;
19 :
20 : /// Margins of the text box w.r.t the [TextBoxComponent.size].
21 : final EdgeInsets margins;
22 :
23 : /// Defaults to 0. If not zero the characters will appear one by one giving
24 : /// a typying effect to the text box, and this will be the delay in seconds
25 : /// between each char.
26 : final double timePerChar;
27 :
28 : /// Defaults to 9. If not zero, this component will disapear this amount of
29 : /// seconds after being completed (if [timePerChar] is set) or after first
30 : /// appearing (otherwise).
31 : final double dismissDelay;
32 :
33 : /// Only relevant if [timePerChar] is set. If true, the box will start with
34 : /// the size to fit the first character and grow as more lines are typed.
35 : /// If false, the box will start with the full necessary size from the
36 : /// beginning (both width and height).
37 : final bool growingBox;
38 :
39 0 : TextBoxConfig({
40 : this.maxWidth = 200.0,
41 : this.margins = const EdgeInsets.all(8.0),
42 : this.timePerChar = 0.0,
43 : this.dismissDelay = 0.0,
44 : this.growingBox = false,
45 : });
46 : }
47 :
48 : class TextBoxComponent<T extends TextRenderer> extends PositionComponent {
49 0 : static final Paint _imagePaint = BasicPalette.white.paint()
50 : ..filterQuality = FilterQuality.high;
51 :
52 : final String _text;
53 : final T _textRenderer;
54 : final TextBoxConfig _boxConfig;
55 :
56 : late List<String> _lines;
57 : double _maxLineWidth = 0.0;
58 : late double _lineHeight;
59 : late int _totalLines;
60 :
61 : double _lifeTime = 0.0;
62 : Image? _cache;
63 : int? _previousChar;
64 :
65 0 : String get text => _text;
66 :
67 0 : TextRenderer get renderer => _textRenderer;
68 :
69 0 : TextBoxConfig get boxConfig => _boxConfig;
70 :
71 0 : TextBoxComponent(
72 : String text, {
73 : T? textRenderer,
74 : TextBoxConfig? boxConfig,
75 : Vector2? position,
76 : Vector2? size,
77 : int? priority,
78 : }) : _text = text,
79 0 : _boxConfig = boxConfig ?? TextBoxConfig(),
80 0 : _textRenderer = textRenderer ?? TextRenderer.createDefault<T>(),
81 0 : super(position: position, size: size, priority: priority) {
82 0 : _lines = [];
83 : double? lineHeight;
84 0 : text.split(' ').forEach((word) {
85 0 : final possibleLine = _lines.isEmpty ? word : '${_lines.last} $word';
86 0 : lineHeight ??= _textRenderer.measureTextHeight(possibleLine);
87 :
88 0 : final textWidth = _textRenderer.measureTextWidth(possibleLine);
89 0 : if (textWidth <= _boxConfig.maxWidth - _boxConfig.margins.horizontal) {
90 0 : if (_lines.isNotEmpty) {
91 0 : _lines.last = possibleLine;
92 : } else {
93 0 : _lines.add(possibleLine);
94 : }
95 0 : _updateMaxWidth(textWidth);
96 : } else {
97 0 : _lines.add(word);
98 0 : _updateMaxWidth(textWidth);
99 : }
100 : });
101 0 : _totalLines = _lines.length;
102 0 : _lineHeight = lineHeight ?? 0.0;
103 : }
104 :
105 0 : void _updateMaxWidth(double w) {
106 0 : if (w > _maxLineWidth) {
107 0 : _maxLineWidth = w;
108 : }
109 : }
110 :
111 0 : double get totalCharTime => _text.length * _boxConfig.timePerChar;
112 :
113 0 : bool get finished => _lifeTime > totalCharTime + _boxConfig.dismissDelay;
114 :
115 0 : int get _actualTextLength {
116 0 : return _lines.map((e) => e.length).fold(0, (p, c) => p + c);
117 : }
118 :
119 0 : int get currentChar => _boxConfig.timePerChar == 0.0
120 0 : ? _actualTextLength
121 0 : : math.min(_lifeTime ~/ _boxConfig.timePerChar, _actualTextLength);
122 :
123 0 : int get currentLine {
124 : var totalCharCount = 0;
125 0 : final _currentChar = currentChar;
126 0 : for (var i = 0; i < _lines.length; i++) {
127 0 : totalCharCount += _lines[i].length;
128 0 : if (totalCharCount > _currentChar) {
129 : return i;
130 : }
131 : }
132 0 : return _lines.length - 1;
133 : }
134 :
135 0 : @override
136 0 : Vector2 get size => Vector2(width, height);
137 :
138 0 : double getLineWidth(String line, int charCount) {
139 0 : return _textRenderer.measureTextWidth(
140 0 : line.substring(0, math.min(charCount, line.length)),
141 : );
142 : }
143 :
144 : double? _cachedWidth;
145 :
146 0 : @override
147 : double get width {
148 0 : if (_cachedWidth != null) {
149 0 : return _cachedWidth!;
150 : }
151 0 : if (_boxConfig.growingBox) {
152 : var i = 0;
153 : var totalCharCount = 0;
154 0 : final _currentChar = currentChar;
155 0 : final _currentLine = currentLine;
156 0 : final textWidth = _lines.sublist(0, _currentLine + 1).map((line) {
157 : final charCount =
158 0 : (i < _currentLine) ? line.length : (_currentChar - totalCharCount);
159 0 : totalCharCount += line.length;
160 0 : i++;
161 0 : return getLineWidth(line, charCount);
162 0 : }).reduce(math.max);
163 0 : _cachedWidth = textWidth + _boxConfig.margins.horizontal;
164 : } else {
165 0 : _cachedWidth = _boxConfig.maxWidth + _boxConfig.margins.horizontal;
166 : }
167 0 : return _cachedWidth!;
168 : }
169 :
170 0 : @override
171 : double get height {
172 0 : if (_boxConfig.growingBox) {
173 0 : return _lineHeight * _lines.length + _boxConfig.margins.vertical;
174 : } else {
175 0 : return _lineHeight * _totalLines + _boxConfig.margins.vertical;
176 : }
177 : }
178 :
179 0 : @override
180 : void render(Canvas c) {
181 0 : if (_cache == null) {
182 : return;
183 : }
184 0 : super.render(c);
185 0 : final devicePixelRatio = window.devicePixelRatio;
186 0 : c.save();
187 0 : c.scale(1 / devicePixelRatio);
188 0 : c.drawImage(_cache!, Offset.zero, _imagePaint);
189 0 : c.restore();
190 : }
191 :
192 0 : Future<Image> _redrawCache() {
193 0 : final devicePixelRatio = window.devicePixelRatio;
194 0 : final recorder = PictureRecorder();
195 0 : final c = Canvas(recorder, size.toRect());
196 0 : c.scale(devicePixelRatio);
197 0 : _fullRender(c);
198 0 : return recorder.endRecording().toImage(
199 0 : (width * devicePixelRatio).ceil(),
200 0 : (height * devicePixelRatio).ceil(),
201 : );
202 : }
203 :
204 : /// Override this method to provide a custom background to the text box.
205 0 : void drawBackground(Canvas c) {}
206 :
207 0 : void _fullRender(Canvas c) {
208 0 : drawBackground(c);
209 :
210 0 : final _currentLine = currentLine;
211 : var charCount = 0;
212 0 : var dy = _boxConfig.margins.top;
213 0 : for (var line = 0; line < _currentLine; line++) {
214 0 : charCount += _lines[line].length;
215 0 : _drawLine(c, _lines[line], dy);
216 0 : dy += _lineHeight;
217 : }
218 0 : final max = math.min(currentChar - charCount, _lines[_currentLine].length);
219 0 : _drawLine(c, _lines[_currentLine].substring(0, max), dy);
220 : }
221 :
222 0 : void _drawLine(Canvas c, String line, double dy) {
223 0 : _textRenderer.render(c, line, Vector2(_boxConfig.margins.left, dy));
224 : }
225 :
226 0 : void redrawLater() async {
227 0 : _cache = await _redrawCache();
228 : }
229 :
230 0 : @override
231 : void update(double dt) {
232 0 : super.update(dt);
233 0 : _lifeTime += dt;
234 0 : if (_previousChar != currentChar) {
235 0 : _cachedWidth = null;
236 0 : redrawLater();
237 : }
238 0 : _previousChar = currentChar;
239 : }
240 : }
|