-
Notifications
You must be signed in to change notification settings - Fork 6k
[web] Rich paragraph getPositionForOffset #23133
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -263,6 +263,64 @@ class TextLayoutService { | |
| } | ||
| return boxes; | ||
| } | ||
|
|
||
| ui.TextPosition getPositionForOffset(ui.Offset offset) { | ||
| // After layout, each line has boxes that contain enough information to make | ||
| // it possible to do hit testing. Once we find the box, we look inside that | ||
| // box to find where exactly the `offset` is located. | ||
|
|
||
| // [offset] is above all the lines. | ||
| if (offset.dy < 0) { | ||
| return ui.TextPosition(offset: 0, affinity: ui.TextAffinity.downstream); | ||
| } | ||
|
|
||
| // [offset] is below all the lines. | ||
| if (offset.dy >= paragraph.height) { | ||
| return ui.TextPosition( | ||
| offset: paragraph.toPlainText().length, | ||
| affinity: ui.TextAffinity.upstream, | ||
| ); | ||
| } | ||
|
|
||
| final EngineLineMetrics line = _findLineForY(offset.dy); | ||
| // [offset] is to the left of the line. | ||
| if (offset.dx <= line.left) { | ||
| return ui.TextPosition( | ||
| offset: line.startIndex, | ||
| affinity: ui.TextAffinity.downstream, | ||
| ); | ||
| } | ||
|
|
||
| // [offset] is to the right of the line. | ||
| if (offset.dx >= line.left + line.widthWithTrailingSpaces) { | ||
| return ui.TextPosition( | ||
| offset: line.endIndexWithoutNewlines, | ||
| affinity: ui.TextAffinity.upstream, | ||
| ); | ||
| } | ||
|
|
||
| final double dx = offset.dx - line.left; | ||
| for (final RangeBox box in line.boxes!) { | ||
| if (box.left <= dx && dx <= box.right) { | ||
| return box.getPositionForX(dx); | ||
| } | ||
| } | ||
| // Is this ever reachable? | ||
| return ui.TextPosition(offset: line.startIndex); | ||
| } | ||
|
|
||
| EngineLineMetrics _findLineForY(double y) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you agree, add a comment: // Not worth using binary search with a line.yOffset since number of lines is typically low. Or something to that effect There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| // We could do a binary search here but it's not worth it because the number | ||
| // of line is typically low, and each iteration is a cheap comparison of | ||
| // doubles. | ||
| for (EngineLineMetrics line in lines) { | ||
| if (y <= line.height) { | ||
| return line; | ||
| } | ||
| y -= line.height; | ||
| } | ||
| return lines.last; | ||
| } | ||
| } | ||
|
|
||
| /// Represents a box inside [span] with the range of [start] to [end]. | ||
|
|
@@ -347,6 +405,61 @@ class RangeBox { | |
| direction, | ||
| ); | ||
| } | ||
|
|
||
| /// Returns the text position within this box's range that's closest to the | ||
| /// given [x] offset. | ||
| /// | ||
| /// The [x] offset is expected to be relative to the left edge of the line, | ||
| /// just like the coordinates of this box. | ||
| ui.TextPosition getPositionForX(double x) { | ||
| spanometer.currentSpan = span as FlatTextSpan; | ||
|
|
||
| // Make `x` relative to this box. | ||
| x -= left; | ||
|
|
||
| final int startIndex = start.index; | ||
| final int endIndex = end.indexWithoutTrailingNewlines; | ||
| // The resulting `cutoff` is the index of the character where the `x` offset | ||
| // falls. We should return the text position of either `cutoff` or | ||
| // `cutoff + 1` depending on which one `x` is closer to. | ||
| // | ||
| // offset x | ||
| // ↓ | ||
| // "A B C D E F" | ||
| // ↑ | ||
| // cutoff | ||
| final int cutoff = spanometer.forceBreak( | ||
| startIndex, | ||
| endIndex, | ||
| availableWidth: x, | ||
| allowEmpty: true, | ||
| ); | ||
|
|
||
| if (cutoff == endIndex) { | ||
| return ui.TextPosition( | ||
| offset: cutoff, | ||
| affinity: ui.TextAffinity.upstream, | ||
| ); | ||
| } | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. final int startIndex = start.index There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Lol the |
||
| final double lowWidth = spanometer._measure(startIndex, cutoff); | ||
| final double highWidth = spanometer._measure(startIndex, cutoff + 1); | ||
|
|
||
| // See if `x` is closer to `cutoff` or `cutoff + 1`. | ||
| if (x - lowWidth < highWidth - x) { | ||
| // The offset is closer to cutoff. | ||
| return ui.TextPosition( | ||
| offset: cutoff, | ||
| affinity: ui.TextAffinity.downstream, | ||
| ); | ||
| } else { | ||
| // The offset is closer to cutoff + 1. | ||
| return ui.TextPosition( | ||
| offset: cutoff + 1, | ||
| affinity: ui.TextAffinity.upstream, | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Represents a segment in a line of a paragraph. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -307,6 +307,246 @@ void testMain() async { | |
| ); | ||
| }); | ||
| }); | ||
|
|
||
| group('$CanvasParagraph.getPositionForOffset', () { | ||
| test('handles single-line multi-span paragraphs', () { | ||
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { | ||
| builder.pushStyle(EngineTextStyle.only(color: blue)); | ||
| builder.addText('Lorem '); | ||
| builder.pushStyle(EngineTextStyle.only(color: green)); | ||
| builder.addText('ipsum '); | ||
| builder.pop(); | ||
| builder.addText('.'); | ||
| }) | ||
| ..layout(constrain(double.infinity)); | ||
|
|
||
| // Above the line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, -5)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the top left corner of the line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 0)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the beginning of the line. | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. test y=0 as well ? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 5)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // Below the line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 12)), | ||
| pos(13, ui.TextAffinity.upstream), | ||
| ); | ||
| // At the end of the line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(130, 5)), | ||
| pos(13, ui.TextAffinity.upstream), | ||
| ); | ||
| // On the left half of "p" in "ipsum". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(74, 5)), | ||
| pos(7, ui.TextAffinity.downstream), | ||
| ); | ||
| // On the right half of "p" in "ipsum". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(76, 5)), | ||
| pos(8, ui.TextAffinity.upstream), | ||
| ); | ||
| // At the top of the line, on the left half of "p" in "ipsum". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(74, 0)), | ||
| pos(7, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the top of the line, on the right half of "p" in "ipsum". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(76, 0)), | ||
| pos(8, ui.TextAffinity.upstream), | ||
| ); | ||
| }); | ||
|
|
||
| test('handles multi-line single-span paragraphs', () { | ||
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { | ||
| builder.addText('Lorem ipsum dolor sit'); | ||
| }) | ||
| ..layout(constrain(90.0)); | ||
|
|
||
| // Lines: | ||
| // "Lorem " | ||
| // "ipsum " | ||
| // "dolor sit" | ||
|
|
||
| // Above the first line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, -5)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the top left corner of the line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 0)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the beginning of the first line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 5)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the end of the first line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(60, 5)), | ||
| pos(6, ui.TextAffinity.upstream), | ||
| ); | ||
| // After the end of the first line to the right. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(70, 5)), | ||
| pos(6, ui.TextAffinity.upstream), | ||
| ); | ||
| // On the left half of " " in "Lorem ". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(54, 5)), | ||
| pos(5, ui.TextAffinity.downstream), | ||
| ); | ||
| // On the right half of " " in "Lorem ". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(56, 5)), | ||
| pos(6, ui.TextAffinity.upstream), | ||
| ); | ||
|
|
||
| // At the beginning of the second line "ipsum ". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 15)), | ||
| pos(6, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the end of the second line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(60, 15)), | ||
| pos(12, ui.TextAffinity.upstream), | ||
| ); | ||
| // After the end of the second line to the right. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(70, 15)), | ||
| pos(12, ui.TextAffinity.upstream), | ||
| ); | ||
|
|
||
| // Below the third line "dolor sit". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 40)), | ||
| pos(21, ui.TextAffinity.upstream), | ||
| ); | ||
| // At the end of the third line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(90, 25)), | ||
| pos(21, ui.TextAffinity.upstream), | ||
| ); | ||
| // After the end of the third line to the right. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(100, 25)), | ||
| pos(21, ui.TextAffinity.upstream), | ||
| ); | ||
| // On the left half of " " in "dolor sit". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(54, 25)), | ||
| pos(17, ui.TextAffinity.downstream), | ||
| ); | ||
| // On the right half of " " in "dolor sit". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(56, 25)), | ||
| pos(18, ui.TextAffinity.upstream), | ||
| ); | ||
| }); | ||
|
|
||
| test('handles multi-line multi-span paragraphs', () { | ||
| final CanvasParagraph paragraph = rich(ahemStyle, (builder) { | ||
| builder.pushStyle(EngineTextStyle.only(color: blue)); | ||
| builder.addText('Lorem ipsum '); | ||
| builder.pushStyle(EngineTextStyle.only(color: green)); | ||
| builder.addText('dolor '); | ||
| builder.pop(); | ||
| builder.addText('sit'); | ||
| }) | ||
| ..layout(constrain(90.0)); | ||
|
|
||
| // Lines: | ||
| // "Lorem " | ||
| // "ipsum " | ||
| // "dolor sit" | ||
|
|
||
| // Above the first line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, -5)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the beginning of the first line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 5)), | ||
| pos(0, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the end of the first line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(60, 5)), | ||
| pos(6, ui.TextAffinity.upstream), | ||
| ); | ||
| // After the end of the first line to the right. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(70, 5)), | ||
| pos(6, ui.TextAffinity.upstream), | ||
| ); | ||
| // On the left half of " " in "Lorem ". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(54, 5)), | ||
| pos(5, ui.TextAffinity.downstream), | ||
| ); | ||
| // On the right half of " " in "Lorem ". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(56, 5)), | ||
| pos(6, ui.TextAffinity.upstream), | ||
| ); | ||
|
|
||
| // At the beginning of the second line "ipsum ". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 15)), | ||
| pos(6, ui.TextAffinity.downstream), | ||
| ); | ||
| // At the end of the second line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(60, 15)), | ||
| pos(12, ui.TextAffinity.upstream), | ||
| ); | ||
| // After the end of the second line to the right. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(70, 15)), | ||
| pos(12, ui.TextAffinity.upstream), | ||
| ); | ||
|
|
||
| // Below the third line "dolor sit". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(0, 40)), | ||
| pos(21, ui.TextAffinity.upstream), | ||
| ); | ||
| // At the end of the third line. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(90, 25)), | ||
| pos(21, ui.TextAffinity.upstream), | ||
| ); | ||
| // After the end of the third line to the right. | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(100, 25)), | ||
| pos(21, ui.TextAffinity.upstream), | ||
| ); | ||
| // On the left half of " " in "dolor sit". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(54, 25)), | ||
| pos(17, ui.TextAffinity.downstream), | ||
| ); | ||
| // On the right half of " " in "dolor sit". | ||
| expect( | ||
| paragraph.getPositionForOffset(ui.Offset(56, 25)), | ||
| pos(18, ui.TextAffinity.upstream), | ||
| ); | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| /// Shortcut to create a [ui.TextBox] with an optional [ui.TextDirection]. | ||
|
|
@@ -319,3 +559,8 @@ ui.TextBox box( | |
| ]) { | ||
| return ui.TextBox.fromLTRBD(left, top, right, bottom, direction); | ||
| } | ||
|
|
||
| /// Shortcut to create a [ui.TextPosition]. | ||
| ui.TextPosition pos(int offset, ui.TextAffinity affinity) { | ||
| return ui.TextPosition(offset: offset, affinity: affinity); | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add the comment: // After layout, each paragraph span should have info about
// its position and dimensions. Use that information to find which span the
// offset belongs to, then search within that span for the exact character.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.