mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
Reland #16206: "[web] Correct getPositionForOffset for multi-line paragraphs" (flutter/engine#16365)
This commit is contained in:
parent
617582e744
commit
efca62703f
@ -373,7 +373,10 @@ class DomTextMeasurementService extends TextMeasurementService {
|
||||
@override
|
||||
ui.TextPosition getTextPositionForOffset(EngineParagraph paragraph,
|
||||
ui.ParagraphConstraints constraints, ui.Offset offset) {
|
||||
assert(paragraph._plainText == null, 'should only be called for multispan');
|
||||
assert(
|
||||
paragraph._measurementResult.lines == null,
|
||||
'should only be called when the faster lines-based approach is not possible',
|
||||
);
|
||||
|
||||
final ParagraphGeometricStyle style = paragraph._geometricStyle;
|
||||
final ParagraphRuler ruler =
|
||||
|
||||
@ -378,17 +378,61 @@ class EngineParagraph implements ui.Paragraph {
|
||||
|
||||
@override
|
||||
ui.TextPosition getPositionForOffset(ui.Offset offset) {
|
||||
if (_plainText == null) {
|
||||
final List<EngineLineMetrics> lines = _measurementResult.lines;
|
||||
if (lines == null) {
|
||||
return getPositionForMultiSpanOffset(offset);
|
||||
}
|
||||
|
||||
// [offset] is above all the lines.
|
||||
if (offset.dy < 0) {
|
||||
return ui.TextPosition(
|
||||
offset: 0,
|
||||
affinity: ui.TextAffinity.downstream,
|
||||
);
|
||||
}
|
||||
|
||||
final int lineNumber = offset.dy ~/ _measurementResult.lineHeight;
|
||||
|
||||
// [offset] is below all the lines.
|
||||
if (lineNumber >= lines.length) {
|
||||
return ui.TextPosition(
|
||||
offset: _plainText.length,
|
||||
affinity: ui.TextAffinity.upstream,
|
||||
);
|
||||
}
|
||||
|
||||
final EngineLineMetrics lineMetrics = lines[lineNumber];
|
||||
final double lineLeft = lineMetrics.left;
|
||||
final double lineRight = lineLeft + lineMetrics.width;
|
||||
|
||||
// [offset] is to the left of the line.
|
||||
if (offset.dx <= lineLeft) {
|
||||
return ui.TextPosition(
|
||||
offset: lineMetrics.startIndex,
|
||||
affinity: ui.TextAffinity.downstream,
|
||||
);
|
||||
}
|
||||
|
||||
// [offset] is to the right of the line.
|
||||
if (offset.dx >= lineRight) {
|
||||
return ui.TextPosition(
|
||||
offset: lineMetrics.endIndex,
|
||||
affinity: ui.TextAffinity.upstream,
|
||||
);
|
||||
}
|
||||
|
||||
// If we reach here, it means the [offset] is somewhere within the line. The
|
||||
// code below will do a binary search to find where exactly the [offset]
|
||||
// falls within the line.
|
||||
|
||||
final double dx = offset.dx - _alignOffset;
|
||||
final TextMeasurementService instance = _measurementService;
|
||||
|
||||
int low = 0;
|
||||
int high = _plainText.length;
|
||||
int low = lineMetrics.startIndex;
|
||||
int high = lineMetrics.endIndex;
|
||||
do {
|
||||
final int current = (low + high) ~/ 2;
|
||||
final double width = instance.measureSubstringWidth(this, 0, current);
|
||||
final double width = instance.measureSubstringWidth(this, lineMetrics.startIndex, current);
|
||||
if (width < dx) {
|
||||
low = current;
|
||||
} else if (width > dx) {
|
||||
@ -403,8 +447,8 @@ class EngineParagraph implements ui.Paragraph {
|
||||
return ui.TextPosition(offset: high, affinity: ui.TextAffinity.upstream);
|
||||
}
|
||||
|
||||
final double lowWidth = instance.measureSubstringWidth(this, 0, low);
|
||||
final double highWidth = instance.measureSubstringWidth(this, 0, high);
|
||||
final double lowWidth = instance.measureSubstringWidth(this, lineMetrics.startIndex, low);
|
||||
final double highWidth = instance.measureSubstringWidth(this, lineMetrics.startIndex, high);
|
||||
|
||||
if (dx - lowWidth < highWidth - dx) {
|
||||
// The offset is closer to the low index.
|
||||
|
||||
@ -8,7 +8,7 @@ import 'package:ui/ui.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
void testEachMeasurement(String description, VoidCallback body, {bool skip}) {
|
||||
test(description, () async {
|
||||
test('$description (dom measurement)', () async {
|
||||
try {
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
return body();
|
||||
@ -130,6 +130,154 @@ void main() async {
|
||||
}, // TODO(nurhan): https://github.com/flutter/flutter/issues/46638
|
||||
skip: (browserEngine == BrowserEngine.firefox));
|
||||
|
||||
testEachMeasurement('getPositionForOffset single-line', () {
|
||||
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
|
||||
fontFamily: 'Ahem',
|
||||
fontStyle: FontStyle.normal,
|
||||
fontWeight: FontWeight.normal,
|
||||
fontSize: 10,
|
||||
textDirection: TextDirection.ltr,
|
||||
));
|
||||
builder.addText('abcd efg');
|
||||
final Paragraph paragraph = builder.build();
|
||||
paragraph.layout(const ParagraphConstraints(width: 1000));
|
||||
|
||||
// At the beginning of the line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, 5)),
|
||||
TextPosition(offset: 0, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// Below the line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, 12)),
|
||||
TextPosition(offset: 8, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// Above the line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, -5)),
|
||||
TextPosition(offset: 0, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// At the end of the line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(80, 5)),
|
||||
TextPosition(offset: 8, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// On the left side of "b".
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(14, 5)),
|
||||
TextPosition(offset: 1, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// On the right side of "b".
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(16, 5)),
|
||||
TextPosition(offset: 2, affinity: TextAffinity.upstream),
|
||||
);
|
||||
});
|
||||
|
||||
test('getPositionForOffset multi-line', () {
|
||||
// [Paragraph.getPositionForOffset] for multi-line text doesn't work well
|
||||
// with dom-based measurement.
|
||||
TextMeasurementService.enableExperimentalCanvasImplementation = true;
|
||||
TextMeasurementService.initialize(rulerCacheCapacity: 2);
|
||||
|
||||
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: 1000));
|
||||
|
||||
// First line: "abcd\n"
|
||||
|
||||
// At the beginning of the first line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, 5)),
|
||||
TextPosition(offset: 0, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// Above the first line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, -15)),
|
||||
TextPosition(offset: 0, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// At the end of the first line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(50, 5)),
|
||||
TextPosition(offset: 5, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// On the left side of "b" in the first line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(14, 5)),
|
||||
TextPosition(offset: 1, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// On the right side of "b" in the first line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(16, 5)),
|
||||
TextPosition(offset: 2, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
|
||||
// Second line: "abcdefg\n"
|
||||
|
||||
// At the beginning of the second line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, 15)),
|
||||
TextPosition(offset: 5, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// At the end of the second line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(100, 15)),
|
||||
TextPosition(offset: 13, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// On the left side of "e" in the second line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(44, 15)),
|
||||
TextPosition(offset: 9, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// On the right side of "e" in the second line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(46, 15)),
|
||||
TextPosition(offset: 10, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
|
||||
// Last (third) line: "ab"
|
||||
|
||||
// At the beginning of the last line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, 25)),
|
||||
TextPosition(offset: 13, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// At the end of the last line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(40, 25)),
|
||||
TextPosition(offset: 15, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// Below the last line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(0, 32)),
|
||||
TextPosition(offset: 15, affinity: TextAffinity.upstream),
|
||||
);
|
||||
// On the left side of "b" in the last line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(12, 25)),
|
||||
TextPosition(offset: 14, affinity: TextAffinity.downstream),
|
||||
);
|
||||
// On the right side of "a" in the last line.
|
||||
expect(
|
||||
paragraph.getPositionForOffset(Offset(9, 25)),
|
||||
TextPosition(offset: 14, affinity: TextAffinity.upstream),
|
||||
);
|
||||
|
||||
TextMeasurementService.clearCache();
|
||||
TextMeasurementService.enableExperimentalCanvasImplementation = false;
|
||||
});
|
||||
|
||||
testEachMeasurement('getBoxesForRange returns a box', () {
|
||||
final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
|
||||
fontFamily: 'Ahem',
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user