Showing
4 changed files
with
264 additions
and
55 deletions
@@ -18,25 +18,85 @@ part of widget; | @@ -18,25 +18,85 @@ part of widget; | ||
18 | 18 | ||
19 | enum TextAlign { left, right, center, justify } | 19 | enum TextAlign { left, right, center, justify } |
20 | 20 | ||
21 | -class _Word { | ||
22 | - _Word(this.text, this.style, this.metrics, this.annotation); | ||
23 | - | ||
24 | - final String text; | 21 | +abstract class _Span { |
22 | + _Span(this.style, this.annotation); | ||
25 | 23 | ||
26 | final TextStyle style; | 24 | final TextStyle style; |
27 | 25 | ||
28 | - final PdfFontMetrics metrics; | 26 | + final AnnotationBuilder annotation; |
29 | 27 | ||
30 | PdfPoint offset = PdfPoint.zero; | 28 | PdfPoint offset = PdfPoint.zero; |
31 | 29 | ||
32 | - final AnnotationBuilder annotation; | 30 | + double left; |
31 | + double top; | ||
32 | + double width; | ||
33 | + double height; | ||
34 | + | ||
35 | + @override | ||
36 | + String toString() { | ||
37 | + return 'Span "offset:$offset'; | ||
38 | + } | ||
39 | + | ||
40 | + void debugPaint( | ||
41 | + Context context, | ||
42 | + double textScaleFactor, | ||
43 | + PdfRect globalBox, | ||
44 | + ) {} | ||
45 | + | ||
46 | + void paint( | ||
47 | + Context context, | ||
48 | + TextStyle style, | ||
49 | + double textScaleFactor, | ||
50 | + PdfPoint point, | ||
51 | + ); | ||
52 | +} | ||
53 | + | ||
54 | +class _Word extends _Span { | ||
55 | + _Word(this.text, TextStyle style, this.metrics, AnnotationBuilder annotation) | ||
56 | + : super(style, annotation); | ||
57 | + | ||
58 | + final String text; | ||
59 | + | ||
60 | + final PdfFontMetrics metrics; | ||
61 | + | ||
62 | + @override | ||
63 | + double get left => metrics.left; | ||
64 | + | ||
65 | + @override | ||
66 | + double get top => metrics.top; | ||
67 | + | ||
68 | + @override | ||
69 | + double get width => metrics.width; | ||
70 | + | ||
71 | + @override | ||
72 | + double get height => metrics.height; | ||
33 | 73 | ||
34 | @override | 74 | @override |
35 | String toString() { | 75 | String toString() { |
36 | return 'Word "$text" offset:$offset metrics:$metrics style:$style'; | 76 | return 'Word "$text" offset:$offset metrics:$metrics style:$style'; |
37 | } | 77 | } |
38 | 78 | ||
39 | - void debugPaint(Context context, double textScaleFactor, PdfRect globalBox) { | 79 | + @override |
80 | + void paint( | ||
81 | + Context context, | ||
82 | + TextStyle style, | ||
83 | + double textScaleFactor, | ||
84 | + PdfPoint point, | ||
85 | + ) { | ||
86 | + context.canvas.drawString( | ||
87 | + style.font.getFont(context), | ||
88 | + style.fontSize * textScaleFactor, | ||
89 | + text, | ||
90 | + point.x + offset.x, | ||
91 | + point.y + offset.y); | ||
92 | + } | ||
93 | + | ||
94 | + @override | ||
95 | + void debugPaint( | ||
96 | + Context context, | ||
97 | + double textScaleFactor, | ||
98 | + PdfRect globalBox, | ||
99 | + ) { | ||
40 | const double deb = 5; | 100 | const double deb = 5; |
41 | 101 | ||
42 | context.canvas | 102 | context.canvas |
@@ -54,27 +114,122 @@ class _Word { | @@ -54,27 +114,122 @@ class _Word { | ||
54 | } | 114 | } |
55 | } | 115 | } |
56 | 116 | ||
57 | -class TextSpan { | ||
58 | - const TextSpan({this.style, this.text, this.children, this.annotation}); | 117 | +class _WidgetSpan extends _Span { |
118 | + _WidgetSpan(this.widget, TextStyle style, AnnotationBuilder annotation) | ||
119 | + : assert(widget != null), | ||
120 | + assert(style != null), | ||
121 | + super(style, annotation); | ||
59 | 122 | ||
60 | - final TextStyle style; | 123 | + final Widget widget; |
61 | 124 | ||
62 | - final String text; | 125 | + @override |
126 | + double get left => 0; | ||
127 | + | ||
128 | + @override | ||
129 | + double get top => 0; | ||
130 | + | ||
131 | + @override | ||
132 | + double get width => widget.box.width; | ||
133 | + | ||
134 | + @override | ||
135 | + double get height => widget.box.height; | ||
63 | 136 | ||
64 | - final List<TextSpan> children; | 137 | + @override |
138 | + PdfPoint get offset => widget.box.offset; | ||
139 | + | ||
140 | + @override | ||
141 | + set offset(PdfPoint value) { | ||
142 | + widget.box = PdfRect.fromPoints(value, widget.box.size); | ||
143 | + } | ||
144 | + | ||
145 | + @override | ||
146 | + String toString() { | ||
147 | + return 'Widget "$widget" offset:$offset'; | ||
148 | + } | ||
149 | + | ||
150 | + @override | ||
151 | + void paint( | ||
152 | + Context context, | ||
153 | + TextStyle style, | ||
154 | + double textScaleFactor, | ||
155 | + PdfPoint point, | ||
156 | + ) { | ||
157 | + widget.box = PdfRect.fromPoints( | ||
158 | + PdfPoint(point.x + widget.box.offset.x, point.y + widget.box.offset.y), | ||
159 | + widget.box.size); | ||
160 | + widget.paint(context); | ||
161 | + } | ||
162 | +} | ||
163 | + | ||
164 | +@immutable | ||
165 | +abstract class InlineSpan { | ||
166 | + const InlineSpan({this.style, this.baseline, this.annotation}); | ||
167 | + | ||
168 | + final TextStyle style; | ||
169 | + | ||
170 | + final double baseline; | ||
65 | 171 | ||
66 | final AnnotationBuilder annotation; | 172 | final AnnotationBuilder annotation; |
67 | 173 | ||
68 | String toPlainText() { | 174 | String toPlainText() { |
69 | final StringBuffer buffer = StringBuffer(); | 175 | final StringBuffer buffer = StringBuffer(); |
70 | - visitTextSpan((TextSpan span, TextStyle style) { | 176 | + visitChildren((InlineSpan span, TextStyle style) { |
177 | + if (span is TextSpan) { | ||
71 | buffer.write(span.text); | 178 | buffer.write(span.text); |
179 | + } | ||
72 | return true; | 180 | return true; |
73 | }, null); | 181 | }, null); |
74 | return buffer.toString(); | 182 | return buffer.toString(); |
75 | } | 183 | } |
76 | 184 | ||
77 | - bool visitTextSpan(bool visitor(TextSpan span, TextStyle parentStyle), | 185 | + bool visitChildren(bool visitor(InlineSpan span, TextStyle parentStyle), |
186 | + TextStyle parentStyle); | ||
187 | +} | ||
188 | + | ||
189 | +class WidgetSpan extends InlineSpan { | ||
190 | + /// Creates a [WidgetSpan] with the given values. | ||
191 | + const WidgetSpan({ | ||
192 | + @required this.child, | ||
193 | + double baseline = 0, | ||
194 | + TextStyle style, | ||
195 | + AnnotationBuilder annotation, | ||
196 | + }) : assert(child != null), | ||
197 | + super(style: style, baseline: baseline, annotation: annotation); | ||
198 | + | ||
199 | + /// The widget to embed inline within text. | ||
200 | + final Widget child; | ||
201 | + | ||
202 | + /// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk. | ||
203 | + @override | ||
204 | + bool visitChildren(bool visitor(InlineSpan span, TextStyle parentStyle), | ||
205 | + TextStyle parentStyle) { | ||
206 | + final TextStyle _style = parentStyle?.merge(style); | ||
207 | + | ||
208 | + if (child != null) { | ||
209 | + if (!visitor(this, _style)) { | ||
210 | + return false; | ||
211 | + } | ||
212 | + } | ||
213 | + | ||
214 | + return true; | ||
215 | + } | ||
216 | +} | ||
217 | + | ||
218 | +class TextSpan extends InlineSpan { | ||
219 | + const TextSpan({ | ||
220 | + TextStyle style, | ||
221 | + this.text, | ||
222 | + double baseline = 0, | ||
223 | + this.children, | ||
224 | + AnnotationBuilder annotation, | ||
225 | + }) : super(style: style, baseline: baseline, annotation: annotation); | ||
226 | + | ||
227 | + final String text; | ||
228 | + | ||
229 | + final List<InlineSpan> children; | ||
230 | + | ||
231 | + @override | ||
232 | + bool visitChildren(bool visitor(InlineSpan span, TextStyle parentStyle), | ||
78 | TextStyle parentStyle) { | 233 | TextStyle parentStyle) { |
79 | final TextStyle _style = parentStyle?.merge(style); | 234 | final TextStyle _style = parentStyle?.merge(style); |
80 | 235 | ||
@@ -84,8 +239,8 @@ class TextSpan { | @@ -84,8 +239,8 @@ class TextSpan { | ||
84 | } | 239 | } |
85 | } | 240 | } |
86 | if (children != null) { | 241 | if (children != null) { |
87 | - for (TextSpan child in children) { | ||
88 | - if (!child.visitTextSpan(visitor, _style)) { | 242 | + for (InlineSpan child in children) { |
243 | + if (!child.visitChildren(visitor, _style)) { | ||
89 | return false; | 244 | return false; |
90 | } | 245 | } |
91 | } | 246 | } |
@@ -105,7 +260,7 @@ class RichText extends Widget { | @@ -105,7 +260,7 @@ class RichText extends Widget { | ||
105 | 260 | ||
106 | static bool debug = false; | 261 | static bool debug = false; |
107 | 262 | ||
108 | - final TextSpan text; | 263 | + final InlineSpan text; |
109 | 264 | ||
110 | final TextAlign textAlign; | 265 | final TextAlign textAlign; |
111 | 266 | ||
@@ -115,9 +270,9 @@ class RichText extends Widget { | @@ -115,9 +270,9 @@ class RichText extends Widget { | ||
115 | 270 | ||
116 | final int maxLines; | 271 | final int maxLines; |
117 | 272 | ||
118 | - final List<_Word> _words = <_Word>[]; | 273 | + final List<_Span> _spans = <_Span>[]; |
119 | 274 | ||
120 | - double _realignLine(List<_Word> words, double totalWidth, double wordsWidth, | 275 | + double _realignLine(List<_Span> spans, double totalWidth, double wordsWidth, |
121 | bool last, double baseline) { | 276 | bool last, double baseline) { |
122 | double delta = 0; | 277 | double delta = 0; |
123 | switch (textAlign) { | 278 | switch (textAlign) { |
@@ -135,17 +290,17 @@ class RichText extends Widget { | @@ -135,17 +290,17 @@ class RichText extends Widget { | ||
135 | totalWidth = wordsWidth; | 290 | totalWidth = wordsWidth; |
136 | break; | 291 | break; |
137 | } | 292 | } |
138 | - delta = (totalWidth - wordsWidth) / (words.length - 1); | 293 | + delta = (totalWidth - wordsWidth) / (spans.length - 1); |
139 | double x = 0; | 294 | double x = 0; |
140 | - for (_Word word in words) { | ||
141 | - word.offset = word.offset.translate(x, -baseline); | 295 | + for (_Span span in spans) { |
296 | + span.offset = span.offset.translate(x, -baseline); | ||
142 | x += delta; | 297 | x += delta; |
143 | } | 298 | } |
144 | return totalWidth; | 299 | return totalWidth; |
145 | } | 300 | } |
146 | 301 | ||
147 | - for (_Word word in words) { | ||
148 | - word.offset = word.offset.translate(delta, -baseline); | 302 | + for (_Span span in spans) { |
303 | + span.offset = span.offset.translate(delta, -baseline); | ||
149 | } | 304 | } |
150 | return totalWidth; | 305 | return totalWidth; |
151 | } | 306 | } |
@@ -153,7 +308,7 @@ class RichText extends Widget { | @@ -153,7 +308,7 @@ class RichText extends Widget { | ||
153 | @override | 308 | @override |
154 | void layout(Context context, BoxConstraints constraints, | 309 | void layout(Context context, BoxConstraints constraints, |
155 | {bool parentUsesSize = false}) { | 310 | {bool parentUsesSize = false}) { |
156 | - _words.clear(); | 311 | + _spans.clear(); |
157 | 312 | ||
158 | final TextStyle defaultstyle = Theme.of(context).defaultTextStyle; | 313 | final TextStyle defaultstyle = Theme.of(context).defaultTextStyle; |
159 | 314 | ||
@@ -174,7 +329,8 @@ class RichText extends Widget { | @@ -174,7 +329,8 @@ class RichText extends Widget { | ||
174 | int wCount = 0; | 329 | int wCount = 0; |
175 | int lineStart = 0; | 330 | int lineStart = 0; |
176 | 331 | ||
177 | - text.visitTextSpan((TextSpan span, TextStyle style) { | 332 | + text.visitChildren((InlineSpan span, TextStyle style) { |
333 | + if (span is TextSpan) { | ||
178 | if (span.text == null) { | 334 | if (span.text == null) { |
179 | return true; | 335 | return true; |
180 | } | 336 | } |
@@ -199,7 +355,7 @@ class RichText extends Widget { | @@ -199,7 +355,7 @@ class RichText extends Widget { | ||
199 | width = math.max( | 355 | width = math.max( |
200 | width, | 356 | width, |
201 | _realignLine( | 357 | _realignLine( |
202 | - _words.sublist(lineStart), | 358 | + _spans.sublist(lineStart), |
203 | constraintWidth, | 359 | constraintWidth, |
204 | offsetX - space.advanceWidth * style.wordSpacing, | 360 | offsetX - space.advanceWidth * style.wordSpacing, |
205 | false, | 361 | false, |
@@ -222,13 +378,16 @@ class RichText extends Widget { | @@ -222,13 +378,16 @@ class RichText extends Widget { | ||
222 | wCount = 0; | 378 | wCount = 0; |
223 | } | 379 | } |
224 | 380 | ||
225 | - top = math.min(top ?? metrics.top, metrics.top); | ||
226 | - bottom = math.max(bottom ?? metrics.bottom, metrics.bottom); | 381 | + final double baseline = span.baseline * textScaleFactor; |
382 | + top = | ||
383 | + math.min(top ?? metrics.top + baseline, metrics.top + baseline); | ||
384 | + bottom = math.max( | ||
385 | + bottom ?? metrics.bottom + baseline, metrics.bottom + baseline); | ||
227 | 386 | ||
228 | final _Word wd = _Word(word, style, metrics, span.annotation); | 387 | final _Word wd = _Word(word, style, metrics, span.annotation); |
229 | - wd.offset = PdfPoint(offsetX, -offsetY); | 388 | + wd.offset = PdfPoint(offsetX, -offsetY + baseline); |
230 | 389 | ||
231 | - _words.add(wd); | 390 | + _spans.add(wd); |
232 | wCount++; | 391 | wCount++; |
233 | offsetX += | 392 | offsetX += |
234 | metrics.advanceWidth + space.advanceWidth * style.wordSpacing; | 393 | metrics.advanceWidth + space.advanceWidth * style.wordSpacing; |
@@ -238,7 +397,7 @@ class RichText extends Widget { | @@ -238,7 +397,7 @@ class RichText extends Widget { | ||
238 | width = math.max( | 397 | width = math.max( |
239 | width, | 398 | width, |
240 | _realignLine( | 399 | _realignLine( |
241 | - _words.sublist(lineStart), | 400 | + _spans.sublist(lineStart), |
242 | constraintWidth, | 401 | constraintWidth, |
243 | offsetX - space.advanceWidth * style.wordSpacing, | 402 | offsetX - space.advanceWidth * style.wordSpacing, |
244 | false, | 403 | false, |
@@ -267,13 +426,55 @@ class RichText extends Widget { | @@ -267,13 +426,55 @@ class RichText extends Widget { | ||
267 | } | 426 | } |
268 | 427 | ||
269 | offsetX -= space.advanceWidth * style.wordSpacing; | 428 | offsetX -= space.advanceWidth * style.wordSpacing; |
429 | + } else if (span is WidgetSpan) { | ||
430 | + span.child.layout( | ||
431 | + context, | ||
432 | + BoxConstraints.tight(PdfPoint( | ||
433 | + double.infinity, | ||
434 | + style.fontSize * textScaleFactor, | ||
435 | + ))); | ||
436 | + final _WidgetSpan ws = _WidgetSpan(span.child, style, span.annotation); | ||
437 | + | ||
438 | + if (offsetX + ws.width > constraintWidth && wCount > 0) { | ||
439 | + width = math.max( | ||
440 | + width, | ||
441 | + _realignLine(_spans.sublist(lineStart), constraintWidth, offsetX, | ||
442 | + false, bottom)); | ||
443 | + | ||
444 | + lineStart += wCount; | ||
445 | + | ||
446 | + if (maxLines != null && ++lines > maxLines) { | ||
447 | + return false; | ||
448 | + } | ||
449 | + | ||
450 | + offsetX = 0.0; | ||
451 | + offsetY += bottom - top + style.lineSpacing; | ||
452 | + top = null; | ||
453 | + bottom = null; | ||
454 | + | ||
455 | + if (offsetY > constraintHeight) { | ||
456 | + return false; | ||
457 | + } | ||
458 | + wCount = 0; | ||
459 | + } | ||
460 | + | ||
461 | + final double baseline = span.baseline * textScaleFactor; | ||
462 | + top = math.min(top ?? baseline, baseline); | ||
463 | + bottom = math.max(bottom ?? ws.height + baseline, ws.height + baseline); | ||
464 | + | ||
465 | + ws.offset = PdfPoint(offsetX, -offsetY + baseline); | ||
466 | + _spans.add(ws); | ||
467 | + wCount++; | ||
468 | + offsetX += ws.left + ws.width; | ||
469 | + } | ||
470 | + | ||
270 | return true; | 471 | return true; |
271 | }, defaultstyle); | 472 | }, defaultstyle); |
272 | 473 | ||
273 | width = math.max( | 474 | width = math.max( |
274 | width, | 475 | width, |
275 | _realignLine( | 476 | _realignLine( |
276 | - _words.sublist(lineStart), constraintWidth, offsetX, true, bottom)); | 477 | + _spans.sublist(lineStart), constraintWidth, offsetX, true, bottom)); |
277 | 478 | ||
278 | bottom ??= 0.0; | 479 | bottom ??= 0.0; |
279 | top ??= 0.0; | 480 | top ??= 0.0; |
@@ -296,37 +497,34 @@ class RichText extends Widget { | @@ -296,37 +497,34 @@ class RichText extends Widget { | ||
296 | TextStyle currentStyle; | 497 | TextStyle currentStyle; |
297 | PdfColor currentColor; | 498 | PdfColor currentColor; |
298 | 499 | ||
299 | - for (_Word word in _words) { | 500 | + for (_Span span in _spans) { |
300 | assert(() { | 501 | assert(() { |
301 | if (Document.debug && RichText.debug) { | 502 | if (Document.debug && RichText.debug) { |
302 | - word.debugPaint(context, textScaleFactor, box); | 503 | + span.debugPaint(context, textScaleFactor, box); |
303 | } | 504 | } |
304 | return true; | 505 | return true; |
305 | }()); | 506 | }()); |
306 | 507 | ||
307 | - if (word.style != currentStyle) { | ||
308 | - currentStyle = word.style; | 508 | + if (span.style != currentStyle) { |
509 | + currentStyle = span.style; | ||
309 | if (currentStyle.color != currentColor) { | 510 | if (currentStyle.color != currentColor) { |
310 | currentColor = currentStyle.color; | 511 | currentColor = currentStyle.color; |
311 | context.canvas.setFillColor(currentColor); | 512 | context.canvas.setFillColor(currentColor); |
312 | } | 513 | } |
313 | } | 514 | } |
314 | 515 | ||
315 | - if (word.annotation != null) { | ||
316 | - final PdfRect wordBox = PdfRect( | ||
317 | - box.x + word.offset.x + word.metrics.left, | ||
318 | - box.top + word.offset.y + word.metrics.top, | ||
319 | - word.metrics.width, | ||
320 | - word.metrics.height); | ||
321 | - word.annotation.build(context, wordBox); | 516 | + if (span.annotation != null) { |
517 | + final PdfRect spanBox = PdfRect(box.x + span.offset.x + span.left, | ||
518 | + box.top + span.offset.y + span.top, span.width, span.height); | ||
519 | + span.annotation.build(context, spanBox); | ||
322 | } | 520 | } |
323 | 521 | ||
324 | - context.canvas.drawString( | ||
325 | - currentStyle.font.getFont(context), | ||
326 | - currentStyle.fontSize * textScaleFactor, | ||
327 | - word.text, | ||
328 | - box.x + word.offset.x, | ||
329 | - box.top + word.offset.y); | 522 | + span.paint( |
523 | + context, | ||
524 | + currentStyle, | ||
525 | + textScaleFactor, | ||
526 | + PdfPoint(box.left, box.top), | ||
527 | + ); | ||
330 | } | 528 | } |
331 | } | 529 | } |
332 | } | 530 | } |
@@ -4,7 +4,7 @@ description: A pdf producer for Dart. It can create pdf files for both web or fl | @@ -4,7 +4,7 @@ description: A pdf producer for Dart. It can create pdf files for both web or fl | ||
4 | homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf | 4 | homepage: https://github.com/DavBfr/dart_pdf/tree/master/pdf |
5 | repository: https://github.com/DavBfr/dart_pdf | 5 | repository: https://github.com/DavBfr/dart_pdf |
6 | issue_tracker: https://github.com/DavBfr/dart_pdf/issues | 6 | issue_tracker: https://github.com/DavBfr/dart_pdf/issues |
7 | -version: 1.3.17 | 7 | +version: 1.3.18 |
8 | 8 | ||
9 | environment: | 9 | environment: |
10 | sdk: ">=2.1.0 <3.0.0" | 10 | sdk: ">=2.1.0 <3.0.0" |
@@ -169,17 +169,24 @@ void main() { | @@ -169,17 +169,24 @@ void main() { | ||
169 | font: ttf, | 169 | font: ttf, |
170 | fontSize: 20, | 170 | fontSize: 20, |
171 | ), | 171 | ), |
172 | - children: <TextSpan>[ | 172 | + children: <InlineSpan>[ |
173 | TextSpan( | 173 | TextSpan( |
174 | text: 'bold', | 174 | text: 'bold', |
175 | style: TextStyle( | 175 | style: TextStyle( |
176 | - font: ttfBold, | ||
177 | - fontSize: 40, | ||
178 | - color: PdfColors.blue)), | 176 | + font: ttfBold, fontSize: 40, color: PdfColors.blue), |
177 | + children: <InlineSpan>[ | ||
178 | + const TextSpan(text: '*', baseline: 20), | ||
179 | + WidgetSpan(child: PdfLogo(), baseline: -10), | ||
180 | + ]), | ||
179 | TextSpan( | 181 | TextSpan( |
180 | text: ' world!\n', | 182 | text: ' world!\n', |
181 | children: spans, | 183 | children: spans, |
182 | ), | 184 | ), |
185 | + WidgetSpan( | ||
186 | + child: PdfLogo(), | ||
187 | + annotation: AnnotationUrl( | ||
188 | + 'https://github.com/DavBfr/dart_pdf', | ||
189 | + )), | ||
183 | ], | 190 | ], |
184 | ), | 191 | ), |
185 | ), | 192 | ), |
-
Please register or login to post a comment