Showing
4 changed files
with
153 additions
and
29 deletions
@@ -9,6 +9,7 @@ | @@ -9,6 +9,7 @@ | ||
9 | - Use covariant on SpanningWidget | 9 | - Use covariant on SpanningWidget |
10 | - ImageProvider.resolve returns non-null object | 10 | - ImageProvider.resolve returns non-null object |
11 | - Fix textScalingFactor with lineSpacing | 11 | - Fix textScalingFactor with lineSpacing |
12 | +- Implement SpanningWidget on RichText | ||
12 | 13 | ||
13 | ## 3.2.0 | 14 | ## 3.2.0 |
14 | 15 |
@@ -22,6 +22,7 @@ import 'package:pdf/pdf.dart'; | @@ -22,6 +22,7 @@ import 'package:pdf/pdf.dart'; | ||
22 | import 'annotations.dart'; | 22 | import 'annotations.dart'; |
23 | import 'document.dart'; | 23 | import 'document.dart'; |
24 | import 'geometry.dart'; | 24 | import 'geometry.dart'; |
25 | +import 'multi_page.dart'; | ||
25 | import 'text_style.dart'; | 26 | import 'text_style.dart'; |
26 | import 'theme.dart'; | 27 | import 'theme.dart'; |
27 | import 'widget.dart'; | 28 | import 'widget.dart'; |
@@ -30,6 +31,15 @@ enum TextAlign { left, right, center, justify } | @@ -30,6 +31,15 @@ enum TextAlign { left, right, center, justify } | ||
30 | 31 | ||
31 | enum TextDirection { ltr, rtl } | 32 | enum TextDirection { ltr, rtl } |
32 | 33 | ||
34 | +/// How overflowing text should be handled. | ||
35 | +enum TextOverflow { | ||
36 | + /// Span to the next page when possible. | ||
37 | + span, | ||
38 | + | ||
39 | + /// Render overflowing text outside of its container. | ||
40 | + visible, | ||
41 | +} | ||
42 | + | ||
33 | abstract class _Span { | 43 | abstract class _Span { |
34 | _Span(this.style); | 44 | _Span(this.style); |
35 | 45 | ||
@@ -81,6 +91,7 @@ class _TextDecoration { | @@ -81,6 +91,7 @@ class _TextDecoration { | ||
81 | if (_box != null) { | 91 | if (_box != null) { |
82 | return _box; | 92 | return _box; |
83 | } | 93 | } |
94 | + | ||
84 | final x1 = spans[startSpan].offset.x + spans[startSpan].left; | 95 | final x1 = spans[startSpan].offset.x + spans[startSpan].left; |
85 | final x2 = | 96 | final x2 = |
86 | spans[endSpan].offset.x + spans[endSpan].left + spans[endSpan].width; | 97 | spans[endSpan].offset.x + spans[endSpan].left + spans[endSpan].width; |
@@ -475,6 +486,7 @@ class _Line { | @@ -475,6 +486,7 @@ class _Line { | ||
475 | 486 | ||
476 | final int firstSpan; | 487 | final int firstSpan; |
477 | final int countSpan; | 488 | final int countSpan; |
489 | + int get lastSpan => firstSpan + countSpan; | ||
478 | 490 | ||
479 | TextAlign get textAlign => parent._textAlign; | 491 | TextAlign get textAlign => parent._textAlign; |
480 | 492 | ||
@@ -484,12 +496,17 @@ class _Line { | @@ -484,12 +496,17 @@ class _Line { | ||
484 | 496 | ||
485 | final TextDirection textDirection; | 497 | final TextDirection textDirection; |
486 | 498 | ||
499 | + double get height => parent._spans | ||
500 | + .sublist(firstSpan, lastSpan) | ||
501 | + .reduce((a, b) => a.height > b.height ? a : b) | ||
502 | + .height; | ||
503 | + | ||
487 | @override | 504 | @override |
488 | String toString() => | 505 | String toString() => |
489 | - '$runtimeType $firstSpan-${firstSpan + countSpan} baseline: $baseline width:$wordsWidth'; | 506 | + '$runtimeType $firstSpan-$lastSpan baseline: $baseline width:$wordsWidth'; |
490 | 507 | ||
491 | void realign(double totalWidth, bool isLast) { | 508 | void realign(double totalWidth, bool isLast) { |
492 | - final spans = parent._spans.sublist(firstSpan, firstSpan + countSpan); | 509 | + final spans = parent._spans.sublist(firstSpan, lastSpan); |
493 | 510 | ||
494 | var delta = 0.0; | 511 | var delta = 0.0; |
495 | switch (textAlign) { | 512 | switch (textAlign) { |
@@ -534,7 +551,31 @@ class _Line { | @@ -534,7 +551,31 @@ class _Line { | ||
534 | } | 551 | } |
535 | } | 552 | } |
536 | 553 | ||
537 | -class RichText extends Widget { | 554 | +class _RichTextContext extends WidgetContext { |
555 | + var startOffset = 0.0; | ||
556 | + var endOffset = 0.0; | ||
557 | + var spanStart = 0; | ||
558 | + var spanEnd = 0; | ||
559 | + | ||
560 | + @override | ||
561 | + void apply(_RichTextContext other) { | ||
562 | + startOffset = other.startOffset; | ||
563 | + endOffset = other.endOffset; | ||
564 | + spanStart = other.spanStart; | ||
565 | + spanEnd = other.spanEnd; | ||
566 | + } | ||
567 | + | ||
568 | + @override | ||
569 | + WidgetContext clone() { | ||
570 | + return _RichTextContext()..apply(this); | ||
571 | + } | ||
572 | + | ||
573 | + @override | ||
574 | + String toString() => | ||
575 | + '$runtimeType Offset: $startOffset -> $endOffset Span: $spanStart -> $spanEnd'; | ||
576 | +} | ||
577 | + | ||
578 | +class RichText extends Widget implements SpanningWidget { | ||
538 | RichText({ | 579 | RichText({ |
539 | required this.text, | 580 | required this.text, |
540 | this.textAlign, | 581 | this.textAlign, |
@@ -543,6 +584,7 @@ class RichText extends Widget { | @@ -543,6 +584,7 @@ class RichText extends Widget { | ||
543 | this.tightBounds = false, | 584 | this.tightBounds = false, |
544 | this.textScaleFactor = 1.0, | 585 | this.textScaleFactor = 1.0, |
545 | this.maxLines, | 586 | this.maxLines, |
587 | + this.overflow = TextOverflow.visible, | ||
546 | }); | 588 | }); |
547 | 589 | ||
548 | static bool debug = false; | 590 | static bool debug = false; |
@@ -567,6 +609,10 @@ class RichText extends Widget { | @@ -567,6 +609,10 @@ class RichText extends Widget { | ||
567 | 609 | ||
568 | final List<_TextDecoration> _decorations = <_TextDecoration>[]; | 610 | final List<_TextDecoration> _decorations = <_TextDecoration>[]; |
569 | 611 | ||
612 | + final _context = _RichTextContext(); | ||
613 | + | ||
614 | + final TextOverflow overflow; | ||
615 | + | ||
570 | void _appendDecoration(bool append, _TextDecoration td) { | 616 | void _appendDecoration(bool append, _TextDecoration td) { |
571 | if (append && _decorations.isNotEmpty) { | 617 | if (append && _decorations.isNotEmpty) { |
572 | final last = _decorations.last; | 618 | final last = _decorations.last; |
@@ -601,7 +647,7 @@ class RichText extends Widget { | @@ -601,7 +647,7 @@ class RichText extends Widget { | ||
601 | : constraints.constrainHeight(); | 647 | : constraints.constrainHeight(); |
602 | 648 | ||
603 | var offsetX = 0.0; | 649 | var offsetX = 0.0; |
604 | - var offsetY = 0.0; | 650 | + var offsetY = _context.startOffset; |
605 | 651 | ||
606 | var top = 0.0; | 652 | var top = 0.0; |
607 | var bottom = 0.0; | 653 | var bottom = 0.0; |
@@ -664,19 +710,20 @@ class RichText extends Widget { | @@ -664,19 +710,20 @@ class RichText extends Widget { | ||
664 | spanStart += spanCount; | 710 | spanStart += spanCount; |
665 | spanCount = 0; | 711 | spanCount = 0; |
666 | 712 | ||
667 | - if (_maxLines != null && lines.length >= _maxLines) { | ||
668 | - return false; | ||
669 | - } | ||
670 | - | ||
671 | offsetX = 0.0; | 713 | offsetX = 0.0; |
672 | - offsetY += bottom - top + style.lineSpacing! * textScaleFactor; | ||
673 | - | 714 | + offsetY += bottom - top; |
674 | top = 0; | 715 | top = 0; |
675 | bottom = 0; | 716 | bottom = 0; |
676 | 717 | ||
718 | + if (_maxLines != null && lines.length >= _maxLines) { | ||
719 | + return false; | ||
720 | + } | ||
721 | + | ||
677 | if (offsetY > constraintHeight) { | 722 | if (offsetY > constraintHeight) { |
678 | return false; | 723 | return false; |
679 | } | 724 | } |
725 | + | ||
726 | + offsetY += style.lineSpacing! * textScaleFactor; | ||
680 | } else { | 727 | } else { |
681 | // One word Overflow, try to split it. | 728 | // One word Overflow, try to split it. |
682 | final pos = _splitWord(word, font, style, constraintWidth); | 729 | final pos = _splitWord(word, font, style, constraintWidth); |
@@ -733,26 +780,25 @@ class RichText extends Widget { | @@ -733,26 +780,25 @@ class RichText extends Widget { | ||
733 | 780 | ||
734 | spanStart += spanCount; | 781 | spanStart += spanCount; |
735 | 782 | ||
736 | - if (_maxLines != null && lines.length >= _maxLines) { | ||
737 | - spanCount = 0; | ||
738 | - return false; | ||
739 | - } | ||
740 | - | ||
741 | offsetX = 0.0; | 783 | offsetX = 0.0; |
742 | if (spanCount > 0) { | 784 | if (spanCount > 0) { |
743 | - offsetY += bottom - top + style.lineSpacing! * textScaleFactor; | 785 | + offsetY += bottom - top; |
744 | } else { | 786 | } else { |
745 | - offsetY += space.ascent + | ||
746 | - space.descent + | ||
747 | - style.lineSpacing! * textScaleFactor; | 787 | + offsetY += space.ascent + space.descent; |
748 | } | 788 | } |
749 | top = 0; | 789 | top = 0; |
750 | bottom = 0; | 790 | bottom = 0; |
751 | spanCount = 0; | 791 | spanCount = 0; |
752 | 792 | ||
793 | + if (_maxLines != null && lines.length >= _maxLines) { | ||
794 | + return false; | ||
795 | + } | ||
796 | + | ||
753 | if (offsetY > constraintHeight) { | 797 | if (offsetY > constraintHeight) { |
754 | return false; | 798 | return false; |
755 | } | 799 | } |
800 | + | ||
801 | + offsetY += style.lineSpacing! * textScaleFactor; | ||
756 | } | 802 | } |
757 | } | 803 | } |
758 | 804 | ||
@@ -789,13 +835,15 @@ class RichText extends Widget { | @@ -789,13 +835,15 @@ class RichText extends Widget { | ||
789 | } | 835 | } |
790 | 836 | ||
791 | offsetX = 0.0; | 837 | offsetX = 0.0; |
792 | - offsetY += bottom - top + style.lineSpacing! * textScaleFactor; | 838 | + offsetY += bottom - top; |
793 | top = 0; | 839 | top = 0; |
794 | bottom = 0; | 840 | bottom = 0; |
795 | 841 | ||
796 | if (offsetY > constraintHeight) { | 842 | if (offsetY > constraintHeight) { |
797 | return false; | 843 | return false; |
798 | } | 844 | } |
845 | + | ||
846 | + offsetY += style.lineSpacing! * textScaleFactor; | ||
799 | } | 847 | } |
800 | 848 | ||
801 | final baseline = span.baseline! * textScaleFactor; | 849 | final baseline = span.baseline! * textScaleFactor; |
@@ -834,6 +882,7 @@ class RichText extends Widget { | @@ -834,6 +882,7 @@ class RichText extends Widget { | ||
834 | offsetX, | 882 | offsetX, |
835 | _textDirection, | 883 | _textDirection, |
836 | )); | 884 | )); |
885 | + offsetY += bottom - top; | ||
837 | } | 886 | } |
838 | 887 | ||
839 | assert(!overflow || constraintWidth.isFinite); | 888 | assert(!overflow || constraintWidth.isFinite); |
@@ -855,7 +904,29 @@ class RichText extends Widget { | @@ -855,7 +904,29 @@ class RichText extends Widget { | ||
855 | } | 904 | } |
856 | 905 | ||
857 | box = PdfRect(0, 0, constraints.constrainWidth(width), | 906 | box = PdfRect(0, 0, constraints.constrainWidth(width), |
858 | - constraints.constrainHeight(offsetY + bottom - top)); | 907 | + constraints.constrainHeight(offsetY)); |
908 | + | ||
909 | + _context | ||
910 | + ..endOffset = offsetY - _context.startOffset | ||
911 | + ..spanEnd = _spans.length; | ||
912 | + | ||
913 | + if (this.overflow != TextOverflow.span) { | ||
914 | + return; | ||
915 | + } | ||
916 | + | ||
917 | + if (offsetY > constraintHeight + 0.0001) { | ||
918 | + _context.spanEnd -= lines.last.countSpan; | ||
919 | + _context.endOffset -= lines.last.height; | ||
920 | + } | ||
921 | + | ||
922 | + for (var index = 0; index < _decorations.length; index++) { | ||
923 | + final decoration = _decorations[index]; | ||
924 | + if (decoration.startSpan >= _context.spanEnd || | ||
925 | + decoration.endSpan < _context.spanStart) { | ||
926 | + _decorations.removeAt(index); | ||
927 | + index--; | ||
928 | + } | ||
929 | + } | ||
859 | } | 930 | } |
860 | 931 | ||
861 | @override | 932 | @override |
@@ -894,7 +965,7 @@ class RichText extends Widget { | @@ -894,7 +965,7 @@ class RichText extends Widget { | ||
894 | ); | 965 | ); |
895 | } | 966 | } |
896 | 967 | ||
897 | - for (var span in _spans) { | 968 | + for (var span in _spans.sublist(_context.spanStart, _context.spanEnd)) { |
898 | assert(() { | 969 | assert(() { |
899 | if (Document.debug && RichText.debug) { | 970 | if (Document.debug && RichText.debug) { |
900 | span.debugPaint(context, textScaleFactor, box); | 971 | span.debugPaint(context, textScaleFactor, box); |
@@ -950,6 +1021,23 @@ class RichText extends Widget { | @@ -950,6 +1021,23 @@ class RichText extends Widget { | ||
950 | 1021 | ||
951 | return pos; | 1022 | return pos; |
952 | } | 1023 | } |
1024 | + | ||
1025 | + @override | ||
1026 | + bool get canSpan => overflow == TextOverflow.span; | ||
1027 | + | ||
1028 | + @override | ||
1029 | + bool get hasMoreWidgets => overflow == TextOverflow.span; | ||
1030 | + | ||
1031 | + @override | ||
1032 | + void restoreContext(_RichTextContext context) { | ||
1033 | + _context.spanStart = context.spanEnd; | ||
1034 | + _context.startOffset = -context.endOffset; | ||
1035 | + } | ||
1036 | + | ||
1037 | + @override | ||
1038 | + WidgetContext saveContext() { | ||
1039 | + return _context; | ||
1040 | + } | ||
953 | } | 1041 | } |
954 | 1042 | ||
955 | class Text extends RichText { | 1043 | class Text extends RichText { |
@@ -962,12 +1050,15 @@ class Text extends RichText { | @@ -962,12 +1050,15 @@ class Text extends RichText { | ||
962 | bool tightBounds = false, | 1050 | bool tightBounds = false, |
963 | double textScaleFactor = 1.0, | 1051 | double textScaleFactor = 1.0, |
964 | int? maxLines, | 1052 | int? maxLines, |
1053 | + TextOverflow overflow = TextOverflow.visible, | ||
965 | }) : super( | 1054 | }) : super( |
966 | - text: TextSpan(text: text, style: style), | ||
967 | - textAlign: textAlign, | ||
968 | - softWrap: softWrap, | ||
969 | - tightBounds: tightBounds, | ||
970 | - textDirection: textDirection, | ||
971 | - textScaleFactor: textScaleFactor, | ||
972 | - maxLines: maxLines); | 1055 | + text: TextSpan(text: text, style: style), |
1056 | + textAlign: textAlign, | ||
1057 | + softWrap: softWrap, | ||
1058 | + tightBounds: tightBounds, | ||
1059 | + textDirection: textDirection, | ||
1060 | + textScaleFactor: textScaleFactor, | ||
1061 | + maxLines: maxLines, | ||
1062 | + overflow: overflow, | ||
1063 | + ); | ||
973 | } | 1064 | } |
@@ -290,6 +290,38 @@ void main() { | @@ -290,6 +290,38 @@ void main() { | ||
290 | ); | 290 | ); |
291 | }); | 291 | }); |
292 | 292 | ||
293 | + test('Text Widgets RichText overflow.span', () { | ||
294 | + final rnd = math.Random(42); | ||
295 | + final para = LoremText(random: rnd).paragraph(100); | ||
296 | + | ||
297 | + pdf.addPage( | ||
298 | + MultiPage( | ||
299 | + pageFormat: const PdfPageFormat(600, 200, marginAll: 10), | ||
300 | + build: (Context context) => [ | ||
301 | + SizedBox(height: 90, width: 20), | ||
302 | + RichText( | ||
303 | + overflow: TextOverflow.span, | ||
304 | + textAlign: TextAlign.justify, | ||
305 | + text: TextSpan( | ||
306 | + text: para, | ||
307 | + children: [ | ||
308 | + const TextSpan(text: ' '), | ||
309 | + const TextSpan( | ||
310 | + text: 'Underline', | ||
311 | + style: TextStyle(decoration: TextDecoration.underline), | ||
312 | + ), | ||
313 | + const TextSpan(text: '. '), | ||
314 | + TextSpan(text: para), | ||
315 | + TextSpan(text: para), | ||
316 | + TextSpan(text: para), | ||
317 | + ], | ||
318 | + ), | ||
319 | + ), | ||
320 | + ], | ||
321 | + ), | ||
322 | + ); | ||
323 | + }); | ||
324 | + | ||
293 | tearDownAll(() async { | 325 | tearDownAll(() async { |
294 | final file = File('widgets-text.pdf'); | 326 | final file = File('widgets-text.pdf'); |
295 | await file.writeAsBytes(await pdf.save()); | 327 | await file.writeAsBytes(await pdf.save()); |
No preview for this file type
-
Please register or login to post a comment