David PHAM-VAN

Implement InlineSpan and WidgetSpan

1 # Changelog 1 # Changelog
2 2
  3 +## 1.3.18
  4 +
  5 +- Implement InlineSpan and WidgetSpan
  6 +
3 ## 1.3.17 7 ## 1.3.17
4 8
5 - Fix MultiPage with multiple save() calls 9 - Fix MultiPage with multiple save() calls
@@ -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 ),