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

[web] Paragraph.getBoxesForRange uses LineMetrics #16625

Merged
merged 3 commits into from
Feb 19, 2020
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 77 additions & 19 deletions lib/web_ui/lib/src/engine/text/paragraph.dart
Original file line number Diff line number Diff line change
@@ -176,6 +176,8 @@ class EngineParagraph implements ui.Paragraph {
/// The measurement result of the last layout operation.
MeasurementResult _measurementResult;

bool get _hasLineMetrics => _measurementResult?.lines != null;

@override
double get width => _measurementResult?.width ?? -1;

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

@override
double get longestLine {
if (_measurementResult.lines != null) {
if (_hasLineMetrics) {
double maxWidth = 0.0;
for (ui.LineMetrics metrics in _measurementResult.lines) {
if (maxWidth < metrics.width) {
@@ -308,7 +310,7 @@ class EngineParagraph implements ui.Paragraph {
/// - Paragraphs that have a non-null word-spacing.
/// - Paragraphs with a background.
bool get _drawOnCanvas {
if (_measurementResult.lines == null) {
if (!_hasLineMetrics) {
return false;
}

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

return _measurementService.measureBoxesForRange(
this,
_lastUsedConstraints,
start: start,
end: end,
alignOffset: _alignOffset,
textDirection: _textDirection,
// Fallback to the old, DOM-based box measurements when there's no line
// metrics.
if (!_hasLineMetrics) {
return _measurementService.measureBoxesForRange(
this,
_lastUsedConstraints,
start: start,
end: end,
alignOffset: _alignOffset,
textDirection: _textDirection,
);
}

final List<EngineLineMetrics> lines = _measurementResult.lines;
final EngineLineMetrics startLine = _getLineForIndex(start);
EngineLineMetrics endLine = _getLineForIndex(end);

// If the range end is exactly at the beginning of a line, we shouldn't
// include any boxes from that line.
if (end == endLine.startIndex) {
endLine = lines[endLine.lineNumber - 1];
}

final List<ui.TextBox> boxes = <ui.TextBox>[];
for (int i = startLine.lineNumber; i <= endLine.lineNumber; i++) {
boxes.add(_getBoxForLine(lines[i], start, end));
}
return boxes;
}

ui.TextBox _getBoxForLine(EngineLineMetrics line, int start, int end) {
final double widthBeforeBox = start <= line.startIndex
? 0.0
: _measurementService.measureSubstringWidth(this, line.startIndex, start);
final double widthAfterBox = end >= line.endIndexWithoutNewlines
? 0.0
: _measurementService.measureSubstringWidth(this, end, line.endIndexWithoutNewlines);

final double top = line.lineNumber * _lineHeight;

// |<------------------ line.width ------------------>|
// |-------------|------------------|-------------|-----------------|
// |<-line.left->|<-widthBeforeBox->|<-box width->|<-widthAfterBox->|
// |-------------|------------------|-------------|-----------------|
//
// ^^^^^^^^^^^^^
// This is the box we want to return.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Love the ASCII art docs 👍

return ui.TextBox.fromLTRBD(
line.left + widthBeforeBox,
top,
line.left + line.width - widthAfterBox,
top + _lineHeight,
_textDirection,
);
}

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

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

@override
ui.TextRange getLineBoundary(ui.TextPosition position) {
EngineLineMetrics _getLineForIndex(int index) {
assert(_hasLineMetrics);
final List<EngineLineMetrics> lines = _measurementResult.lines;
if (lines != null) {
final int offset = position.offset;
assert(index >= lines.first.startIndex);
assert(index <= lines.last.endIndex);

for (int i = 0; i < lines.length; i++) {
final EngineLineMetrics line = lines[i];
if (offset >= line.startIndex && offset < line.endIndex) {
return ui.TextRange(start: line.startIndex, end: line.endIndex);
}
for (int i = 0; i < lines.length; i++) {
final EngineLineMetrics line = lines[i];
if (index >= line.startIndex && index < line.endIndex) {
return line;
}
}

assert(index == lines.last.endIndex);
return lines.last;
}

@override
ui.TextRange getLineBoundary(ui.TextPosition position) {
if (_hasLineMetrics) {
final EngineLineMetrics line = _getLineForIndex(position.offset);
return ui.TextRange(start: line.startIndex, end: line.endIndex);
}
return ui.TextRange.empty;
}

203 changes: 203 additions & 0 deletions lib/web_ui/test/paragraph_test.dart
Original file line number Diff line number Diff line change
@@ -426,6 +426,209 @@ void main() async {
expect(paragraph.getBoxesForRange(0, 0), isEmpty);
});

testEachMeasurement('getBoxesForRange multi-line', () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it worth adding a test for a ParagraphStyle with maxLines such that the start/end range fall outside the max line bounds?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good corner case to test. I'd like to merge this PR while the tree is green though. So I'll add the maxLines test in a future PR.

final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
fontFamily: 'Ahem',
fontStyle: FontStyle.normal,
fontWeight: FontWeight.normal,
fontSize: 10,
textDirection: TextDirection.ltr,
));
builder.addText('abcd\n');
builder.addText('abcdefg\n');
builder.addText('ab');
final Paragraph paragraph = builder.build();
paragraph.layout(const ParagraphConstraints(width: 100));

// In the dom-based measurement (except Firefox), there will be some
// discrepancies around line ends.
final isDiscrepancyExpected =
!TextMeasurementService.enableExperimentalCanvasImplementation &&
browserEngine != BrowserEngine.firefox;

// First line: "abcd\n"

// At the beginning of the first line.
expect(
paragraph.getBoxesForRange(0, 0),
<TextBox>[],
);
// At the end of the first line.
expect(
paragraph.getBoxesForRange(4, 4),
<TextBox>[],
);
// Between "b" and "c" in the first line.
expect(
paragraph.getBoxesForRange(2, 2),
<TextBox>[],
);
// The range "ab" in the first line.
expect(
paragraph.getBoxesForRange(0, 2),
<TextBox>[
TextBox.fromLTRBD(0.0, 0.0, 20.0, 10.0, TextDirection.ltr),
],
);
// The range "bc" in the first line.
expect(
paragraph.getBoxesForRange(1, 3),
<TextBox>[
TextBox.fromLTRBD(10.0, 0.0, 30.0, 10.0, TextDirection.ltr),
],
);
// The range "d" in the first line.
expect(
paragraph.getBoxesForRange(3, 4),
<TextBox>[
TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr),
],
);
// The range "\n" in the first line.
expect(
paragraph.getBoxesForRange(4, 5),
<TextBox>[
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
],
);
// The range "cd\n" in the first line.
expect(
paragraph.getBoxesForRange(2, 5),
<TextBox>[
TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
],
);

// Second line: "abcdefg\n"

// At the beginning of the second line.
expect(
paragraph.getBoxesForRange(5, 5),
<TextBox>[],
);
// At the end of the second line.
expect(
paragraph.getBoxesForRange(12, 12),
<TextBox>[],
);
// The range "efg" in the second line.
expect(
paragraph.getBoxesForRange(9, 12),
<TextBox>[
TextBox.fromLTRBD(40.0, 10.0, 70.0, 20.0, TextDirection.ltr),
],
);
// The range "bcde" in the second line.
expect(
paragraph.getBoxesForRange(6, 10),
<TextBox>[
TextBox.fromLTRBD(10.0, 10.0, 50.0, 20.0, TextDirection.ltr),
],
);
// The range "fg\n" in the second line.
expect(
paragraph.getBoxesForRange(10, 13),
<TextBox>[
TextBox.fromLTRBD(50.0, 10.0, 70.0, 20.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
],
);

// Last (third) line: "ab"

// At the beginning of the last line.
expect(
paragraph.getBoxesForRange(13, 13),
<TextBox>[],
);
// At the end of the last line.
expect(
paragraph.getBoxesForRange(15, 15),
<TextBox>[],
);
// The range "a" in the last line.
expect(
paragraph.getBoxesForRange(14, 15),
<TextBox>[
TextBox.fromLTRBD(10.0, 20.0, 20.0, 30.0, TextDirection.ltr),
],
);
// The range "ab" in the last line.
expect(
paragraph.getBoxesForRange(13, 15),
<TextBox>[
TextBox.fromLTRBD(0.0, 20.0, 20.0, 30.0, TextDirection.ltr),
],
);


// Combine multiple lines

// The range "cd\nabc".
expect(
paragraph.getBoxesForRange(2, 8),
<TextBox>[
TextBox.fromLTRBD(20.0, 0.0, 40.0, 10.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 10.0, 30.0, 20.0, TextDirection.ltr),
],
);

// The range "\nabcd".
expect(
paragraph.getBoxesForRange(4, 9),
<TextBox>[
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 10.0, 40.0, 20.0, TextDirection.ltr),
],
);

// The range "d\nabcdefg\na".
expect(
paragraph.getBoxesForRange(3, 14),
<TextBox>[
TextBox.fromLTRBD(30.0, 0.0, 40.0, 10.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 20.0, 10.0, 30.0, TextDirection.ltr),
],
);

// The range "abcd\nabcdefg\n".
expect(
paragraph.getBoxesForRange(0, 13),
<TextBox>[
TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
],
);

// The range "abcd\nabcdefg\nab".
expect(
paragraph.getBoxesForRange(0, 15),
<TextBox>[
TextBox.fromLTRBD(0.0, 0.0, 40.0, 10.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(40.0, 0.0, 40.0, 10.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 10.0, 70.0, 20.0, TextDirection.ltr),
if (isDiscrepancyExpected)
TextBox.fromLTRBD(70.0, 10.0, 70.0, 20.0, TextDirection.ltr),
TextBox.fromLTRBD(0.0, 20.0, 20.0, 30.0, TextDirection.ltr),
],
);
});

test('longestLine', () {
// [Paragraph.longestLine] is only supported by canvas-based measurement.
TextMeasurementService.enableExperimentalCanvasImplementation = true;