Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 19950f5

Browse files
authored
[web] Rich paragraph getBoxesForRange (#23098)
1 parent 27ebbc4 commit 19950f5

File tree

4 files changed

+509
-17
lines changed

4 files changed

+509
-17
lines changed

lib/web_ui/lib/src/engine/text/canvas_paragraph.dart

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -192,15 +192,7 @@ class CanvasParagraph implements EngineParagraph {
192192
ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
193193
ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
194194
}) {
195-
// TODO(mdebbar): After layout, each paragraph span should have info about
196-
// its position and dimensions.
197-
//
198-
// 1. Find the spans where the `start` and `end` indices fall.
199-
// 2. If it's the same span, find the sub-box from `start` to `end`.
200-
// 3. Else, find the trailing box(es) of the `start` span, and the `leading`
201-
// box(es) of the `end` span.
202-
// 4. Include the boxes of all the spans in between.
203-
return <ui.TextBox>[];
195+
return _layoutService.getBoxesForRange(start, end, boxHeightStyle, boxWidthStyle);
204196
}
205197

206198
@override

lib/web_ui/lib/src/engine/text/layout_service.dart

Lines changed: 170 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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);

lib/web_ui/lib/src/engine/text/paragraph.dart

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ class EngineLineMetrics implements ui.LineMetrics {
2525
startIndex = -1,
2626
endIndex = -1,
2727
endIndexWithoutNewlines = -1,
28-
widthWithTrailingSpaces = width;
28+
widthWithTrailingSpaces = width,
29+
boxes = null;
2930

3031
EngineLineMetrics.withText(
3132
String this.displayText, {
@@ -50,7 +51,8 @@ class EngineLineMetrics implements ui.LineMetrics {
5051
descent = double.infinity,
5152
unscaledAscent = double.infinity,
5253
height = double.infinity,
53-
baseline = double.infinity;
54+
baseline = double.infinity,
55+
boxes = null;
5456

5557
EngineLineMetrics.rich(
5658
this.lineNumber, {
@@ -64,10 +66,14 @@ class EngineLineMetrics implements ui.LineMetrics {
6466
required this.left,
6567
required this.height,
6668
required this.baseline,
69+
// Didn't use `this.boxes` because we want it to be non-null in this
70+
// constructor.
71+
required List<RangeBox> boxes,
6772
}) : displayText = null,
6873
ascent = double.infinity,
6974
descent = double.infinity,
70-
unscaledAscent = double.infinity;
75+
unscaledAscent = double.infinity,
76+
this.boxes = boxes;
7177

7278
/// The text to be rendered on the screen representing this line.
7379
final String? displayText;
@@ -91,6 +97,10 @@ class EngineLineMetrics implements ui.LineMetrics {
9197
/// characters.
9298
final int endIndexWithoutNewlines;
9399

100+
/// The list of boxes representing the entire line, possibly across multiple
101+
/// spans.
102+
final List<RangeBox>? boxes;
103+
94104
@override
95105
final bool hardBreak;
96106

@@ -130,6 +140,10 @@ class EngineLineMetrics implements ui.LineMetrics {
130140
@override
131141
final int lineNumber;
132142

143+
bool overlapsWith(int startIndex, int endIndex) {
144+
return startIndex < this.endIndex && this.startIndex < endIndex;
145+
}
146+
133147
@override
134148
int get hashCode => ui.hashValues(
135149
displayText,

0 commit comments

Comments
 (0)