David PHAM-VAN

Add RichText Widget

1 # 1.3.3 1 # 1.3.3
2 * Fix dart lint warnings 2 * Fix dart lint warnings
3 * Improve font bounds calculation 3 * Improve font bounds calculation
  4 +* Add RichText Widget
4 5
5 # 1.3.2 6 # 1.3.2
6 * Update Readme 7 * Update Readme
@@ -34,14 +34,18 @@ class TextStyle { @@ -34,14 +34,18 @@ class TextStyle {
34 34
35 final PdfFont font; 35 final PdfFont font;
36 36
  37 + // font height, in pdf unit
37 final double fontSize; 38 final double fontSize;
38 39
39 static const double _defaultFontSize = 12.0 * PdfPageFormat.point; 40 static const double _defaultFontSize = 12.0 * PdfPageFormat.point;
40 41
  42 + // spacing between letters, 1.0 being natural spacing
41 final double letterSpacing; 43 final double letterSpacing;
42 44
  45 + // spacing between lines, in pdf unit
43 final double lineSpacing; 46 final double lineSpacing;
44 47
  48 + // spacing between words, 1.0 being natural spacing
45 final double wordSpacing; 49 final double wordSpacing;
46 50
47 final double height; 51 final double height;
@@ -69,37 +73,96 @@ class TextStyle { @@ -69,37 +73,96 @@ class TextStyle {
69 background: background ?? this.background, 73 background: background ?? this.background,
70 ); 74 );
71 } 75 }
  76 +
  77 + @override
  78 + String toString() =>
  79 + 'TextStyle(color:$color font:$font letterSpacing:$letterSpacing wordSpacing:$wordSpacing lineSpacing:$lineSpacing height:$height background:$background)';
72 } 80 }
73 81
74 enum TextAlign { left, right, center, justify } 82 enum TextAlign { left, right, center, justify }
75 83
76 class _Word { 84 class _Word {
77 - _Word(this.text, this._box); 85 + _Word(this.text, this.style, this.metrics);
78 86
79 final String text; 87 final String text;
80 88
81 - PdfRect _box; 89 + final TextStyle style;
  90 +
  91 + final PdfFontMetrics metrics;
  92 +
  93 + PdfPoint offset = PdfPoint.zero;
82 94
83 @override 95 @override
84 String toString() { 96 String toString() {
85 - return 'Word $text $_box'; 97 + return 'Word "$text" offset:$offset metrics:$metrics style:$style';
  98 + }
  99 +
  100 + void debugPaint(Context context, double textScaleFactor, PdfRect globalBox) {
  101 + const double deb = 5.0;
  102 +
  103 + context.canvas
  104 + ..drawRect(globalBox.x + offset.x + metrics.left,
  105 + globalBox.top + offset.y + metrics.top, metrics.width, metrics.height)
  106 + ..setStrokeColor(PdfColor.orange)
  107 + ..strokePath()
  108 + ..drawLine(
  109 + globalBox.x + offset.x - deb,
  110 + globalBox.top + offset.y,
  111 + globalBox.x + offset.x + metrics.right + deb,
  112 + globalBox.top + offset.y)
  113 + ..setStrokeColor(PdfColor.deepPurple)
  114 + ..strokePath();
86 } 115 }
87 } 116 }
88 117
89 -class Text extends Widget {  
90 - Text(  
91 - this.data, {  
92 - this.style,  
93 - this.textAlign = TextAlign.left,  
94 - bool softWrap = true,  
95 - this.textScaleFactor = 1.0,  
96 - int maxLines,  
97 - }) : maxLines = !softWrap ? 1 : maxLines,  
98 - assert(data != null); 118 +class TextSpan {
  119 + const TextSpan({this.style, this.text, this.children});
  120 +
  121 + final TextStyle style;
  122 +
  123 + final String text;
  124 +
  125 + final List<TextSpan> children;
  126 +
  127 + String toPlainText() {
  128 + final StringBuffer buffer = StringBuffer();
  129 + visitTextSpan((TextSpan span) {
  130 + buffer.write(span.text);
  131 + return true;
  132 + });
  133 + return buffer.toString();
  134 + }
  135 +
  136 + bool visitTextSpan(bool visitor(TextSpan span)) {
  137 + if (text != null) {
  138 + if (!visitor(this)) {
  139 + return false;
  140 + }
  141 + }
  142 + if (children != null) {
  143 + for (TextSpan child in children) {
  144 + if (!child.visitTextSpan(visitor)) {
  145 + return false;
  146 + }
  147 + }
  148 + }
  149 + return true;
  150 + }
  151 +}
99 152
100 - final String data; 153 +class RichText extends Widget {
  154 + RichText(
  155 + {@required this.text,
  156 + this.textAlign = TextAlign.left,
  157 + bool softWrap = true,
  158 + this.textScaleFactor = 1.0,
  159 + int maxLines})
  160 + : maxLines = !softWrap ? 1 : maxLines,
  161 + assert(text != null);
101 162
102 - TextStyle style; 163 + static const bool debug = false;
  164 +
  165 + final TextSpan text;
103 166
104 final TextAlign textAlign; 167 final TextAlign textAlign;
105 168
@@ -109,12 +172,13 @@ class Text extends Widget { @@ -109,12 +172,13 @@ class Text extends Widget {
109 172
110 final List<_Word> _words = <_Word>[]; 173 final List<_Word> _words = <_Word>[];
111 174
112 - double _realignLine(  
113 - List<_Word> words, double totalWidth, double wordsWidth, bool last) { 175 + double _realignLine(List<_Word> words, double totalWidth, double wordsWidth,
  176 + bool last, double baseline) {
114 double delta = 0.0; 177 double delta = 0.0;
115 switch (textAlign) { 178 switch (textAlign) {
116 case TextAlign.left: 179 case TextAlign.left:
117 - return wordsWidth; 180 + totalWidth = wordsWidth;
  181 + break;
118 case TextAlign.right: 182 case TextAlign.right:
119 delta = totalWidth - wordsWidth; 183 delta = totalWidth - wordsWidth;
120 break; 184 break;
@@ -123,20 +187,20 @@ class Text extends Widget { @@ -123,20 +187,20 @@ class Text extends Widget {
123 break; 187 break;
124 case TextAlign.justify: 188 case TextAlign.justify:
125 if (last) { 189 if (last) {
126 - return wordsWidth; 190 + totalWidth = wordsWidth;
  191 + break;
127 } 192 }
128 delta = (totalWidth - wordsWidth) / (words.length - 1); 193 delta = (totalWidth - wordsWidth) / (words.length - 1);
129 double x = 0.0; 194 double x = 0.0;
130 for (_Word word in words) { 195 for (_Word word in words) {
131 - word._box = PdfRect(  
132 - word._box.x + x, word._box.y, word._box.width, word._box.height); 196 + word.offset = word.offset.translate(x, -baseline);
133 x += delta; 197 x += delta;
134 } 198 }
135 return totalWidth; 199 return totalWidth;
136 } 200 }
  201 +
137 for (_Word word in words) { 202 for (_Word word in words) {
138 - word._box = PdfRect(  
139 - word._box.x + delta, word._box.y, word._box.width, word._box.height); 203 + word.offset = word.offset.translate(delta, -baseline);
140 } 204 }
141 return totalWidth; 205 return totalWidth;
142 } 206 }
@@ -146,74 +210,89 @@ class Text extends Widget { @@ -146,74 +210,89 @@ class Text extends Widget {
146 {bool parentUsesSize = false}) { 210 {bool parentUsesSize = false}) {
147 _words.clear(); 211 _words.clear();
148 212
149 - style ??= Theme.of(context).defaultTextStyle; 213 + final TextStyle defaultstyle = Theme.of(context).defaultTextStyle;
150 214
151 - final double cw = constraints.hasBoundedWidth 215 + final double constraintWidth = constraints.hasBoundedWidth
152 ? constraints.maxWidth 216 ? constraints.maxWidth
153 : constraints.constrainWidth(); 217 : constraints.constrainWidth();
154 - final double ch = constraints.hasBoundedHeight 218 + final double constraintHeight = constraints.hasBoundedHeight
155 ? constraints.maxHeight 219 ? constraints.maxHeight
156 : constraints.constrainHeight(); 220 : constraints.constrainHeight();
157 221
158 - double x = 0.0;  
159 - double y = 0.0;  
160 - double w = 0.0;  
161 - double h = 0.0;  
162 - double lh = 0.0;  
163 -  
164 - final PdfRect space =  
165 - style.font.stringBounds(' ') * (style.fontSize * textScaleFactor); 222 + double offsetX = 0.0;
  223 + double offsetY = 0.0;
  224 + double width = 0.0;
  225 + double top;
  226 + double bottom;
166 227
167 int lines = 1; 228 int lines = 1;
168 int wCount = 0; 229 int wCount = 0;
169 int lineStart = 0; 230 int lineStart = 0;
170 231
171 - for (String word in data.split(' ')) {  
172 - final PdfRect box =  
173 - style.font.stringBounds(word) * (style.fontSize * textScaleFactor); 232 + text.visitTextSpan((TextSpan span) {
  233 + if (span.text == null) {
  234 + return true;
  235 + }
174 236
175 - final double ww = box.width;  
176 - final double wh = box.height; 237 + final TextStyle style = span.style ?? defaultstyle;
177 238
178 - if (x + ww > cw) {  
179 - if (wCount == 0) {  
180 - break;  
181 - }  
182 - w = math.max(  
183 - w,  
184 - _realignLine(  
185 - _words.sublist(lineStart), cw, x - space.width, false));  
186 - lineStart += wCount;  
187 - if (maxLines != null && ++lines > maxLines) {  
188 - break; 239 + final PdfFontMetrics space =
  240 + style.font.stringMetrics(' ') * (style.fontSize * textScaleFactor);
  241 +
  242 + for (String word in span.text.split(' ')) {
  243 + if (word.isEmpty) {
  244 + offsetX += space.width;
  245 + continue;
189 } 246 }
190 247
191 - x = 0.0;  
192 - y += lh + style.lineSpacing;  
193 - h += lh + style.lineSpacing;  
194 - lh = 0.0;  
195 - if (y > ch) {  
196 - break; 248 + final PdfFontMetrics metrics =
  249 + style.font.stringMetrics(word) * (style.fontSize * textScaleFactor);
  250 +
  251 + if (offsetX + metrics.width > constraintWidth) {
  252 + if (wCount == 0) {
  253 + break;
  254 + }
  255 + width = math.max(
  256 + width,
  257 + _realignLine(_words.sublist(lineStart), constraintWidth,
  258 + offsetX - space.width, false, bottom));
  259 + lineStart += wCount;
  260 + if (maxLines != null && ++lines > maxLines) {
  261 + break;
  262 + }
  263 +
  264 + offsetX = 0.0;
  265 + offsetY += bottom - top + style.lineSpacing;
  266 + top = null;
  267 + bottom = null;
  268 +
  269 + if (offsetY > constraintHeight) {
  270 + return false;
  271 + }
  272 + wCount = 0;
197 } 273 }
198 - wCount = 0;  
199 - }  
200 274
201 - final double wx = x;  
202 - final double wy = y; 275 + top = math.min(top ?? metrics.top, metrics.top);
  276 + bottom = math.max(bottom ?? metrics.bottom, metrics.bottom);
203 277
204 - x += ww + space.width;  
205 - lh = math.max(lh, wh); 278 + final _Word wd = _Word(word, style, metrics);
  279 + wd.offset = PdfPoint(offsetX, -offsetY);
206 280
207 - final _Word wd =  
208 - _Word(word, PdfRect(box.x + wx, box.y + wy + wh, ww, wh));  
209 - _words.add(wd);  
210 - wCount++;  
211 - }  
212 - w = math.max(  
213 - w, _realignLine(_words.sublist(lineStart), cw, x - space.width, true));  
214 - h += lh;  
215 - box = PdfRect(0.0, 0.0, constraints.constrainWidth(w),  
216 - constraints.constrainHeight(h)); 281 + _words.add(wd);
  282 + wCount++;
  283 + offsetX += metrics.width + space.advanceWidth;
  284 + }
  285 +
  286 + offsetX -= space.width;
  287 + return true;
  288 + });
  289 +
  290 + width = math.max(
  291 + width,
  292 + _realignLine(
  293 + _words.sublist(lineStart), constraintWidth, offsetX, true, bottom));
  294 + box = PdfRect(0.0, 0.0, constraints.constrainWidth(width),
  295 + constraints.constrainHeight(offsetY + bottom - top));
217 } 296 }
218 297
219 @override 298 @override
@@ -227,11 +306,48 @@ class Text extends Widget { @@ -227,11 +306,48 @@ class Text extends Widget {
227 @override 306 @override
228 void paint(Context context) { 307 void paint(Context context) {
229 super.paint(context); 308 super.paint(context);
230 - context.canvas.setFillColor(style.color); 309 + TextStyle currentStyle;
  310 + PdfColor currentColor;
231 311
232 for (_Word word in _words) { 312 for (_Word word in _words) {
233 - context.canvas.drawString(style.font, style.fontSize * textScaleFactor,  
234 - word.text, box.x + word._box.x, box.y + box.height - word._box.y); 313 + assert(() {
  314 + if (Document.debug && RichText.debug) {
  315 + word.debugPaint(context, textScaleFactor, box);
  316 + }
  317 + return true;
  318 + }());
  319 +
  320 + if (word.style != currentStyle) {
  321 + currentStyle = word.style;
  322 + if (currentStyle.color != currentColor) {
  323 + currentColor = currentStyle.color;
  324 + context.canvas.setFillColor(currentColor);
  325 + }
  326 + }
  327 +
  328 + context.canvas.drawString(
  329 + currentStyle.font,
  330 + currentStyle.fontSize * textScaleFactor,
  331 + word.text,
  332 + box.x + word.offset.x,
  333 + box.top + word.offset.y);
235 } 334 }
236 } 335 }
237 } 336 }
  337 +
  338 +class Text extends RichText {
  339 + Text(
  340 + String text, {
  341 + TextStyle style,
  342 + TextAlign textAlign = TextAlign.left,
  343 + bool softWrap = true,
  344 + double textScaleFactor = 1.0,
  345 + int maxLines,
  346 + }) : assert(text != null),
  347 + super(
  348 + text: TextSpan(text: text, style: style),
  349 + textAlign: textAlign,
  350 + softWrap: softWrap,
  351 + textScaleFactor: textScaleFactor,
  352 + maxLines: maxLines);
  353 +}
@@ -16,6 +16,7 @@ @@ -16,6 +16,7 @@
16 16
17 import 'dart:convert'; 17 import 'dart:convert';
18 import 'dart:io'; 18 import 'dart:io';
  19 +import 'dart:typed_data';
19 20
20 import 'package:pdf/pdf.dart'; 21 import 'package:pdf/pdf.dart';
21 import 'package:pdf/widgets.dart'; 22 import 'package:pdf/widgets.dart';
@@ -97,6 +98,10 @@ void main() { @@ -97,6 +98,10 @@ void main() {
97 children: List<Widget>.generate( 98 children: List<Widget>.generate(
98 9, (int n) => FittedBox(child: Text('${n + 1}'))))))); 99 9, (int n) => FittedBox(child: Text('${n + 1}')))))));
99 100
  101 + final Uint8List robotoData = File('open-sans.ttf').readAsBytesSync();
  102 + final PdfTtfFont roboto =
  103 + PdfTtfFont(pdf.document, robotoData.buffer.asByteData());
  104 +
100 pdf.addPage(MultiPage( 105 pdf.addPage(MultiPage(
101 pageFormat: const PdfPageFormat(400.0, 200.0), 106 pageFormat: const PdfPageFormat(400.0, 200.0),
102 margin: const EdgeInsets.all(10.0), 107 margin: const EdgeInsets.all(10.0),
@@ -141,6 +146,22 @@ void main() { @@ -141,6 +146,22 @@ void main() {
141 ..drawRRect(0, 0, size.x, size.y, 10, 10) 146 ..drawRRect(0, 0, size.x, size.y, 10, 10)
142 ..fillPath(); 147 ..fillPath();
143 }), 148 }),
  149 + RichText(
  150 + text: TextSpan(
  151 + text: 'Hello ',
  152 + style: Theme.of(context).defaultTextStyle,
  153 + children: <TextSpan>[
  154 + TextSpan(
  155 + text: 'bold',
  156 + style: Theme.of(context)
  157 + .defaultTextStyleBold
  158 + .copyWith(fontSize: 20, color: PdfColor.blue)),
  159 + const TextSpan(
  160 + text: ' world!',
  161 + ),
  162 + ],
  163 + ),
  164 + )
144 ])); 165 ]));
145 166
146 final File file = File('widgets.pdf'); 167 final File file = File('widgets.pdf');