Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
2 changes: 2 additions & 0 deletions lib/ui/dart_ui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,8 @@ typedef CanvasPath Path;
V(Paragraph, computeLineMetrics, 1) \
V(Paragraph, didExceedMaxLines, 1) \
V(Paragraph, dispose, 1) \
V(Paragraph, getClosestGlyphInfo, 3) \
V(Paragraph, getGlyphInfoAt, 2) \
V(Paragraph, getLineBoundary, 2) \
V(Paragraph, getLineMetricsAt, 3) \
V(Paragraph, getLineNumberAt, 2) \
Expand Down
75 changes: 75 additions & 0 deletions lib/ui/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -1191,6 +1191,54 @@ class FontVariation {
String toString() => "FontVariation('$axis', $value)";
}

/// The measurements of a character (or a sequence of visually connected
/// characters) within a paragraph.
///
/// See also:
///
/// * [Paragraph.getGlyphInfoAt], which finds the [GlyphInfo] associated with
/// a code unit in the text.
/// * [Paragraph.getClosestGlyphInfoForOffset], which finds the [GlyphInfo] of
/// the glyph(s) onscreen that's closest to the given [Offset].
final class GlyphInfo {
GlyphInfo._(double left, double top, double right, double bottom, int graphemeStart, int graphemeEnd, bool isLTR)
: graphemeClusterLayoutBounds = Rect.fromLTRB(left, top, right, bottom),
graphemeClusterCodeUnitRange = TextRange(start: graphemeStart, end: graphemeEnd),
writingDirection = isLTR ? TextDirection.ltr : TextDirection.rtl;

/// The layout bounding rect of the associated character, in the paragraph's
/// coordinates.
///
/// This is **not** the tight bounding box that encloses the character's outline.
/// The vertical extent reported is derived from the font metrics (instead of
/// glyph metrics), and the horizontal extent is the horizontal advance of the
/// character.
final Rect graphemeClusterLayoutBounds;

/// The UTF-16 range of the associated character in the text.
final TextRange graphemeClusterCodeUnitRange;

/// The writing direction within the [GlyphInfo].
final TextDirection writingDirection;

@override
bool operator ==(Object other) {
if (identical(this, other)) {
return true;
}
return other is GlyphInfo
&& graphemeClusterLayoutBounds == other.graphemeClusterLayoutBounds
&& graphemeClusterCodeUnitRange == other.graphemeClusterCodeUnitRange
&& writingDirection == other.writingDirection;
}

@override
int get hashCode => Object.hash(graphemeClusterLayoutBounds, graphemeClusterCodeUnitRange, writingDirection);

@override
String toString() => 'Glyph($graphemeClusterLayoutBounds, textRange: $graphemeClusterCodeUnitRange, direction: $writingDirection)';
}

/// Whether and how to align text horizontally.
// The order of this enum must match the order of the values in RenderStyleConstants.h's ETextAlign.
enum TextAlign {
Expand Down Expand Up @@ -2971,6 +3019,23 @@ abstract class Paragraph {
/// Returns the text position closest to the given offset.
TextPosition getPositionForOffset(Offset offset);

/// Returns the [GlyphInfo] of the glyph closest to the given `offset` in the
/// paragraph coordinate system, or null if the glyph is not in the visible
/// range.
///
/// This method first finds the line closest to `offset.dy`, and then returns
/// the [GlyphInfo] of the closest glyph(s) within that line.
///
/// This method can be used to implement per-glyph hit-testing. The returned
/// [GlyphInfo] can help determine whether the given `offset` directly hits a
/// glyph in the paragraph.
GlyphInfo? getClosestGlyphInfoForOffset(Offset offset);

/// Returns the [GlyphInfo] located at the given UTF-16 `codeUnitOffset` in
/// the paragraph, or null if the given `codeUnitOffset` is out of the visible
/// lines or is ellipsized.
GlyphInfo? getGlyphInfoAt(int codeUnitOffset);

/// Returns the [TextRange] of the word at the given [TextPosition].
///
/// Characters not part of a word, such as spaces, symbols, and punctuation,
Expand Down Expand Up @@ -3135,6 +3200,16 @@ base class _NativeParagraph extends NativeFieldWrapperClass1 implements Paragrap
@Native<Handle Function(Pointer<Void>, Double, Double)>(symbol: 'Paragraph::getPositionForOffset')
external List<int> _getPositionForOffset(double dx, double dy);

@override
GlyphInfo? getGlyphInfoAt(int codeUnitOffset) => _getGlyphInfoAt(codeUnitOffset, GlyphInfo._);
@Native<Handle Function(Pointer<Void>, Uint32, Handle)>(symbol: 'Paragraph::getGlyphInfoAt')
external GlyphInfo? _getGlyphInfoAt(int codeUnitOffset, Function constructor);

@override
GlyphInfo? getClosestGlyphInfoForOffset(Offset offset) => _getClosestGlyphInfoForOffset(offset.dx, offset.dy, GlyphInfo._);
@Native<Handle Function(Pointer<Void>, Double, Double, Handle)>(symbol: 'Paragraph::getClosestGlyphInfo')
external GlyphInfo? _getClosestGlyphInfoForOffset(double dx, double dy, Function constructor);

@override
TextRange getWordBoundary(TextPosition position) {
final int characterPosition;
Expand Down
47 changes: 45 additions & 2 deletions lib/ui/text/paragraph.cc
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,51 @@ Dart_Handle Paragraph::getPositionForOffset(double dx, double dy) {
return tonic::DartConverter<decltype(result)>::ToDart(result);
}

Dart_Handle Paragraph::getWordBoundary(unsigned offset) {
txt::Paragraph::Range<size_t> point = m_paragraph_->GetWordBoundary(offset);
Dart_Handle glyphInfoFrom(
Dart_Handle constructor,
const skia::textlayout::Paragraph::GlyphInfo& glyphInfo) {
std::array<Dart_Handle, 7> arguments = {
Dart_NewDouble(glyphInfo.fGraphemeLayoutBounds.fLeft),
Dart_NewDouble(glyphInfo.fGraphemeLayoutBounds.fTop),
Dart_NewDouble(glyphInfo.fGraphemeLayoutBounds.fRight),
Dart_NewDouble(glyphInfo.fGraphemeLayoutBounds.fBottom),
Dart_NewInteger(glyphInfo.fGraphemeClusterTextRange.start),
Dart_NewInteger(glyphInfo.fGraphemeClusterTextRange.end),
Dart_NewBoolean(glyphInfo.fDirection ==
skia::textlayout::TextDirection::kLtr),
};
return Dart_InvokeClosure(constructor, arguments.size(), arguments.data());
}

Dart_Handle Paragraph::getGlyphInfoAt(unsigned utf16Offset,
Dart_Handle constructor) const {
skia::textlayout::Paragraph::GlyphInfo glyphInfo;
const bool found = m_paragraph_->GetGlyphInfoAt(utf16Offset, &glyphInfo);
if (!found) {
return Dart_Null();
}
Dart_Handle handle = glyphInfoFrom(constructor, glyphInfo);
tonic::CheckAndHandleError(handle);
return handle;
}

Dart_Handle Paragraph::getClosestGlyphInfo(double dx,
double dy,
Dart_Handle constructor) const {
skia::textlayout::Paragraph::GlyphInfo glyphInfo;
const bool found =
m_paragraph_->GetClosestGlyphInfoAtCoordinate(dx, dy, &glyphInfo);
if (!found) {
return Dart_Null();
}
Dart_Handle handle = glyphInfoFrom(constructor, glyphInfo);
tonic::CheckAndHandleError(handle);
return handle;
}

Dart_Handle Paragraph::getWordBoundary(unsigned utf16Offset) {
txt::Paragraph::Range<size_t> point =
m_paragraph_->GetWordBoundary(utf16Offset);
std::vector<size_t> result = {point.start, point.end};
return tonic::DartConverter<decltype(result)>::ToDart(result);
}
Expand Down
5 changes: 5 additions & 0 deletions lib/ui/text/paragraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ class Paragraph : public RefCountedDartWrappable<Paragraph> {
unsigned boxWidthStyle);
tonic::Float32List getRectsForPlaceholders();
Dart_Handle getPositionForOffset(double dx, double dy);
Dart_Handle getGlyphInfoAt(unsigned utf16Offset,
Dart_Handle constructor) const;
Dart_Handle getClosestGlyphInfo(double dx,
double dy,
Dart_Handle constructor) const;
Dart_Handle getWordBoundary(unsigned offset);
Dart_Handle getLineBoundary(unsigned offset);
tonic::Float64List computeLineMetrics() const;
Expand Down
31 changes: 31 additions & 0 deletions lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3193,6 +3193,29 @@ extension SkLineMetricsExtension on SkLineMetrics {
double get lineNumber => _lineNumber.toDartDouble;
}

@JS()
@anonymous
@staticInterop
class SkGlyphClusterInfo {}

extension SkGlyphClusterInfoExtension on SkGlyphClusterInfo {
@JS('graphemeLayoutBounds')
external JSArray get _bounds;

@JS('dir')
external SkTextDirection get _direction;

@JS('graphemeClusterTextRange')
external SkTextRange get _textRange;

ui.GlyphInfo get _glyphInfo {
final List<JSNumber> list = _bounds.toDart.cast<JSNumber>();
final ui.Rect bounds = ui.Rect.fromLTRB(list[0].toDartDouble, list[1].toDartDouble, list[2].toDartDouble, list[3].toDartDouble);
final ui.TextRange textRange = ui.TextRange(start: _textRange.start.toInt(), end: _textRange.end.toInt());
return ui.GlyphInfo(bounds, textRange, ui.TextDirection.values[_direction.value.toInt()]);
}
}

@JS()
@anonymous
@staticInterop
Expand Down Expand Up @@ -3295,6 +3318,14 @@ extension SkParagraphExtension on SkParagraph {
double y,
) => _getGlyphPositionAtCoordinate(x.toJS, y.toJS);

@JS('getGlyphInfoAt')
external SkGlyphClusterInfo? _getGlyphInfoAt(JSNumber position);
ui.GlyphInfo? getGlyphInfoAt(double position) => _getGlyphInfoAt(position.toJS)?._glyphInfo;

@JS('getClosestGlyphInfoAtCoordinate')
external SkGlyphClusterInfo? _getClosestGlyphInfoAtCoordinate(JSNumber x, JSNumber y);
ui.GlyphInfo? getClosestGlyphInfoAt(double x, double y) => _getClosestGlyphInfoAtCoordinate(x.toJS, y.toJS)?._glyphInfo;

@JS('getWordBoundary')
external SkTextRange _getWordBoundary(JSNumber position);
SkTextRange getWordBoundary(double position) =>
Expand Down
12 changes: 12 additions & 0 deletions lib/web_ui/lib/src/engine/canvaskit/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -658,6 +658,18 @@ class CkParagraph implements ui.Paragraph {
return fromPositionWithAffinity(positionWithAffinity);
}

@override
ui.GlyphInfo? getClosestGlyphInfoForOffset(ui.Offset offset) {
assert(!_disposed, 'Paragraph has been disposed.');
return skiaObject.getClosestGlyphInfoAt(offset.dx, offset.dy);
}

@override
ui.GlyphInfo? getGlyphInfoAt(int codeUnitOffset) {
assert(!_disposed, 'Paragraph has been disposed.');
return skiaObject.getGlyphInfoAt(codeUnitOffset.toDouble());
}

@override
ui.TextRange getWordBoundary(ui.TextPosition position) {
assert(!_disposed, 'Paragraph has been disposed.');
Expand Down
32 changes: 32 additions & 0 deletions lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,38 @@ class SkwasmParagraph extends SkwasmObjectWrapper<RawParagraph> implements ui.Pa
);
});

@override
ui.GlyphInfo? getGlyphInfoAt(int codeUnitOffset) {
return withStackScope((StackScope scope) {
final Pointer<Float> outRect = scope.allocFloatArray(4);
final Pointer<Uint32> outRange = scope.allocUint32Array(2);
final Pointer<Bool> outBooleanFlags = scope.allocBoolArray(1);
return paragraphGetGlyphInfoAt(handle, codeUnitOffset, outRect, outRange, outBooleanFlags)
? ui.GlyphInfo(
scope.convertRectFromNative(outRect),
ui.TextRange(start: outRange[0], end: outRange[1]),
outBooleanFlags[0] ? ui.TextDirection.ltr : ui.TextDirection.rtl,
)
: null;
});
}

@override
ui.GlyphInfo? getClosestGlyphInfoForOffset(ui.Offset offset) {
return withStackScope((StackScope scope) {
final Pointer<Float> outRect = scope.allocFloatArray(4);
final Pointer<Uint32> outRange = scope.allocUint32Array(2);
final Pointer<Bool> outBooleanFlags = scope.allocBoolArray(1);
return paragraphGetClosestGlyphInfoAtCoordinate(handle, offset.dx, offset.dy, outRect, outRange, outBooleanFlags)
? ui.GlyphInfo(
scope.convertRectFromNative(outRect),
ui.TextRange(start: outRange[0], end: outRange[1]),
outBooleanFlags[0] ? ui.TextDirection.ltr : ui.TextDirection.rtl,
)
: null;
});
}

@override
ui.TextRange getWordBoundary(ui.TextPosition position) => withStackScope((StackScope scope) {
final Pointer<Int32> outRange = scope.allocInt32Array(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,11 @@ class StackScope {
return pointer;
}

Pointer<Bool> allocBoolArray(int count) {
final int length = count * sizeOf<Bool>();
return stackAlloc(length).cast<Bool>();
}

Pointer<Int8> allocInt8Array(int count) {
final int length = count * sizeOf<Int8>();
return stackAlloc(length).cast<Int8>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,24 @@ external int paragraphGetPositionForOffset(
Pointer<Int32> outAffinity,
);

@Native<Bool Function(ParagraphHandle, Float, Float, RawRect, Pointer<Uint32>, Pointer<Bool>)>(symbol: 'paragraph_getClosestGlyphInfoAtCoordinate')
external bool paragraphGetClosestGlyphInfoAtCoordinate(
ParagraphHandle handle,
double offsetX, double offsetY,
RawRect graphemeLayoutBounds, // 4 floats, [LTRB]
Pointer<Uint32> graphemeCodeUnitRange, // 2 `size_t`s, start and end.
Pointer<Bool> booleanFlags, // 1 boolean, isLTR.
);

@Native<Bool Function(ParagraphHandle, Uint32, RawRect, Pointer<Uint32>, Pointer<Bool>)>(symbol: 'paragraph_getGlyphInfoAt')
external bool paragraphGetGlyphInfoAt(
ParagraphHandle handle,
int codeUnitOffset,
RawRect graphemeLayoutBounds, // 4 floats, [LTRB]
Pointer<Uint32> graphemeCodeUnitRange, // 2 `size_t`s, start and end.
Pointer<Bool> booleanFlags, // 1 boolean, isLTR.
);

@Native<Void Function(
ParagraphHandle,
UnsignedInt,
Expand Down
40 changes: 38 additions & 2 deletions lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,35 @@ class CanvasParagraph implements ui.Paragraph {
return _layoutService.getPositionForOffset(offset);
}

@override
ui.GlyphInfo? getClosestGlyphInfoForOffset(ui.Offset offset) => _layoutService.getClosestGlyphInfo(offset);

@override
ui.GlyphInfo? getGlyphInfoAt(int codeUnitOffset) {
final int? lineNumber = _findLine(codeUnitOffset, 0, numberOfLines);
if (lineNumber == null) {
return null;
}
final ParagraphLine line = lines[lineNumber];
final ui.TextRange? range = line.getCharacterRangeAt(codeUnitOffset);
if (range == null) {
return null;
}
assert(line.overlapsWith(range.start, range.end));
for (final LayoutFragment fragment in line.fragments) {
if (fragment.overlapsWith(range.start, range.end)) {
// If the grapheme cluster is split into multiple fragments (which really
// shouldn't happen but currently if they are in different TextSpans they
// don't combine), use the layout box of the first base character as its
// layout box has a better chance to be not that far-off.
final ui.TextBox textBox = fragment.toTextBox(start: range.start, end: range.end);
return ui.GlyphInfo(textBox.toRect(), range, textBox.direction);
}
}
assert(false, 'This should not be reachable.');
return null;
}

@override
ui.TextRange getWordBoundary(ui.TextPosition position) {
final int characterPosition;
Expand Down Expand Up @@ -249,11 +278,18 @@ class CanvasParagraph implements ui.Paragraph {
int? getLineNumberAt(int codeUnitOffset) => _findLine(codeUnitOffset, 0, lines.length);

int? _findLine(int codeUnitOffset, int startLine, int endLine) {
if (endLine <= startLine || codeUnitOffset < lines[startLine].startIndex || lines[endLine - 1].endIndex <= codeUnitOffset) {
assert(endLine <= lines.length);
final bool isOutOfBounds = endLine <= startLine
|| codeUnitOffset < lines[startLine].startIndex
|| (endLine < numberOfLines && lines[endLine].startIndex <= codeUnitOffset);
if (isOutOfBounds) {
return null;
}

if (endLine == startLine + 1) {
return startLine;
assert(lines[startLine].startIndex <= codeUnitOffset);
assert(endLine == numberOfLines || codeUnitOffset < lines[endLine].startIndex);
return codeUnitOffset >= lines[startLine].visibleEndIndex ? null : startLine;
}
// endLine >= startLine + 2 thus we have
// startLine + 1 <= midIndex <= endLine - 1
Expand Down
Loading