David PHAM-VAN

Implement SpanningWidget on RichText

@@ -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());