@@ -111,12 +111,14 @@ class TextLayoutService {
111
111
// TODO(mdebbar):
112
112
// (1) adjust the current line's height to fit the placeholder.
113
113
// (2) update accumulated line width.
114
+ // (3) add placeholder box to line.
114
115
} else {
115
116
// The placeholder can't fit on the current line.
116
117
// TODO(mdebbar):
117
118
// (1) create a line.
118
119
// (2) adjust the new line's height to fit the placeholder.
119
120
// (3) update `lineStart`, etc.
121
+ // (4) add placeholder box to line.
120
122
}
121
123
} else if (span is FlatTextSpan ) {
122
124
spanometer.currentSpan = span;
@@ -171,6 +173,7 @@ class TextLayoutService {
171
173
172
174
// Only go to the next span if we've reached the end of this span.
173
175
if (currentLine.end.index >= span.end && spanIndex < spanCount - 1 ) {
176
+ currentLine.createBox ();
174
177
span = paragraph.spans[++ spanIndex];
175
178
}
176
179
}
@@ -229,6 +232,121 @@ class TextLayoutService {
229
232
}
230
233
}
231
234
}
235
+
236
+ List <ui.TextBox > getBoxesForRange (
237
+ int start,
238
+ int end,
239
+ ui.BoxHeightStyle boxHeightStyle,
240
+ ui.BoxWidthStyle boxWidthStyle,
241
+ ) {
242
+ // Zero-length ranges and invalid ranges return an empty list.
243
+ if (start >= end || start < 0 || end < 0 ) {
244
+ return < ui.TextBox > [];
245
+ }
246
+
247
+ final int length = paragraph.toPlainText ().length;
248
+ // Ranges that are out of bounds should return an empty list.
249
+ if (start > length || end > length) {
250
+ return < ui.TextBox > [];
251
+ }
252
+
253
+ final List <ui.TextBox > boxes = < ui.TextBox > [];
254
+
255
+ for (final EngineLineMetrics line in lines) {
256
+ if (line.overlapsWith (start, end)) {
257
+ for (final RangeBox box in line.boxes! ) {
258
+ if (box.overlapsWith (start, end)) {
259
+ boxes.add (box.intersect (line, start, end));
260
+ }
261
+ }
262
+ }
263
+ }
264
+ return boxes;
265
+ }
266
+ }
267
+
268
+ /// Represents a box inside [span] with the range of [start] to [end] .
269
+ ///
270
+ /// The box's coordinates are all relative to the line it belongs to. For
271
+ /// example, [left] is the distance from the left edge of the line to the left
272
+ /// edge of the box.
273
+ class RangeBox {
274
+ RangeBox .fromSpanometer (
275
+ this .spanometer, {
276
+ required this .start,
277
+ required this .end,
278
+ required this .left,
279
+ }) : span = spanometer.currentSpan,
280
+ height = spanometer.height,
281
+ baseline = spanometer.alphabeticBaseline,
282
+ width = spanometer.measureIncludingSpace (start, end);
283
+
284
+ final Spanometer spanometer;
285
+ final ParagraphSpan span;
286
+ final LineBreakResult start;
287
+ final LineBreakResult end;
288
+
289
+ /// The distance from the left edge of the line to the left edge of the box.
290
+ final double left;
291
+
292
+ /// The distance from the left edge to the right edge of the box.
293
+ final double width;
294
+
295
+ /// The distance from the top edge to the bottom edge of the box.
296
+ final double height;
297
+
298
+ /// The distance from the top edge of the box to the alphabetic baseline of
299
+ /// the box.
300
+ final double baseline;
301
+
302
+ /// The direction in which text inside this box flows.
303
+ ui.TextDirection get direction =>
304
+ spanometer.paragraph.paragraphStyle._effectiveTextDirection;
305
+
306
+ /// The distance from the left edge of the line to the right edge of the box.
307
+ double get right => left + width;
308
+
309
+ /// Whether this box's range overlaps with the range from [startIndex] to
310
+ /// [endIndex] .
311
+ bool overlapsWith (int startIndex, int endIndex) {
312
+ return startIndex < this .end.index && this .start.index < endIndex;
313
+ }
314
+
315
+ /// Performs the intersection of this box with the range given by [start] and
316
+ /// [end] indices, and returns a [ui.TextBox] representing that intersection.
317
+ ///
318
+ /// The coordinates of the resulting [ui.TextBox] are relative to the
319
+ /// paragraph, not to the line.
320
+ ui.TextBox intersect (EngineLineMetrics line, int start, int end) {
321
+ final double top = line.baseline - baseline;
322
+ final double left, right;
323
+
324
+ if (start <= this .start.index) {
325
+ left = this .left;
326
+ } else {
327
+ spanometer.currentSpan = span as FlatTextSpan ;
328
+ left = this .left + spanometer._measure (this .start.index, start);
329
+ }
330
+
331
+ if (end >= this .end.indexWithoutTrailingNewlines) {
332
+ right = this .right;
333
+ } else {
334
+ spanometer.currentSpan = span as FlatTextSpan ;
335
+ right = this .right -
336
+ spanometer._measure (end, this .end.indexWithoutTrailingNewlines);
337
+ }
338
+
339
+ // The [RangeBox]'s left and right edges are relative to the line. In order
340
+ // to make them relative to the paragraph, we need to add the left edge of
341
+ // the line.
342
+ return ui.TextBox .fromLTRBD (
343
+ left + line.left,
344
+ top,
345
+ right + line.left,
346
+ top + height,
347
+ direction,
348
+ );
349
+ }
232
350
}
233
351
234
352
/// Represents a segment in a line of a paragraph.
@@ -310,6 +428,7 @@ class LineBuilder {
310
428
}
311
429
312
430
final List <LineSegment > _segments = < LineSegment > [];
431
+ final List <RangeBox > _boxes = < RangeBox > [];
313
432
314
433
final double maxWidth;
315
434
final CanvasParagraph paragraph;
@@ -398,7 +517,7 @@ class LineBuilder {
398
517
// The segment starts at the end of the line.
399
518
final LineBreakResult segmentStart = end;
400
519
return LineSegment (
401
- span: spanometer.currentSpan! ,
520
+ span: spanometer.currentSpan,
402
521
start: segmentStart,
403
522
end: segmentEnd,
404
523
width: spanometer.measure (segmentStart, segmentEnd),
@@ -542,8 +661,53 @@ class LineBuilder {
542
661
LineBreakResult .sameIndex (breakingPoint, LineBreakType .prohibited));
543
662
}
544
663
664
+ LineBreakResult get _boxStart {
665
+ if (_boxes.isEmpty) {
666
+ return start;
667
+ }
668
+ // The end of the last box is the start of the new box.
669
+ return _boxes.last.end;
670
+ }
671
+
672
+ double get _boxLeft {
673
+ if (_boxes.isEmpty) {
674
+ return 0.0 ;
675
+ }
676
+ return _boxes.last.right;
677
+ }
678
+
679
+ ui.TextDirection get direction =>
680
+ paragraph.paragraphStyle._effectiveTextDirection;
681
+
682
+ /// Cuts a new box in the line.
683
+ ///
684
+ /// If this is the first box in the line, it'll start at the beginning of the
685
+ /// line. Else, it'll start at the end of the last box.
686
+ ///
687
+ /// A box should be cut whenever the end of line is reached, or when switching
688
+ /// from one span to another.
689
+ void createBox () {
690
+ final LineBreakResult boxStart = _boxStart;
691
+ final LineBreakResult boxEnd = end;
692
+ // Avoid creating empty boxes. This could happen when the end of a span
693
+ // coincides with the end of a line. In this case, `createBox` is called twice.
694
+ if (boxStart == boxEnd) {
695
+ return ;
696
+ }
697
+
698
+ _boxes.add (RangeBox .fromSpanometer (
699
+ spanometer,
700
+ start: boxStart,
701
+ end: boxEnd,
702
+ left: _boxLeft,
703
+ ));
704
+ }
705
+
545
706
/// Builds the [EngineLineMetrics] instance that represents this line.
546
707
EngineLineMetrics build ({String ? ellipsis}) {
708
+ // At the end of each line, we cut the last box of the line.
709
+ createBox ();
710
+
547
711
final double ellipsisWidth =
548
712
ellipsis == null ? 0.0 : spanometer.measureText (ellipsis);
549
713
@@ -559,6 +723,7 @@ class LineBuilder {
559
723
left: alignOffset,
560
724
height: height,
561
725
baseline: accumulatedHeight + alphabeticBaseline,
726
+ boxes: _boxes,
562
727
);
563
728
}
564
729
@@ -601,12 +766,12 @@ class Spanometer {
601
766
602
767
String _cssFontString = '' ;
603
768
604
- double ? get letterSpacing => _currentSpan ! .style._letterSpacing;
769
+ double ? get letterSpacing => currentSpan .style._letterSpacing;
605
770
606
771
TextHeightRuler ? _currentRuler;
607
772
FlatTextSpan ? _currentSpan;
608
773
609
- FlatTextSpan ? get currentSpan => _currentSpan;
774
+ FlatTextSpan get currentSpan => _currentSpan! ;
610
775
set currentSpan (FlatTextSpan ? span) {
611
776
if (span == _currentSpan) {
612
777
return ;
@@ -681,7 +846,7 @@ class Spanometer {
681
846
}) {
682
847
assert (_currentSpan != null );
683
848
684
- final FlatTextSpan span = _currentSpan ! ;
849
+ final FlatTextSpan span = currentSpan ;
685
850
686
851
// Make sure the range is within the current span.
687
852
assert (start >= span.start && start <= span.end);
@@ -713,7 +878,7 @@ class Spanometer {
713
878
714
879
double _measure (int start, int end) {
715
880
assert (_currentSpan != null );
716
- final FlatTextSpan span = _currentSpan ! ;
881
+ final FlatTextSpan span = currentSpan ;
717
882
718
883
// Make sure the range is within the current span.
719
884
assert (start >= span.start && start <= span.end);
0 commit comments