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

Commit 8dba815

Browse files
authored
[web] Paragraph.getBoxesForRange uses LineMetrics (#16625)
1 parent 89830c8 commit 8dba815

File tree

2 files changed

+280
-19
lines changed

2 files changed

+280
-19
lines changed

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

Lines changed: 77 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,8 @@ class EngineParagraph implements ui.Paragraph {
176176
/// The measurement result of the last layout operation.
177177
MeasurementResult _measurementResult;
178178

179+
bool get _hasLineMetrics => _measurementResult?.lines != null;
180+
179181
@override
180182
double get width => _measurementResult?.width ?? -1;
181183

@@ -197,7 +199,7 @@ class EngineParagraph implements ui.Paragraph {
197199

198200
@override
199201
double get longestLine {
200-
if (_measurementResult.lines != null) {
202+
if (_hasLineMetrics) {
201203
double maxWidth = 0.0;
202204
for (ui.LineMetrics metrics in _measurementResult.lines) {
203205
if (maxWidth < metrics.width) {
@@ -308,7 +310,7 @@ class EngineParagraph implements ui.Paragraph {
308310
/// - Paragraphs that have a non-null word-spacing.
309311
/// - Paragraphs with a background.
310312
bool get _drawOnCanvas {
311-
if (_measurementResult.lines == null) {
313+
if (!_hasLineMetrics) {
312314
return false;
313315
}
314316

@@ -363,13 +365,59 @@ class EngineParagraph implements ui.Paragraph {
363365
return <ui.TextBox>[];
364366
}
365367

366-
return _measurementService.measureBoxesForRange(
367-
this,
368-
_lastUsedConstraints,
369-
start: start,
370-
end: end,
371-
alignOffset: _alignOffset,
372-
textDirection: _textDirection,
368+
// Fallback to the old, DOM-based box measurements when there's no line
369+
// metrics.
370+
if (!_hasLineMetrics) {
371+
return _measurementService.measureBoxesForRange(
372+
this,
373+
_lastUsedConstraints,
374+
start: start,
375+
end: end,
376+
alignOffset: _alignOffset,
377+
textDirection: _textDirection,
378+
);
379+
}
380+
381+
final List<EngineLineMetrics> lines = _measurementResult.lines;
382+
final EngineLineMetrics startLine = _getLineForIndex(start);
383+
EngineLineMetrics endLine = _getLineForIndex(end);
384+
385+
// If the range end is exactly at the beginning of a line, we shouldn't
386+
// include any boxes from that line.
387+
if (end == endLine.startIndex) {
388+
endLine = lines[endLine.lineNumber - 1];
389+
}
390+
391+
final List<ui.TextBox> boxes = <ui.TextBox>[];
392+
for (int i = startLine.lineNumber; i <= endLine.lineNumber; i++) {
393+
boxes.add(_getBoxForLine(lines[i], start, end));
394+
}
395+
return boxes;
396+
}
397+
398+
ui.TextBox _getBoxForLine(EngineLineMetrics line, int start, int end) {
399+
final double widthBeforeBox = start <= line.startIndex
400+
? 0.0
401+
: _measurementService.measureSubstringWidth(this, line.startIndex, start);
402+
final double widthAfterBox = end >= line.endIndexWithoutNewlines
403+
? 0.0
404+
: _measurementService.measureSubstringWidth(this, end, line.endIndexWithoutNewlines);
405+
406+
final double top = line.lineNumber * _lineHeight;
407+
408+
// |<------------------ line.width ------------------>|
409+
// |-------------|------------------|-------------|-----------------|
410+
// |<-line.left->|<-widthBeforeBox->|<-box width->|<-widthAfterBox->|
411+
// |-------------|------------------|-------------|-----------------|
412+
//
413+
// ^^^^^^^^^^^^^
414+
// This is the box we want to return.
415+
return ui.TextBox.fromLTRBD(
416+
line.left + widthBeforeBox,
417+
top,
418+
line.left + line.width - widthAfterBox,
419+
top + _lineHeight,
420+
_textDirection,
373421
);
374422
}
375423

@@ -388,7 +436,7 @@ class EngineParagraph implements ui.Paragraph {
388436
@override
389437
ui.TextPosition getPositionForOffset(ui.Offset offset) {
390438
final List<EngineLineMetrics> lines = _measurementResult.lines;
391-
if (lines == null) {
439+
if (!_hasLineMetrics) {
392440
return getPositionForMultiSpanOffset(offset);
393441
}
394442

@@ -489,19 +537,29 @@ class EngineParagraph implements ui.Paragraph {
489537
return ui.TextRange(start: start, end: end);
490538
}
491539

492-
@override
493-
ui.TextRange getLineBoundary(ui.TextPosition position) {
540+
EngineLineMetrics _getLineForIndex(int index) {
541+
assert(_hasLineMetrics);
494542
final List<EngineLineMetrics> lines = _measurementResult.lines;
495-
if (lines != null) {
496-
final int offset = position.offset;
543+
assert(index >= lines.first.startIndex);
544+
assert(index <= lines.last.endIndex);
497545

498-
for (int i = 0; i < lines.length; i++) {
499-
final EngineLineMetrics line = lines[i];
500-
if (offset >= line.startIndex && offset < line.endIndex) {
501-
return ui.TextRange(start: line.startIndex, end: line.endIndex);
502-
}
546+
for (int i = 0; i < lines.length; i++) {
547+
final EngineLineMetrics line = lines[i];
548+
if (index >= line.startIndex && index < line.endIndex) {
549+
return line;
503550
}
504551
}
552+
553+
assert(index == lines.last.endIndex);
554+
return lines.last;
555+
}
556+
557+
@override
558+
ui.TextRange getLineBoundary(ui.TextPosition position) {
559+
if (_hasLineMetrics) {
560+
final EngineLineMetrics line = _getLineForIndex(position.offset);
561+
return ui.TextRange(start: line.startIndex, end: line.endIndex);
562+
}
505563
return ui.TextRange.empty;
506564
}
507565

lib/web_ui/test/paragraph_test.dart

Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -426,6 +426,209 @@ void main() async {
426426
expect(paragraph.getBoxesForRange(0, 0), isEmpty);
427427
});
428428

429+
testEachMeasurement('getBoxesForRange multi-line', () {
430+
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
431+
fontFamily: 'Ahem',
432+
fontStyle: FontStyle.normal,
433+
fontWeight: FontWeight.normal,
434+
fontSize: 10,
435+
textDirection: TextDirection.ltr,
436+
));
437+
builder.addText('abcd\n');
438+
builder.addText('abcdefg\n');
439+
builder.addText('ab');
440+
final Paragraph paragraph = builder.build();
441+
paragraph.layout(const ParagraphConstraints(width: 100));
442+
443+
// In the dom-based measurement (except Firefox), there will be some
444+
// discrepancies around line ends.
445+
final isDiscrepancyExpected =
446+
!TextMeasurementService.enableExperimentalCanvasImplementation &&
447+
browserEngine != BrowserEngine.firefox;
448+
449+
// First line: "abcd\n"
450+
451+
// At the beginning of the first line.
452+
expect(
453+
paragraph.getBoxesForRange(0, 0),
454+
<TextBox>[],
455+
);
456+
// At the end of the first line.
457+
expect(
458+
paragraph.getBoxesForRange(4, 4),
459+
<TextBox>[],
460+
);
461+
// Between "b" and "c" in the first line.
462+
expect(
463+
paragraph.getBoxesForRange(2, 2),
464+
<TextBox>[],
465+
);
466+
// The range "ab" in the first line.
467+
expect(
468+
paragraph.getBoxesForRange(0, 2),
469+
<TextBox>[
470+
TextBox.fromLTRBD(0.0, 0.0, 20.0, 10.0, TextDirection.ltr),
471+
],
472+
);
473+
// The range "bc" in the first line.
474+
expect(
475+
paragraph.getBoxesForRange(1, 3),
476+
<TextBox>[
477+
TextBox.fromLTRBD(10.0, 0.0, 30.0, 10.0, TextDirection.ltr),
478+
],
479+
);
480+
// The range "d" in the first line.
481+
expect(
482+
paragraph.getBoxesForRange(3, 4),
483+
<TextBox>[
484+
TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr),
485+
],
486+
);
487+
// The range "\n" in the first line.
488+
expect(
489+
paragraph.getBoxesForRange(4, 5),
490+
<TextBox>[
491+
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
492+
],
493+
);
494+
// The range "cd\n" in the first line.
495+
expect(
496+
paragraph.getBoxesForRange(2, 5),
497+
<TextBox>[
498+
TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr),
499+
if (isDiscrepancyExpected)
500+
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
501+
],
502+
);
503+
504+
// Second line: "abcdefg\n"
505+
506+
// At the beginning of the second line.
507+
expect(
508+
paragraph.getBoxesForRange(5, 5),
509+
<TextBox>[],
510+
);
511+
// At the end of the second line.
512+
expect(
513+
paragraph.getBoxesForRange(12, 12),
514+
<TextBox>[],
515+
);
516+
// The range "efg" in the second line.
517+
expect(
518+
paragraph.getBoxesForRange(9, 12),
519+
<TextBox>[
520+
TextBox.fromLTRBD(40.0, 10.0, 70.0, 20.0, TextDirection.ltr),
521+
],
522+
);
523+
// The range "bcde" in the second line.
524+
expect(
525+
paragraph.getBoxesForRange(6, 10),
526+
<TextBox>[
527+
TextBox.fromLTRBD(10.0, 10.0, 50.0, 20.0, TextDirection.ltr),
528+
],
529+
);
530+
// The range "fg\n" in the second line.
531+
expect(
532+
paragraph.getBoxesForRange(10, 13),
533+
<TextBox>[
534+
TextBox.fromLTRBD(50.0, 10.0, 70.0, 20.0, TextDirection.ltr),
535+
if (isDiscrepancyExpected)
536+
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
537+
],
538+
);
539+
540+
// Last (third) line: "ab"
541+
542+
// At the beginning of the last line.
543+
expect(
544+
paragraph.getBoxesForRange(13, 13),
545+
<TextBox>[],
546+
);
547+
// At the end of the last line.
548+
expect(
549+
paragraph.getBoxesForRange(15, 15),
550+
<TextBox>[],
551+
);
552+
// The range "a" in the last line.
553+
expect(
554+
paragraph.getBoxesForRange(14, 15),
555+
<TextBox>[
556+
TextBox.fromLTRBD(10.0, 20.0, 20.0, 30.0, TextDirection.ltr),
557+
],
558+
);
559+
// The range "ab" in the last line.
560+
expect(
561+
paragraph.getBoxesForRange(13, 15),
562+
<TextBox>[
563+
TextBox.fromLTRBD(0.0, 20.0, 20.0, 30.0, TextDirection.ltr),
564+
],
565+
);
566+
567+
568+
// Combine multiple lines
569+
570+
// The range "cd\nabc".
571+
expect(
572+
paragraph.getBoxesForRange(2, 8),
573+
<TextBox>[
574+
TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr),
575+
if (isDiscrepancyExpected)
576+
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
577+
TextBox.fromLTRBD(0.0, 10.0, 30.0, 20.0, TextDirection.ltr),
578+
],
579+
);
580+
581+
// The range "\nabcd".
582+
expect(
583+
paragraph.getBoxesForRange(4, 9),
584+
<TextBox>[
585+
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
586+
TextBox.fromLTRBD(0.0, 10.0, 40.0, 20.0, TextDirection.ltr),
587+
],
588+
);
589+
590+
// The range "d\nabcdefg\na".
591+
expect(
592+
paragraph.getBoxesForRange(3, 14),
593+
<TextBox>[
594+
TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr),
595+
if (isDiscrepancyExpected)
596+
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
597+
TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr),
598+
if (isDiscrepancyExpected)
599+
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
600+
TextBox.fromLTRBD(0.0, 20.0, 10.0, 30.0, TextDirection.ltr),
601+
],
602+
);
603+
604+
// The range "abcd\nabcdefg\n".
605+
expect(
606+
paragraph.getBoxesForRange(0, 13),
607+
<TextBox>[
608+
TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr),
609+
if (isDiscrepancyExpected)
610+
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
611+
TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr),
612+
if (isDiscrepancyExpected)
613+
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
614+
],
615+
);
616+
617+
// The range "abcd\nabcdefg\nab".
618+
expect(
619+
paragraph.getBoxesForRange(0, 15),
620+
<TextBox>[
621+
TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr),
622+
if (isDiscrepancyExpected)
623+
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
624+
TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr),
625+
if (isDiscrepancyExpected)
626+
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
627+
TextBox.fromLTRBD(0.0, 20.0, 20.0, 30.0, TextDirection.ltr),
628+
],
629+
);
630+
});
631+
429632
test('longestLine', () {
430633
// [Paragraph.longestLine] is only supported by canvas-based measurement.
431634
TextMeasurementService.enableExperimentalCanvasImplementation = true;

0 commit comments

Comments
 (0)