Showing
4 changed files
with
147 additions
and
23 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,6 +1050,7 @@ class Text extends RichText { | @@ -962,6 +1050,7 @@ 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), | 1055 | text: TextSpan(text: text, style: style), |
| 967 | textAlign: textAlign, | 1056 | textAlign: textAlign, |
| @@ -969,5 +1058,7 @@ class Text extends RichText { | @@ -969,5 +1058,7 @@ class Text extends RichText { | ||
| 969 | tightBounds: tightBounds, | 1058 | tightBounds: tightBounds, |
| 970 | textDirection: textDirection, | 1059 | textDirection: textDirection, |
| 971 | textScaleFactor: textScaleFactor, | 1060 | textScaleFactor: textScaleFactor, |
| 972 | - maxLines: maxLines); | 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