Expose more methods on `ui.Paragraph`: lines (#46125)
[C++, Objective-C, Java style guides]: https://github.com/flutter/engine/blob/main/CONTRIBUTING.md#style
diff --git a/lib/ui/dart_ui.cc b/lib/ui/dart_ui.cc
index 310de11..6b048b4 100644
--- a/lib/ui/dart_ui.cc
+++ b/lib/ui/dart_ui.cc
@@ -218,6 +218,9 @@
V(Paragraph, didExceedMaxLines, 1) \
V(Paragraph, dispose, 1) \
V(Paragraph, getLineBoundary, 2) \
+ V(Paragraph, getLineMetricsAt, 3) \
+ V(Paragraph, getLineNumberAt, 2) \
+ V(Paragraph, getNumberOfLines, 1) \
V(Paragraph, getPositionForOffset, 3) \
V(Paragraph, getRectsForPlaceholders, 1) \
V(Paragraph, getRectsForRange, 5) \
diff --git a/lib/ui/text.dart b/lib/ui/text.dart
index 8bf5a8c..276c326 100644
--- a/lib/ui/text.dart
+++ b/lib/ui/text.dart
@@ -2772,6 +2772,18 @@
required this.lineNumber,
});
+ LineMetrics._(
+ this.hardBreak,
+ this.ascent,
+ this.descent,
+ this.unscaledAscent,
+ this.height,
+ this.width,
+ this.left,
+ this.baseline,
+ this.lineNumber,
+ );
+
/// True if this line ends with an explicit line break (e.g. '\n') or is the end
/// of the paragraph. False otherwise.
final bool hardBreak;
@@ -2992,6 +3004,32 @@
/// to repeatedly call this. Instead, cache the results.
List<LineMetrics> computeLineMetrics();
+ /// Returns the [LineMetrics] for the line at `lineNumber`, or null if the
+ /// given `lineNumber` is greater than or equal to [numberOfLines].
+ LineMetrics? getLineMetricsAt(int lineNumber);
+
+ /// The total number of visible lines in the paragraph.
+ ///
+ /// Returns a non-negative number. If `maxLines` is non-null, the value of
+ /// [numberOfLines] never exceeds `maxLines`.
+ int get numberOfLines;
+
+ /// Returns the line number of the line that contains the code unit that
+ /// `codeUnitOffset` points to.
+ ///
+ /// This method returns null if the given `codeUnitOffset` is out of bounds, or
+ /// is logically after the last visible codepoint. This includes the case where
+ /// its codepoint belongs to a visible line, but the text layout library
+ /// replaced it with an ellipsis.
+ ///
+ /// If the target code unit points to a control character that introduces
+ /// mandatory line breaks (most notably the line feed character `LF`, typically
+ /// represented in strings as the escape sequence "\n"), to conform to
+ /// [the unicode rules](https://unicode.org/reports/tr14/#LB4), the control
+ /// character itself is always considered to be at the end of "current" line
+ /// rather than the beginning of the new line.
+ int? getLineNumberAt(int codeUnitOffset);
+
/// Release the resources used by this object. The object is no longer usable
/// after this method is called.
void dispose();
@@ -3170,6 +3208,23 @@
external Float64List _computeLineMetrics();
@override
+ LineMetrics? getLineMetricsAt(int lineNumber) => _getLineMetricsAt(lineNumber, LineMetrics._);
+ @Native<Handle Function(Pointer<Void>, Uint32, Handle)>(symbol: 'Paragraph::getLineMetricsAt')
+ external LineMetrics? _getLineMetricsAt(int lineNumber, Function constructor);
+
+ @override
+ @Native<Uint32 Function(Pointer<Void>)>(symbol: 'Paragraph::getNumberOfLines')
+ external int get numberOfLines;
+
+ @override
+ int? getLineNumberAt(int codeUnitOffset) {
+ final int lineNumber = _getLineNumber(codeUnitOffset);
+ return lineNumber < 0 ? null : lineNumber;
+ }
+ @Native<Int32 Function(Pointer<Void>, Uint32)>(symbol: 'Paragraph::getLineNumberAt')
+ external int _getLineNumber(int codeUnitOffset);
+
+ @override
void dispose() {
assert(!_disposed);
assert(() {
diff --git a/lib/ui/text/paragraph.cc b/lib/ui/text/paragraph.cc
index 624ed14..fedf84e 100644
--- a/lib/ui/text/paragraph.cc
+++ b/lib/ui/text/paragraph.cc
@@ -8,10 +8,14 @@
#include "flutter/common/task_runners.h"
#include "flutter/fml/logging.h"
#include "flutter/fml/task_runner.h"
+#include "third_party/dart/runtime/include/dart_api.h"
+#include "third_party/skia/modules/skparagraph/include/DartTypes.h"
+#include "third_party/skia/modules/skparagraph/include/Paragraph.h"
#include "third_party/tonic/converter/dart_converter.h"
#include "third_party/tonic/dart_args.h"
#include "third_party/tonic/dart_binding_macros.h"
#include "third_party/tonic/dart_library_natives.h"
+#include "third_party/tonic/logging/dart_invoke.h"
namespace flutter {
@@ -122,12 +126,12 @@
return tonic::DartConverter<decltype(result)>::ToDart(result);
}
-Dart_Handle Paragraph::getLineBoundary(unsigned offset) {
+Dart_Handle Paragraph::getLineBoundary(unsigned utf16Offset) {
std::vector<txt::LineMetrics> metrics = m_paragraph->GetLineMetrics();
int line_start = -1;
int line_end = -1;
for (txt::LineMetrics& line : metrics) {
- if (offset >= line.start_index && offset <= line.end_index) {
+ if (utf16Offset >= line.start_index && utf16Offset <= line.end_index) {
line_start = line.start_index;
line_end = line.end_index;
break;
@@ -137,7 +141,7 @@
return tonic::DartConverter<decltype(result)>::ToDart(result);
}
-tonic::Float64List Paragraph::computeLineMetrics() {
+tonic::Float64List Paragraph::computeLineMetrics() const {
std::vector<txt::LineMetrics> metrics = m_paragraph->GetLineMetrics();
// Layout:
@@ -165,6 +169,42 @@
return result;
}
+Dart_Handle Paragraph::getLineMetricsAt(int lineNumber,
+ Dart_Handle constructor) const {
+ skia::textlayout::LineMetrics line;
+ const bool found = m_paragraph->GetLineMetricsAt(lineNumber, &line);
+ if (!found) {
+ return Dart_Null();
+ }
+ std::array<Dart_Handle, 9> arguments = {
+ Dart_NewBoolean(line.fHardBreak),
+ Dart_NewDouble(line.fAscent),
+ Dart_NewDouble(line.fDescent),
+ Dart_NewDouble(line.fUnscaledAscent),
+ // We add then round to get the height. The
+ // definition of height here is different
+ // than the one in LibTxt.
+ Dart_NewDouble(round(line.fAscent + line.fDescent)),
+ Dart_NewDouble(line.fWidth),
+ Dart_NewDouble(line.fLeft),
+ Dart_NewDouble(line.fBaseline),
+ Dart_NewInteger(line.fLineNumber),
+ };
+
+ Dart_Handle handle =
+ Dart_InvokeClosure(constructor, arguments.size(), arguments.data());
+ tonic::CheckAndHandleError(handle);
+ return handle;
+}
+
+size_t Paragraph::getNumberOfLines() const {
+ return m_paragraph->GetNumberOfLines();
+}
+
+int Paragraph::getLineNumberAt(size_t utf16Offset) const {
+ return m_paragraph->GetLineNumberAt(utf16Offset);
+}
+
void Paragraph::dispose() {
m_paragraph.reset();
ClearDartWrapper();
diff --git a/lib/ui/text/paragraph.h b/lib/ui/text/paragraph.h
index 1a5d821..311c33a 100644
--- a/lib/ui/text/paragraph.h
+++ b/lib/ui/text/paragraph.h
@@ -47,7 +47,10 @@
Dart_Handle getPositionForOffset(double dx, double dy);
Dart_Handle getWordBoundary(unsigned offset);
Dart_Handle getLineBoundary(unsigned offset);
- tonic::Float64List computeLineMetrics();
+ tonic::Float64List computeLineMetrics() const;
+ Dart_Handle getLineMetricsAt(int lineNumber, Dart_Handle constructor) const;
+ size_t getNumberOfLines() const;
+ int getLineNumberAt(size_t utf16Offset) const;
void dispose();
diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
index dff531d..bc98818 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
@@ -3237,6 +3237,18 @@
List<SkLineMetrics> getLineMetrics() =>
_getLineMetrics().toDart.cast<SkLineMetrics>();
+ @JS('getLineMetricsAt')
+ external SkLineMetrics? _getLineMetricsAt(JSNumber index);
+ SkLineMetrics? getLineMetricsAt(double index) => _getLineMetricsAt(index.toJS);
+
+ @JS('getNumberOfLines')
+ external JSNumber _getNumberOfLines();
+ double getNumberOfLines() => _getNumberOfLines().toDartDouble;
+
+ @JS('getLineNumberAt')
+ external JSNumber _getLineNumberAt(JSNumber index);
+ double getLineNumberAt(double index) => _getLineNumberAt(index.toJS).toDartDouble;
+
@JS('getLongestLine')
external JSNumber _getLongestLine();
double getLongestLine() => _getLongestLine().toDartDouble;
diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart
index 0948916..6284ffe 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/text.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart
@@ -728,6 +728,26 @@
return result;
}
+ @override
+ ui.LineMetrics? getLineMetricsAt(int lineNumber) {
+ assert(!_disposed, 'Paragraph has been disposed.');
+ final SkLineMetrics? metrics = skiaObject.getLineMetricsAt(lineNumber.toDouble());
+ return metrics == null ? null : CkLineMetrics._(metrics);
+ }
+
+ @override
+ int get numberOfLines {
+ assert(!_disposed, 'Paragraph has been disposed.');
+ return skiaObject.getNumberOfLines().toInt();
+ }
+
+ @override
+ int? getLineNumberAt(int codeUnitOffset) {
+ assert(!_disposed, 'Paragraph has been disposed.');
+ final int lineNumber = skiaObject.getLineNumberAt(codeUnitOffset.toDouble()).toInt();
+ return lineNumber >= 0 ? lineNumber : null;
+ }
+
bool _disposed = false;
@override
diff --git a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart
index c00c1bc..af721f7 100644
--- a/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart
+++ b/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/paragraph.dart
@@ -102,6 +102,15 @@
bool get didExceedMaxLines => paragraphGetDidExceedMaxLines(handle);
@override
+ int get numberOfLines => paragraphGetLineCount(handle);
+
+ @override
+ int? getLineNumberAt(int codeUnitOffset) {
+ final int lineNumber = paragraphGetLineNumberAt(handle, codeUnitOffset);
+ return lineNumber >= 0 ? lineNumber : null;
+ }
+
+ @override
void layout(ui.ParagraphConstraints constraints) {
paragraphLayout(handle, constraints.width);
if (!_hasCheckedForMissingCodePoints) {
@@ -214,6 +223,12 @@
(int index) => SkwasmLineMetrics._(paragraphGetLineMetricsAtIndex(handle, index))
);
}
+
+ @override
+ ui.LineMetrics? getLineMetricsAt(int index) {
+ final LineMetricsHandle lineMetrics = paragraphGetLineMetricsAtIndex(handle, index);
+ return lineMetrics == nullptr ? SkwasmLineMetrics._(lineMetrics) : null;
+ }
}
void withScopedFontList(
diff --git a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
index f527d57..c749a85 100644
--- a/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
+++ b/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
@@ -40,7 +40,7 @@
final EngineParagraphStyle paragraphStyle;
/// The full textual content of the paragraph.
- late String plainText;
+ final String plainText;
/// Whether this paragraph can be drawn on a bitmap canvas.
///
@@ -221,17 +221,12 @@
@override
ui.TextRange getLineBoundary(ui.TextPosition position) {
- final int index = position.offset;
-
- int i;
- for (i = 0; i < lines.length - 1; i++) {
- final ParagraphLine line = lines[i];
- if (index >= line.startIndex && index < line.endIndex) {
- break;
- }
+ if (lines.isEmpty) {
+ return ui.TextRange.empty;
}
-
- final ParagraphLine line = lines[i];
+ final int? lineNumber = getLineNumberAt(position.offset);
+ // Fallback to the last line for backward compatibility.
+ final ParagraphLine line = lineNumber != null ? lines[lineNumber] : lines.last;
return ui.TextRange(start: line.startIndex, end: line.endIndex - line.trailingNewlines);
}
@@ -240,6 +235,32 @@
return lines.map((ParagraphLine line) => line.lineMetrics).toList();
}
+ @override
+ EngineLineMetrics? getLineMetricsAt(int lineNumber) {
+ return 0 <= lineNumber && lineNumber < lines.length
+ ? lines[lineNumber].lineMetrics
+ : null;
+ }
+
+ @override
+ int get numberOfLines => lines.length;
+
+ @override
+ 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) {
+ return null;
+ }
+ if (endLine == startLine + 1) {
+ return startLine;
+ }
+ // endLine >= startLine + 2 thus we have
+ // startLine + 1 <= midIndex <= endLine - 1
+ final int midIndex = (startLine + endLine) ~/ 2;
+ return _findLine(codeUnitOffset, midIndex, endLine) ?? _findLine(codeUnitOffset, startLine, midIndex);
+ }
+
bool _disposed = false;
@override
diff --git a/lib/web_ui/lib/src/engine/text/layout_service.dart b/lib/web_ui/lib/src/engine/text/layout_service.dart
index ae46ae6..d03a70b 100644
--- a/lib/web_ui/lib/src/engine/text/layout_service.dart
+++ b/lib/web_ui/lib/src/engine/text/layout_service.dart
@@ -390,7 +390,10 @@
// it possible to do hit testing. Once we find the box, we look inside that
// box to find where exactly the `offset` is located.
- final ParagraphLine line = _findLineForY(offset.dy);
+ final ParagraphLine? line = _findLineForY(offset.dy);
+ if (line == null) {
+ return const ui.TextPosition(offset: 0);
+ }
// [offset] is to the left of the line.
if (offset.dx <= line.left) {
return ui.TextPosition(
@@ -416,7 +419,10 @@
return ui.TextPosition(offset: line.startIndex);
}
- ParagraphLine _findLineForY(double y) {
+ ParagraphLine? _findLineForY(double y) {
+ if (lines.isEmpty) {
+ return null;
+ }
// 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.
diff --git a/lib/web_ui/lib/text.dart b/lib/web_ui/lib/text.dart
index 5d39948..ba09ee5 100644
--- a/lib/web_ui/lib/text.dart
+++ b/lib/web_ui/lib/text.dart
@@ -695,6 +695,9 @@
TextRange getLineBoundary(TextPosition position);
List<TextBox> getBoxesForPlaceholders();
List<LineMetrics> computeLineMetrics();
+ LineMetrics? getLineMetricsAt(int lineNumber);
+ int get numberOfLines;
+ int? getLineNumberAt(int codeUnitOffset);
void dispose();
bool get debugDisposed;
}
diff --git a/lib/web_ui/skwasm/text/paragraph.cpp b/lib/web_ui/skwasm/text/paragraph.cpp
index dcd1cd2..a44baf4 100644
--- a/lib/web_ui/skwasm/text/paragraph.cpp
+++ b/lib/web_ui/skwasm/text/paragraph.cpp
@@ -74,14 +74,18 @@
SKWASM_EXPORT int paragraph_getLineNumberAt(Paragraph* paragraph,
size_t characterIndex) {
- return paragraph->getLineNumberAt(characterIndex);
+ return paragraph->getLineNumberAtUTF16Offset(characterIndex);
}
SKWASM_EXPORT LineMetrics* paragraph_getLineMetricsAtIndex(Paragraph* paragraph,
- size_t index) {
+ size_t lineNumber) {
auto metrics = new LineMetrics();
- paragraph->getLineMetricsAt(index, metrics);
- return metrics;
+ if (paragraph->getLineMetricsAt(lineNumber, metrics)) {
+ return metrics;
+ } else {
+ delete metrics;
+ return nullptr;
+ }
}
struct TextBoxList {
diff --git a/lib/web_ui/test/canvaskit/text_test.dart b/lib/web_ui/test/canvaskit/text_test.dart
index 2446145..2c788cf 100644
--- a/lib/web_ui/test/canvaskit/text_test.dart
+++ b/lib/web_ui/test/canvaskit/text_test.dart
@@ -124,6 +124,43 @@
});
});
+ test('empty paragraph', () {
+ const double fontSize = 10.0;
+ final ui.Paragraph paragraph = ui.ParagraphBuilder(CkParagraphStyle(
+ fontSize: fontSize,
+ )).build();
+ paragraph.layout(const ui.ParagraphConstraints(width: double.infinity));
+
+ expect(paragraph.getLineMetricsAt(0), isNull);
+ expect(paragraph.numberOfLines, 0);
+ expect(paragraph.getLineNumberAt(0), isNull);
+ });
+
+ test('Basic line related metrics', () {
+ const double fontSize = 10;
+ final ui.ParagraphBuilder builder = ui.ParagraphBuilder(CkParagraphStyle(
+ fontStyle: ui.FontStyle.normal,
+ fontWeight: ui.FontWeight.normal,
+ fontSize: fontSize,
+ maxLines: 1,
+ ellipsis: 'BBB',
+ ))..addText('A' * 100);
+ final ui.Paragraph paragraph = builder.build();
+ paragraph.layout(const ui.ParagraphConstraints(width: 100.0));
+
+ expect(paragraph.numberOfLines, 1);
+
+ expect(paragraph.getLineMetricsAt(-1), isNull);
+ expect(paragraph.getLineMetricsAt(0), isNotNull);
+ expect(paragraph.getLineMetricsAt(1), isNull);
+
+ expect(paragraph.getLineNumberAt(-1), isNull);
+ expect(paragraph.getLineNumberAt(0), 0);
+ expect(paragraph.getLineNumberAt(6), 0);
+ // The last 3 characters on the first line are ellipsized with BBB.
+ expect(paragraph.getLineMetricsAt(7), isNull);
+ });
+
test('rounding hack disabled by default', () {
expect(ui.ParagraphBuilder.shouldDisableRoundingHack, isTrue);
diff --git a/lib/web_ui/test/html/text_test.dart b/lib/web_ui/test/html/text_test.dart
index d9c7780..fc0db1f 100644
--- a/lib/web_ui/test/html/text_test.dart
+++ b/lib/web_ui/test/html/text_test.dart
@@ -79,10 +79,6 @@
expect(paragraph.height, fontSize * 2.0); // because it wraps
expect(paragraph.width, fontSize * 5.0);
expect(paragraph.minIntrinsicWidth, fontSize * 4.0);
-
- // TODO(yjbanov): due to https://github.com/flutter/flutter/issues/21965
- // Flutter reports a different number. Ours is correct
- // though.
expect(paragraph.maxIntrinsicWidth, fontSize * 9.0);
expect(paragraph.alphabeticBaseline, fontSize * .8);
expect(
@@ -94,6 +90,31 @@
}
});
+ test('Basic line related metrics', () {
+ const double fontSize = 10;
+ final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
+ fontStyle: FontStyle.normal,
+ fontWeight: FontWeight.normal,
+ fontSize: fontSize,
+ maxLines: 1,
+ ellipsis: 'BBB',
+ ))..addText('A' * 100);
+ final Paragraph paragraph = builder.build();
+ paragraph.layout(const ParagraphConstraints(width: 100.0));
+
+ expect(paragraph.numberOfLines, 1);
+
+ expect(paragraph.getLineMetricsAt(-1), isNull);
+ expect(paragraph.getLineMetricsAt(0), isNotNull);
+ expect(paragraph.getLineMetricsAt(1), isNull);
+
+ expect(paragraph.getLineNumberAt(-1), isNull);
+ expect(paragraph.getLineNumberAt(0), 0);
+ expect(paragraph.getLineNumberAt(6), 0);
+ // The last 3 characters on the first line are ellipsized with BBB.
+ expect(paragraph.getLineNumberAt(7), isNull);
+ });
+
test('Can disable rounding hack', () {
if (!ParagraphBuilder.shouldDisableRoundingHack) {
ParagraphBuilder.setDisableRoundingHack(true);
diff --git a/testing/dart/paragraph_test.dart b/testing/dart/paragraph_test.dart
index 771cf98..4af61bb 100644
--- a/testing/dart/paragraph_test.dart
+++ b/testing/dart/paragraph_test.dart
@@ -219,6 +219,74 @@
expect(line.end, 10);
});
+ test('getLineMetricsAt', () {
+ const double fontSize = 10.0;
+ final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
+ fontSize: fontSize,
+ textDirection: TextDirection.rtl,
+ height: 2.0,
+ ));
+ builder.addText('Test\npppp');
+ final Paragraph paragraph = builder.build();
+ paragraph.layout(const ParagraphConstraints(width: 100.0));
+ final LineMetrics? line = paragraph.getLineMetricsAt(1);
+ expect(line?.hardBreak, isTrue);
+ expect(line?.ascent, 15.0);
+ expect(line?.descent, 5.0);
+ expect(line?.height, 20.0);
+ expect(line?.width, 4 * 10.0);
+ expect(line?.left, 100.0 - 40.0);
+ expect(line?.baseline, 20.0 + 15.0);
+ expect(line?.lineNumber, 1);
+ });
+
+ test('line number', () {
+ const double fontSize = 10.0;
+ final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(fontSize: fontSize));
+ builder.addText('Test\n\nTest');
+ final Paragraph paragraph = builder.build();
+ paragraph.layout(const ParagraphConstraints(width: 100.0));
+ expect(paragraph.numberOfLines, 3);
+ expect(paragraph.getLineNumberAt(4), 0); // first LF
+ expect(paragraph.getLineNumberAt(5), 1); // second LF
+ expect(paragraph.getLineNumberAt(6), 2); // "T" in the second "Test"
+ });
+
+ test('empty paragraph', () {
+ const double fontSize = 10.0;
+ final Paragraph paragraph = ParagraphBuilder(ParagraphStyle(
+ fontSize: fontSize,
+ )).build();
+ paragraph.layout(const ParagraphConstraints(width: double.infinity));
+
+ expect(paragraph.getLineMetricsAt(0), isNull);
+ expect(paragraph.numberOfLines, 0);
+ expect(paragraph.getLineNumberAt(0), isNull);
+ });
+
+ test('OOB indices as input', () {
+ const double fontSize = 10.0;
+ final ParagraphBuilder builder = ParagraphBuilder(ParagraphStyle(
+ fontSize: fontSize,
+ maxLines: 1,
+ ellipsis: 'BBB',
+ ))..addText('A' * 100);
+ final Paragraph paragraph = builder.build();
+ paragraph.layout(const ParagraphConstraints(width: 100));
+
+ expect(paragraph.numberOfLines, 1);
+
+ expect(paragraph.getLineMetricsAt(-1), isNull);
+ expect(paragraph.getLineMetricsAt(0), isNotNull);
+ expect(paragraph.getLineMetricsAt(1), isNull);
+
+ expect(paragraph.getLineNumberAt(-1), isNull);
+ expect(paragraph.getLineNumberAt(0), 0);
+ expect(paragraph.getLineNumberAt(6), 0);
+ // The last 3 characters on the first line are ellipsized with BBB.
+ expect(paragraph.getLineMetricsAt(7), isNull);
+ });
+
test('painting a disposed paragraph does not crash', () {
final Paragraph paragraph = ParagraphBuilder(ParagraphStyle()).build();
paragraph.dispose();
diff --git a/third_party/txt/src/skia/paragraph_skia.cc b/third_party/txt/src/skia/paragraph_skia.cc
index 67d67b1..cbb7b85 100644
--- a/third_party/txt/src/skia/paragraph_skia.cc
+++ b/third_party/txt/src/skia/paragraph_skia.cc
@@ -301,6 +301,11 @@
return line_metrics_.value();
}
+bool ParagraphSkia::GetLineMetricsAt(int lineNumber,
+ skt::LineMetrics* lineMetrics) const {
+ return paragraph_->getLineMetricsAt(lineNumber, lineMetrics);
+};
+
double ParagraphSkia::GetMinIntrinsicWidth() {
return SkScalarToDouble(paragraph_->getMinIntrinsicWidth());
}
@@ -378,6 +383,14 @@
return Paragraph::Range<size_t>(range.start, range.end);
}
+size_t ParagraphSkia::GetNumberOfLines() const {
+ return paragraph_->lineNumber();
+}
+
+int ParagraphSkia::GetLineNumberAt(size_t codeUnitIndex) const {
+ return paragraph_->getLineNumberAtUTF16Offset(codeUnitIndex);
+}
+
TextStyle ParagraphSkia::SkiaToTxt(const skt::TextStyle& skia) {
TextStyle txt;
diff --git a/third_party/txt/src/skia/paragraph_skia.h b/third_party/txt/src/skia/paragraph_skia.h
index 5779445..bc23b78 100644
--- a/third_party/txt/src/skia/paragraph_skia.h
+++ b/third_party/txt/src/skia/paragraph_skia.h
@@ -50,6 +50,14 @@
std::vector<LineMetrics>& GetLineMetrics() override;
+ bool GetLineMetricsAt(
+ int lineNumber,
+ skia::textlayout::LineMetrics* lineMetrics) const override;
+
+ size_t GetNumberOfLines() const override;
+
+ int GetLineNumberAt(size_t utf16Offset) const override;
+
bool DidExceedMaxLines() override;
void Layout(double width) override;
diff --git a/third_party/txt/src/txt/paragraph.h b/third_party/txt/src/txt/paragraph.h
index 2d736fb..b8ee55d 100644
--- a/third_party/txt/src/txt/paragraph.h
+++ b/third_party/txt/src/txt/paragraph.h
@@ -22,6 +22,8 @@
#include "line_metrics.h"
#include "paragraph_style.h"
#include "third_party/skia/include/core/SkRect.h"
+#include "third_party/skia/modules/skparagraph/include/Metrics.h"
+#include "third_party/skia/modules/skparagraph/include/Paragraph.h"
class SkCanvas;
@@ -178,6 +180,22 @@
virtual Range<size_t> GetWordBoundary(size_t offset) = 0;
virtual std::vector<LineMetrics>& GetLineMetrics() = 0;
+
+ virtual bool GetLineMetricsAt(
+ int lineNumber,
+ skia::textlayout::LineMetrics* lineMetrics) const = 0;
+
+ // Returns the total number of visible lines in the paragraph.
+ virtual size_t GetNumberOfLines() const = 0;
+
+ // Returns the zero-indexed line number that contains the given code unit
+ // offset. Returns -1 if the given offset is out of bounds, or points to a
+ // codepoint that is logically after the last visible codepoint.
+ //
+ // If the offset points to a hard line break, this method returns the line
+ // number of the line this hard line break breaks, intead of the new line it
+ // creates.
+ virtual int GetLineNumberAt(size_t utf16Offset) const = 0;
};
} // namespace txt