Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/ui/dart_ui.cc
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,9 @@ typedef CanvasPath Path;
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) \
Expand Down
55 changes: 55 additions & 0 deletions lib/ui/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2772,6 +2772,18 @@ class LineMetrics {
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;
Expand Down Expand Up @@ -2992,6 +3004,32 @@ abstract class Paragraph {
/// 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();
Expand Down Expand Up @@ -3169,6 +3207,23 @@ base class _NativeParagraph extends NativeFieldWrapperClass1 implements Paragrap
@Native<Handle Function(Pointer<Void>)>(symbol: 'Paragraph::computeLineMetrics')
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);
Expand Down
46 changes: 43 additions & 3 deletions lib/ui/text/paragraph.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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 {

Expand Down Expand Up @@ -122,12 +126,12 @@ Dart_Handle Paragraph::getWordBoundary(unsigned offset) {
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;
Expand All @@ -137,7 +141,7 @@ Dart_Handle Paragraph::getLineBoundary(unsigned offset) {
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:
Expand Down Expand Up @@ -165,6 +169,42 @@ tonic::Float64List Paragraph::computeLineMetrics() {
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();
}
Dart_Handle arguments[9] = {
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, sizeof(arguments) / sizeof(Dart_Handle), arguments);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a std::array for arguments and call size() here

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();
Expand Down
5 changes: 4 additions & 1 deletion lib/ui/text/paragraph.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ class Paragraph : public RefCountedDartWrappable<Paragraph> {
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();

Expand Down
12 changes: 12 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 @@ -3227,6 +3227,18 @@ extension SkParagraphExtension on SkParagraph {
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;
Expand Down
20 changes: 20 additions & 0 deletions lib/web_ui/lib/src/engine/canvaskit/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,26 @@ class CkParagraph implements ui.Paragraph {
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
Expand Down
15 changes: 15 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 @@ -101,6 +101,15 @@ class SkwasmParagraph extends SkwasmObjectWrapper<RawParagraph> implements ui.Pa
@override
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);
Expand Down Expand Up @@ -214,6 +223,12 @@ class SkwasmParagraph extends SkwasmObjectWrapper<RawParagraph> implements ui.Pa
(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(
Expand Down
28 changes: 27 additions & 1 deletion lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ class CanvasParagraph implements ui.Paragraph {
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.
///
Expand Down Expand Up @@ -240,6 +240,32 @@ class CanvasParagraph implements ui.Paragraph {
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);
}
Comment on lines +251 to +262
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's code here that does the same thing:

int i;
for (i = 0; i < lines.length - 1; i++) {
final ParagraphLine line = lines[i];
if (index >= line.startIndex && index < line.endIndex) {
break;
}
}

Yours seems to be more performant (because binary search), so it would be nice to use it in getLineBoundary().


bool _disposed = false;

@override
Expand Down
10 changes: 8 additions & 2 deletions lib/web_ui/lib/src/engine/text/layout_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,10 @@ class TextLayoutService {
// 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(
Expand All @@ -416,7 +419,10 @@ class TextLayoutService {
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.
Expand Down
3 changes: 3 additions & 0 deletions lib/web_ui/lib/text.dart
Original file line number Diff line number Diff line change
Expand Up @@ -695,6 +695,9 @@ abstract class Paragraph {
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;
}
Expand Down
7 changes: 3 additions & 4 deletions lib/web_ui/skwasm/text/paragraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -74,14 +74,13 @@ SKWASM_EXPORT size_t paragraph_getLineCount(Paragraph* paragraph) {

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;
return paragraph->getLineMetricsAt(lineNumber, metrics) ? metrics : nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You need to delete metrics; in the case where you're returning a nullptr, otherwise this will leak.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

}

struct TextBoxList {
Expand Down
37 changes: 37 additions & 0 deletions lib/web_ui/test/canvaskit/text_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,43 @@ void testMain() {
});
});

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);

Expand Down
Loading