David PHAM-VAN

Fix Text layout with softwrap

@@ -6,6 +6,7 @@ @@ -6,6 +6,7 @@
6 - Add PdfPage.rotate attribute 6 - Add PdfPage.rotate attribute
7 - Add RadialGrid for charts with polar coordinates 7 - Add RadialGrid for charts with polar coordinates
8 - Add PieChart 8 - Add PieChart
  9 +- Fix Text layout with softwrap
9 10
10 ## 3.0.1 11 ## 3.0.1
11 12
@@ -461,92 +461,114 @@ class TextSpan extends InlineSpan { @@ -461,92 +461,114 @@ class TextSpan extends InlineSpan {
461 } 461 }
462 } 462 }
463 463
464 -class RichText extends Widget {  
465 - RichText(  
466 - {required this.text,  
467 - TextAlign? textAlign,  
468 - this.textDirection,  
469 - bool? softWrap,  
470 - this.tightBounds = false,  
471 - this.textScaleFactor = 1.0,  
472 - int? maxLines})  
473 - : _textAlign = textAlign,  
474 - _softWrap = softWrap,  
475 - _maxLines = maxLines;  
476 -  
477 - static bool debug = false;  
478 -  
479 - final InlineSpan text; 464 +class _Line {
  465 + const _Line(
  466 + this.parent,
  467 + this.firstSpan,
  468 + this.countSpan,
  469 + this.baseline,
  470 + this.wordsWidth,
  471 + this.textDirection,
  472 + );
480 473
481 - TextAlign get textAlign => _textAlign!;  
482 - TextAlign? _textAlign; 474 + final RichText parent;
483 475
484 - final TextDirection? textDirection; 476 + final int firstSpan;
  477 + final int countSpan;
485 478
486 - final double textScaleFactor; 479 + TextAlign get textAlign => parent.textAlign;
487 480
488 - bool? get softWrap => _softWrap;  
489 - bool? _softWrap; 481 + final double baseline;
490 482
491 - final bool tightBounds; 483 + final double wordsWidth;
492 484
493 - int? get maxLines => _maxLines;  
494 - int? _maxLines; 485 + final TextDirection textDirection;
495 486
496 - final List<_Span> _spans = <_Span>[]; 487 + @override
  488 + String toString() =>
  489 + '$runtimeType $firstSpan-${firstSpan + countSpan} baseline: $baseline width:$wordsWidth';
497 490
498 - final List<_TextDecoration> _decorations = <_TextDecoration>[]; 491 + void realign(double totalWidth, bool isLast) {
  492 + final spans = parent._spans.sublist(firstSpan, firstSpan + countSpan);
499 493
500 - double? _realignLine(  
501 - List<_Span> spans,  
502 - List<_TextDecoration> decorations,  
503 - double? totalWidth,  
504 - double wordsWidth,  
505 - bool last,  
506 - double? baseline,  
507 - TextDirection textDirection,  
508 - ) {  
509 var delta = 0.0; 494 var delta = 0.0;
510 switch (textAlign) { 495 switch (textAlign) {
511 case TextAlign.left: 496 case TextAlign.left:
512 break; 497 break;
513 case TextAlign.right: 498 case TextAlign.right:
514 - delta = totalWidth! - wordsWidth; 499 + delta = totalWidth - wordsWidth;
515 break; 500 break;
516 case TextAlign.center: 501 case TextAlign.center:
517 - delta = (totalWidth! - wordsWidth) / 2.0; 502 + delta = (totalWidth - wordsWidth) / 2.0;
518 break; 503 break;
519 case TextAlign.justify: 504 case TextAlign.justify:
520 - if (last) { 505 + if (isLast) {
521 totalWidth = wordsWidth; 506 totalWidth = wordsWidth;
522 break; 507 break;
523 } 508 }
524 - delta = (totalWidth! - wordsWidth) / (spans.length - 1); 509 + delta = (totalWidth - wordsWidth) / (spans.length - 1);
525 var x = 0.0; 510 var x = 0.0;
526 for (var span in spans) { 511 for (var span in spans) {
527 - span.offset = span.offset.translate(x, -baseline!); 512 + span.offset = span.offset.translate(x, -baseline);
528 x += delta; 513 x += delta;
529 } 514 }
530 - return totalWidth; 515 + return;
531 } 516 }
532 517
533 if (textDirection == TextDirection.rtl) { 518 if (textDirection == TextDirection.rtl) {
534 for (var span in spans) { 519 for (var span in spans) {
535 span.offset = PdfPoint( 520 span.offset = PdfPoint(
536 - totalWidth! - (span.offset.x + span.width!) - delta,  
537 - span.offset.y - baseline!, 521 + totalWidth - (span.offset.x + span.width!) - delta,
  522 + span.offset.y - baseline,
538 ); 523 );
539 } 524 }
540 525
541 - return totalWidth; 526 + return;
542 } 527 }
543 528
544 for (var span in spans) { 529 for (var span in spans) {
545 - span.offset = span.offset.translate(delta, -baseline!); 530 + span.offset = span.offset.translate(delta, -baseline);
546 } 531 }
547 532
548 - return totalWidth; 533 + return;
549 } 534 }
  535 +}
  536 +
  537 +class RichText extends Widget {
  538 + RichText({
  539 + required this.text,
  540 + TextAlign? textAlign,
  541 + this.textDirection,
  542 + bool? softWrap,
  543 + this.tightBounds = false,
  544 + this.textScaleFactor = 1.0,
  545 + int? maxLines,
  546 + }) : _textAlign = textAlign,
  547 + _softWrap = softWrap,
  548 + _maxLines = maxLines;
  549 +
  550 + static bool debug = false;
  551 +
  552 + final InlineSpan text;
  553 +
  554 + TextAlign get textAlign => _textAlign!;
  555 + TextAlign? _textAlign;
  556 +
  557 + final TextDirection? textDirection;
  558 +
  559 + final double textScaleFactor;
  560 +
  561 + bool get softWrap => _softWrap!;
  562 + bool? _softWrap;
  563 +
  564 + final bool tightBounds;
  565 +
  566 + int? get maxLines => _maxLines;
  567 + int? _maxLines;
  568 +
  569 + final List<_Span> _spans = <_Span>[];
  570 +
  571 + final List<_TextDecoration> _decorations = <_TextDecoration>[];
550 572
551 void _appendDecoration(bool append, _TextDecoration td) { 573 void _appendDecoration(bool append, _TextDecoration td) {
552 if (append && _decorations.isNotEmpty) { 574 if (append && _decorations.isNotEmpty) {
@@ -583,14 +605,14 @@ class RichText extends Widget { @@ -583,14 +605,14 @@ class RichText extends Widget {
583 605
584 var offsetX = 0.0; 606 var offsetX = 0.0;
585 var offsetY = 0.0; 607 var offsetY = 0.0;
586 - var width = 0.0; 608 +
587 double? top; 609 double? top;
588 double? bottom; 610 double? bottom;
589 611
590 - var lines = 1; 612 + final lines = <_Line>[];
591 var spanCount = 0; 613 var spanCount = 0;
592 var spanStart = 0; 614 var spanStart = 0;
593 - var decorationStart = 0; 615 + var overflow = false;
594 616
595 text.visitChildren(( 617 text.visitChildren((
596 InlineSpan span, 618 InlineSpan span,
@@ -626,25 +648,22 @@ class RichText extends Widget { @@ -626,25 +648,22 @@ class RichText extends Widget {
626 (style.fontSize! * textScaleFactor); 648 (style.fontSize! * textScaleFactor);
627 649
628 if (offsetX + metrics.width > constraintWidth && spanCount > 0) { 650 if (offsetX + metrics.width > constraintWidth && spanCount > 0) {
629 - width = math.max(  
630 - width,  
631 - _realignLine(  
632 - _spans.sublist(spanStart),  
633 - _decorations.sublist(decorationStart),  
634 - constraintWidth,  
635 - offsetX -  
636 - space.advanceWidth * style.wordSpacing! -  
637 - style.letterSpacing!,  
638 - false,  
639 - bottom,  
640 - _textDirection,  
641 - )!); 651 + overflow = true;
  652 + lines.add(_Line(
  653 + this,
  654 + spanStart,
  655 + spanCount,
  656 + bottom ?? 0,
  657 + offsetX -
  658 + space.advanceWidth * style.wordSpacing! -
  659 + style.letterSpacing!,
  660 + _textDirection,
  661 + ));
642 662
643 spanStart += spanCount; 663 spanStart += spanCount;
644 - decorationStart = _decorations.length; 664 + spanCount = 0;
645 665
646 - lines++;  
647 - if (maxLines != null && lines > maxLines!) { 666 + if (maxLines != null && lines.length >= maxLines!) {
648 return false; 667 return false;
649 } 668 }
650 669
@@ -656,7 +675,6 @@ class RichText extends Widget { @@ -656,7 +675,6 @@ class RichText extends Widget {
656 if (offsetY > constraintHeight) { 675 if (offsetY > constraintHeight) {
657 return false; 676 return false;
658 } 677 }
659 - spanCount = 0;  
660 } 678 }
661 679
662 final baseline = span.baseline! * textScaleFactor; 680 final baseline = span.baseline! * textScaleFactor;
@@ -689,26 +707,21 @@ class RichText extends Widget { @@ -689,26 +707,21 @@ class RichText extends Widget {
689 style.letterSpacing!; 707 style.letterSpacing!;
690 } 708 }
691 709
692 - if (softWrap! && line < spanLines.length - 1) {  
693 - width = math.max(  
694 - width,  
695 - _realignLine(  
696 - _spans.sublist(spanStart),  
697 - _decorations.sublist(decorationStart),  
698 - constraintWidth,  
699 - offsetX -  
700 - space.advanceWidth * style.wordSpacing! -  
701 - style.letterSpacing!,  
702 - true,  
703 - bottom,  
704 - _textDirection,  
705 - )!); 710 + if (softWrap && line < spanLines.length - 1) {
  711 + lines.add(_Line(
  712 + this,
  713 + spanStart,
  714 + spanCount,
  715 + bottom ?? 0,
  716 + offsetX -
  717 + space.advanceWidth * style.wordSpacing! -
  718 + style.letterSpacing!,
  719 + _textDirection));
706 720
707 spanStart += spanCount; 721 spanStart += spanCount;
708 - decorationStart = _decorations.length;  
709 722
710 - lines++;  
711 - if (maxLines != null && lines > maxLines!) { 723 + if (maxLines != null && lines.length >= maxLines!) {
  724 + spanCount = 0;
712 return false; 725 return false;
713 } 726 }
714 727
@@ -720,11 +733,11 @@ class RichText extends Widget { @@ -720,11 +733,11 @@ class RichText extends Widget {
720 } 733 }
721 top = null; 734 top = null;
722 bottom = null; 735 bottom = null;
  736 + spanCount = 0;
723 737
724 if (offsetY > constraintHeight) { 738 if (offsetY > constraintHeight) {
725 return false; 739 return false;
726 } 740 }
727 - spanCount = 0;  
728 } 741 }
729 } 742 }
730 743
@@ -743,23 +756,20 @@ class RichText extends Widget { @@ -743,23 +756,20 @@ class RichText extends Widget {
743 ); 756 );
744 757
745 if (offsetX + ws.width! > constraintWidth && spanCount > 0) { 758 if (offsetX + ws.width! > constraintWidth && spanCount > 0) {
746 - width = math.max(  
747 - width,  
748 - _realignLine(  
749 - _spans.sublist(spanStart),  
750 - _decorations.sublist(decorationStart),  
751 - constraintWidth,  
752 - offsetX,  
753 - false,  
754 - bottom,  
755 - _textDirection,  
756 - )!); 759 + overflow = true;
  760 + lines.add(_Line(
  761 + this,
  762 + spanStart,
  763 + spanCount,
  764 + bottom ?? 0,
  765 + offsetX,
  766 + _textDirection,
  767 + ));
757 768
758 spanStart += spanCount; 769 spanStart += spanCount;
759 - decorationStart = _decorations.length; 770 + spanCount = 0;
760 771
761 - lines++;  
762 - if (maxLines != null && lines > maxLines!) { 772 + if (maxLines != null && lines.length > maxLines!) {
763 return false; 773 return false;
764 } 774 }
765 775
@@ -771,7 +781,6 @@ class RichText extends Widget { @@ -771,7 +781,6 @@ class RichText extends Widget {
771 if (offsetY > constraintHeight) { 781 if (offsetY > constraintHeight) {
772 return false; 782 return false;
773 } 783 }
774 - spanCount = 0;  
775 } 784 }
776 785
777 final baseline = span.baseline! * textScaleFactor; 786 final baseline = span.baseline! * textScaleFactor;
@@ -801,23 +810,37 @@ class RichText extends Widget { @@ -801,23 +810,37 @@ class RichText extends Widget {
801 return true; 810 return true;
802 }, defaultstyle, null); 811 }, defaultstyle, null);
803 812
804 - width = math.max(  
805 - width,  
806 - _realignLine(  
807 - _spans.sublist(spanStart),  
808 - _decorations.sublist(decorationStart),  
809 - lines > 1 ? constraintWidth : offsetX,  
810 - offsetX,  
811 - true,  
812 - bottom,  
813 - _textDirection,  
814 - )!);  
815 -  
816 - bottom ??= 0.0;  
817 - top ??= 0.0; 813 + if (spanCount > 0) {
  814 + lines.add(_Line(
  815 + this,
  816 + spanStart,
  817 + spanCount,
  818 + bottom ?? 0,
  819 + offsetX,
  820 + _textDirection,
  821 + ));
  822 + }
  823 +
  824 + assert(!overflow || constraintWidth.isFinite);
  825 + var width = overflow ? constraintWidth : constraints.minWidth;
  826 +
  827 + if (lines.isNotEmpty) {
  828 + if (!overflow) {
  829 + // Calculate the final width
  830 + for (final line in lines) {
  831 + width = math.max(width, line.wordsWidth);
  832 + }
  833 + }
  834 +
  835 + // Realign all the lines
  836 + for (final line in lines.sublist(0, lines.length - 1)) {
  837 + line.realign(width, false);
  838 + }
  839 + lines.last.realign(width, true);
  840 + }
818 841
819 box = PdfRect(0, 0, constraints.constrainWidth(width), 842 box = PdfRect(0, 0, constraints.constrainWidth(width),
820 - constraints.constrainHeight(offsetY + bottom! - top!)); 843 + constraints.constrainHeight(offsetY + (bottom ?? 0) - (top ?? 0)));
821 } 844 }
822 845
823 @override 846 @override