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

Commit 261615e

Browse files
tests
1 parent d33b67d commit 261615e

File tree

6 files changed

+47
-23
lines changed

6 files changed

+47
-23
lines changed

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

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -594,15 +594,16 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
594594
return x;
595595
}
596596

597-
// A naive implementation that only accounts for valid surrogate pairs. This
598-
// is only used if Intl.Segmenter() is not supported so _fromDomSegmenter can't
599-
// be called.
597+
// This is the fallback graphme breaker that is only used if Intl.Segmenter()
598+
// is not supported so _fromDomSegmenter can't be called. This implementation
599+
// breaks the text into UTF-16 codepoints instead of graphme clusters.
600600
List<int> _fallbackGraphemeStartIterable(String fragmentText) {
601601
final List<int> graphemeStarts = <int>[];
602602
bool precededByHighSurrogate = false;
603603
for (int i = 0; i < fragmentText.length; i++) {
604604
final int maskedCodeUnit = fragmentText.codeUnitAt(i) & 0xFC00;
605-
if (maskedCodeUnit != 0xDC00 || precededByHighSurrogate) {
605+
// Only skip `i` if it points to a low surrogate in a valid surrogate pair.
606+
if (maskedCodeUnit != 0xDC00 || !precededByHighSurrogate) {
606607
graphemeStarts.add(start + i);
607608
}
608609
precededByHighSurrogate = maskedCodeUnit == 0xD800;
@@ -625,7 +626,7 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
625626
return graphemeStarts;
626627
}
627628

628-
// This List is an ordered (ascending) sequence of UTF16 offsets that points to
629+
// This List contains an ascending sequence of UTF16 offsets that points to
629630
// grapheme starts within the fragment. Each UTF16 offset is relative to the
630631
// start of the paragraph, instead of the start of the fragment.
631632
late final List<int> _graphemeStarts = _breakFragmentText(
@@ -640,10 +641,8 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
640641
return graphemeStarts;
641642
}
642643

643-
// Returns the GlyphCluster within the range
644-
// [_graphemeStarts[startIndex], _graphemeStarts[endIndex]) in the fragment
645-
// that's visually closeset to the given horizontal offset `x` (in the
646-
// paragraph's coordinates).
644+
// Returns the GlyphInfo within the range [_graphemeStarts[startIndex], _graphemeStarts[endIndex]),
645+
// that's visually closeset to the given horizontal offset `x` (in the paragraph's coordinates).
647646
ui.GlyphInfo _getClosestCharacterInRange(double x, int startIndex, int endIndex) {
648647
final ui.TextRange fullRange = ui.TextRange(start: _graphemeStarts[startIndex], end: _graphemeStarts[endIndex]);
649648
final ui.TextBox fullBox = toTextBox(start: fullRange.start, end: fullRange.end);
@@ -652,8 +651,11 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
652651
}
653652
assert(startIndex + 1 < endIndex);
654653
final ui.TextBox(:double left, :double right) = fullBox;
655-
// The toTextBox call is potentially expensive so we'll try avoiding
656-
// measuring every character in the fragment.
654+
655+
// The toTextBox call is potentially expensive so we'll try reducing the
656+
// search steps with a binary search.
657+
//
658+
// x ∈ (left, right),
657659
if (left < x && x < right) {
658660
final int midIndex = (startIndex + endIndex) ~/ 2;
659661
// endIndex >= startIndex + 2, so midIndex >= start + 1
@@ -672,15 +674,14 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
672674
return distanceToFirst > distanceToSecond ? firstHalf : secondHalf;
673675
}
674676

675-
// Whether the first character or the last character is the closest.
677+
// x ∉ (left, right), it's either the first character or the last, since
678+
// there can only be one writing direction in the fragment.
676679
final ui.TextRange range = switch ((fullBox.direction, x <= left)) {
677-
(ui.TextDirection.ltr, true) ||
678-
(ui.TextDirection.rtl, false) => ui.TextRange(
680+
(ui.TextDirection.ltr, true) || (ui.TextDirection.rtl, false) => ui.TextRange(
679681
start: start + _graphemeStarts[startIndex],
680682
end: start + _graphemeStarts[startIndex + 1],
681683
),
682-
(ui.TextDirection.ltr, false) ||
683-
(ui.TextDirection.rtl, true) => ui.TextRange(
684+
(ui.TextDirection.ltr, false) || (ui.TextDirection.rtl, true) => ui.TextRange(
684685
start: start + _graphemeStarts[endIndex - 1],
685686
end: start + _graphemeStarts[endIndex],
686687
),
@@ -689,6 +690,8 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
689690
return ui.GlyphInfo(box.toRect(), range, box.direction);
690691
}
691692

693+
/// Returns the UTF-16 range of the character that encloses the code unit at
694+
/// the given offset.
692695
ui.TextRange? getCharacterRangeAt(int codeUnitOffset) {
693696
if (end == start) {
694697
return null;
@@ -710,8 +713,8 @@ mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
710713
return null;
711714
}
712715

713-
// There doesn't seem to be na easy way to get GlyphClusters using the web API.
714-
// Pretend each grapheme cluster corresponds to a glyph cluster.
716+
/// Returns the GlyphInfo of the character in the fragment that is closest to
717+
/// the given offset x.
715718
ui.GlyphInfo? getClosestCharacterBox(double x) {
716719
if (end == start) {
717720
return null;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -419,7 +419,7 @@ class TextLayoutService {
419419
return ui.TextPosition(offset: line.startIndex);
420420
}
421421

422-
ui.GlyphInfo? getClosestGlyphCluster(ui.Offset offset) {
422+
ui.GlyphInfo? getClosestGlyphInfo(ui.Offset offset) {
423423
return _findLineForY(offset.dy)
424424
?.closestFragmentAtOffset(offset.dx)
425425
?.getClosestCharacterBox(offset.dx);

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -208,16 +208,16 @@ class ParagraphLine {
208208
return buffer.toString();
209209
}
210210

211-
// Find the closest [LayoutFragment] to the given horizontal offset `dx`, that
211+
// Finds the closest [LayoutFragment] to the given horizontal offset `dx`, that
212212
// is not an [EllipsisFragment].
213213
LayoutFragment? closestFragmentAtOffset(double dx) {
214-
final List<LayoutFragment> fs = switch (fragments) {
214+
final List<LayoutFragment> fragments = switch (this.fragments) {
215215
[...final List<LayoutFragment> rest, EllipsisFragment()]
216216
|| final List<LayoutFragment> rest => rest,
217217
};
218218

219219
({LayoutFragment fragment, double distance})? closestFragment;
220-
for (final LayoutFragment fragment in fs) {
220+
for (final LayoutFragment fragment in fragments) {
221221
assert(fragment is! EllipsisFragment);
222222
final double distance;
223223
if (dx < fragment.left) {

lib/web_ui/skwasm/text/paragraph.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ SKWASM_EXPORT bool paragraph_getClosestGlyphInfoAtCoordinate(
7070
bool* booleanFlags) {
7171
Paragraph::GlyphInfo glyphInfo;
7272
if (!paragraph->getClosestUTF16GlyphInfoAt(offsetX, offsetY, &glyphInfo)) {
73+
delete glyphInfo;
7374
return false;
7475
}
7576
// This is more verbose than memcpying the whole struct but ideally we don't
@@ -90,6 +91,7 @@ SKWASM_EXPORT bool paragraph_getGlyphInfoAt(Paragraph* paragraph,
9091
bool* booleanFlags) {
9192
Paragraph::GlyphInfo glyphInfo;
9293
if (!paragraph->getGlyphInfoAtUTF16Offset(index, &glyphInfo)) {
94+
delete glyphInfo;
9395
return false;
9496
}
9597
std::memcpy(graphemeLayoutBounds, &glyphInfo.fGraphemeLayoutBounds,

lib/web_ui/test/canvaskit/text_test.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,7 @@ void testMain() {
183183
expect(paragraph.getGlyphInfoAt(200), isNull);
184184
});
185185

186-
test('Basic glyph metrics 2', () {
186+
test('Basic glyph metrics - hit test', () {
187187
const double fontSize = 10.0;
188188
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(CkParagraphStyle(
189189
fontSize: fontSize,

lib/web_ui/test/html/text_test.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,25 @@ Future<void> testMain() async {
136136
expect(paragraph.getGlyphInfoAt(200), isNull);
137137
});
138138

139+
test('Basic glyph metrics - hit test', () {
140+
const double fontSize = 10.0;
141+
final ParagraphBuilder builder = ParagraphBuilder(CkParagraphStyle(
142+
fontSize: fontSize,
143+
fontFamily: 'FlutterTest',
144+
))..addText('Test\nTest');
145+
final Paragraph paragraph = builder.build();
146+
paragraph.layout(const ParagraphConstraints(width: double.infinity));
147+
148+
final GlyphInfo? bottomRight = paragraph.getClosestGlyphInfoForOffset(const Offset(99.0, 99.0));
149+
final GlyphInfo? last = paragraph.getGlyphInfoAt(8);
150+
expect(bottomRight, equals(last));
151+
expect(bottomRight, isNot(paragraph.getGlyphInfoAt(0)));
152+
153+
expect(bottomRight?.graphemeClusterLayoutBounds, const Rect.fromLTWH(30, 10, 10, 10));
154+
expect(bottomRight?.graphemeClusterCodeUnitRange, const TextRange(start: 8, end: 9));
155+
expect(bottomRight?.writingDirection, TextDirection.ltr);
156+
});
157+
139158
test('Can disable rounding hack', () {
140159
if (!ParagraphBuilder.shouldDisableRoundingHack) {
141160
ParagraphBuilder.setDisableRoundingHack(true);

0 commit comments

Comments
 (0)