@@ -111,12 +111,14 @@ class TextLayoutService {
111111 // TODO(mdebbar):
112112 // (1) adjust the current line's height to fit the placeholder.
113113 // (2) update accumulated line width.
114+ // (3) add placeholder box to line.
114115 } else {
115116 // The placeholder can't fit on the current line.
116117 // TODO(mdebbar):
117118 // (1) create a line.
118119 // (2) adjust the new line's height to fit the placeholder.
119120 // (3) update `lineStart`, etc.
121+ // (4) add placeholder box to line.
120122 }
121123 } else if (span is FlatTextSpan ) {
122124 spanometer.currentSpan = span;
@@ -171,6 +173,7 @@ class TextLayoutService {
171173
172174 // Only go to the next span if we've reached the end of this span.
173175 if (currentLine.end.index >= span.end && spanIndex < spanCount - 1 ) {
176+ currentLine.createBox ();
174177 span = paragraph.spans[++ spanIndex];
175178 }
176179 }
@@ -229,6 +232,121 @@ class TextLayoutService {
229232 }
230233 }
231234 }
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+ }
232350}
233351
234352/// Represents a segment in a line of a paragraph.
@@ -310,6 +428,7 @@ class LineBuilder {
310428 }
311429
312430 final List <LineSegment > _segments = < LineSegment > [];
431+ final List <RangeBox > _boxes = < RangeBox > [];
313432
314433 final double maxWidth;
315434 final CanvasParagraph paragraph;
@@ -398,7 +517,7 @@ class LineBuilder {
398517 // The segment starts at the end of the line.
399518 final LineBreakResult segmentStart = end;
400519 return LineSegment (
401- span: spanometer.currentSpan! ,
520+ span: spanometer.currentSpan,
402521 start: segmentStart,
403522 end: segmentEnd,
404523 width: spanometer.measure (segmentStart, segmentEnd),
@@ -542,8 +661,53 @@ class LineBuilder {
542661 LineBreakResult .sameIndex (breakingPoint, LineBreakType .prohibited));
543662 }
544663
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+
545706 /// Builds the [EngineLineMetrics] instance that represents this line.
546707 EngineLineMetrics build ({String ? ellipsis}) {
708+ // At the end of each line, we cut the last box of the line.
709+ createBox ();
710+
547711 final double ellipsisWidth =
548712 ellipsis == null ? 0.0 : spanometer.measureText (ellipsis);
549713
@@ -559,6 +723,7 @@ class LineBuilder {
559723 left: alignOffset,
560724 height: height,
561725 baseline: accumulatedHeight + alphabeticBaseline,
726+ boxes: _boxes,
562727 );
563728 }
564729
@@ -601,12 +766,12 @@ class Spanometer {
601766
602767 String _cssFontString = '' ;
603768
604- double ? get letterSpacing => _currentSpan ! .style._letterSpacing;
769+ double ? get letterSpacing => currentSpan .style._letterSpacing;
605770
606771 TextHeightRuler ? _currentRuler;
607772 FlatTextSpan ? _currentSpan;
608773
609- FlatTextSpan ? get currentSpan => _currentSpan;
774+ FlatTextSpan get currentSpan => _currentSpan! ;
610775 set currentSpan (FlatTextSpan ? span) {
611776 if (span == _currentSpan) {
612777 return ;
@@ -681,7 +846,7 @@ class Spanometer {
681846 }) {
682847 assert (_currentSpan != null );
683848
684- final FlatTextSpan span = _currentSpan ! ;
849+ final FlatTextSpan span = currentSpan ;
685850
686851 // Make sure the range is within the current span.
687852 assert (start >= span.start && start <= span.end);
@@ -713,7 +878,7 @@ class Spanometer {
713878
714879 double _measure (int start, int end) {
715880 assert (_currentSpan != null );
716- final FlatTextSpan span = _currentSpan ! ;
881+ final FlatTextSpan span = currentSpan ;
717882
718883 // Make sure the range is within the current span.
719884 assert (start >= span.start && start <= span.end);
0 commit comments