mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Render RTL text correctly (flutter/engine#26811)
This commit is contained in:
parent
8867a5c405
commit
d760a28918
@ -587,6 +587,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/measurement.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/paint_service.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/paragraph.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/ruler.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/text_direction.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/unicode_range.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/word_break_properties.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/word_breaker.dart
|
||||
|
||||
@ -1,2 +1,2 @@
|
||||
repository: https://github.com/flutter/goldens.git
|
||||
revision: 8df047cbfb1edb34569f3170a26e718fef22ffa7
|
||||
revision: 2b69a902e4c70b4e29044e66837e6b64aab1f473
|
||||
|
||||
@ -165,6 +165,8 @@ export 'engine/text/canvas_paragraph.dart';
|
||||
|
||||
export 'engine/text/ruler.dart';
|
||||
|
||||
export 'engine/text/text_direction.dart';
|
||||
|
||||
export 'engine/text/unicode_range.dart';
|
||||
|
||||
export 'engine/text/word_break_properties.dart';
|
||||
|
||||
@ -74,6 +74,8 @@ class CanvasParagraph implements EngineParagraph {
|
||||
@override
|
||||
bool isLaidOut = false;
|
||||
|
||||
bool get isRtl => paragraphStyle.effectiveTextDirection == ui.TextDirection.rtl;
|
||||
|
||||
ui.ParagraphConstraints? _lastUsedConstraints;
|
||||
|
||||
late final TextLayoutService _layoutService = TextLayoutService(this);
|
||||
@ -168,7 +170,7 @@ class CanvasParagraph implements EngineParagraph {
|
||||
|
||||
// 2. Append all spans to the paragraph.
|
||||
|
||||
ParagraphSpan? span;
|
||||
FlatTextSpan? span;
|
||||
|
||||
html.HtmlElement element = rootElement;
|
||||
final List<EngineLineMetrics> lines = computeLineMetrics();
|
||||
@ -180,21 +182,34 @@ class CanvasParagraph implements EngineParagraph {
|
||||
}
|
||||
|
||||
final EngineLineMetrics line = lines[i];
|
||||
for (final RangeBox box in line.boxes!) {
|
||||
final List<RangeBox> boxes = line.boxes!;
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
|
||||
int j = 0;
|
||||
while (j < boxes.length) {
|
||||
final RangeBox box = boxes[j++];
|
||||
if (box is SpanBox && box.span == span) {
|
||||
buffer.write(box.toText());
|
||||
continue;
|
||||
}
|
||||
|
||||
if (buffer.isNotEmpty) {
|
||||
domRenderer.appendText(element, buffer.toString());
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
if (box is SpanBox) {
|
||||
if (box.span != span) {
|
||||
span = box.span;
|
||||
element = domRenderer.createElement('span') as html.HtmlElement;
|
||||
applyTextStyleToElement(
|
||||
element: element,
|
||||
style: box.span.style,
|
||||
isSpan: true
|
||||
);
|
||||
domRenderer.append(rootElement, element);
|
||||
}
|
||||
domRenderer.appendText(element, box.toText());
|
||||
span = box.span;
|
||||
element = domRenderer.createElement('span') as html.HtmlElement;
|
||||
applyTextStyleToElement(
|
||||
element: element,
|
||||
style: box.span.style,
|
||||
isSpan: true,
|
||||
);
|
||||
domRenderer.append(rootElement, element);
|
||||
buffer.write(box.toText());
|
||||
} else if (box is PlaceholderBox) {
|
||||
span = box.placeholder;
|
||||
span = null;
|
||||
// If there's a line-end after this placeholder, we want the <BR> to
|
||||
// be inserted in the root paragraph element.
|
||||
element = rootElement;
|
||||
@ -207,6 +222,11 @@ class CanvasParagraph implements EngineParagraph {
|
||||
}
|
||||
}
|
||||
|
||||
if (buffer.isNotEmpty) {
|
||||
domRenderer.appendText(element, buffer.toString());
|
||||
buffer.clear();
|
||||
}
|
||||
|
||||
final String? ellipsis = line.ellipsis;
|
||||
if (ellipsis != null) {
|
||||
domRenderer.appendText(element, ellipsis);
|
||||
|
||||
@ -13,6 +13,7 @@ import 'line_breaker.dart';
|
||||
import 'measurement.dart';
|
||||
import 'paragraph.dart';
|
||||
import 'ruler.dart';
|
||||
import 'text_direction.dart';
|
||||
|
||||
/// Performs layout on a [CanvasParagraph].
|
||||
///
|
||||
@ -137,9 +138,9 @@ class TextLayoutService {
|
||||
spanIndex++;
|
||||
} else if (span is FlatTextSpan) {
|
||||
spanometer.currentSpan = span;
|
||||
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
|
||||
final DirectionalPosition nextBreak = currentLine.findNextBreak();
|
||||
final double additionalWidth =
|
||||
currentLine.getAdditionalWidthTo(nextBreak);
|
||||
currentLine.getAdditionalWidthTo(nextBreak.lineBreak);
|
||||
|
||||
if (currentLine.width + additionalWidth <= constraints.width) {
|
||||
// TODO(mdebbar): Handle the case when `nextBreak` is just a span end
|
||||
@ -160,8 +161,11 @@ class TextLayoutService {
|
||||
// We've reached the line that requires an ellipsis to be appended
|
||||
// to it.
|
||||
|
||||
currentLine.forceBreak(nextBreak,
|
||||
allowEmpty: true, ellipsis: ellipsis);
|
||||
currentLine.forceBreak(
|
||||
nextBreak,
|
||||
allowEmpty: true,
|
||||
ellipsis: ellipsis,
|
||||
);
|
||||
lines.add(currentLine.build(ellipsis: ellipsis));
|
||||
break;
|
||||
} else if (currentLine.isEmpty) {
|
||||
@ -226,7 +230,7 @@ class TextLayoutService {
|
||||
spanIndex++;
|
||||
} else if (span is FlatTextSpan) {
|
||||
spanometer.currentSpan = span;
|
||||
final LineBreakResult nextBreak = currentLine.findNextBreak(span.end);
|
||||
final DirectionalPosition nextBreak = currentLine.findNextBreak();
|
||||
|
||||
// For the purpose of max intrinsic width, we don't care if the line
|
||||
// fits within the constraints or not. So we always extend it.
|
||||
@ -348,23 +352,89 @@ class TextLayoutService {
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents a box inside [span] with the range of [start] to [end].
|
||||
/// Represents a box inside a paragraph span with the range of [start] to [end].
|
||||
///
|
||||
/// The box's coordinates are all relative to the line it belongs to. For
|
||||
/// example, [left] is the distance from the left edge of the line to the left
|
||||
/// edge of the box.
|
||||
///
|
||||
/// This is what the various measurements/coordinates look like for a box in an
|
||||
/// LTR paragraph:
|
||||
///
|
||||
/// *------------------------lineWidth------------------*
|
||||
/// *--width--*
|
||||
/// ┌─────────────────┬─────────┬───────────────────────┐
|
||||
/// │ │---BOX---│ │
|
||||
/// └─────────────────┴─────────┴───────────────────────┘
|
||||
/// *---startOffset---*
|
||||
/// *------left-------*
|
||||
/// *--------endOffset----------*
|
||||
/// *----------right------------*
|
||||
///
|
||||
///
|
||||
/// And in an RTL paragraph, [startOffset] and [endOffset] are flipped because
|
||||
/// the line starts from the right. Here's what they look like:
|
||||
///
|
||||
/// *------------------------lineWidth------------------*
|
||||
/// *--width--*
|
||||
/// ┌─────────────────┬─────────┬───────────────────────┐
|
||||
/// │ │---BOX---│ │
|
||||
/// └─────────────────┴─────────┴───────────────────────┘
|
||||
/// *------startOffset------*
|
||||
/// *------left-------*
|
||||
/// *-----------endOffset-------------*
|
||||
/// *----------right------------*
|
||||
///
|
||||
abstract class RangeBox {
|
||||
LineBreakResult get start;
|
||||
LineBreakResult get end;
|
||||
RangeBox(
|
||||
this.start,
|
||||
this.end,
|
||||
this.width,
|
||||
this.paragraphDirection,
|
||||
this.boxDirection,
|
||||
);
|
||||
|
||||
final LineBreakResult start;
|
||||
final LineBreakResult end;
|
||||
|
||||
/// The distance from the beginning of the line to the beginning of the box.
|
||||
late final double startOffset;
|
||||
|
||||
/// The distance from the beginning of the line to the end of the box.
|
||||
double get endOffset => startOffset + width;
|
||||
|
||||
/// The distance from the left edge of the line to the left edge of the box.
|
||||
double get left;
|
||||
double get left => paragraphDirection == ui.TextDirection.ltr
|
||||
? startOffset
|
||||
: lineWidth - endOffset;
|
||||
|
||||
/// The distance from the left edge of the line to the right edge of the box.
|
||||
double get right;
|
||||
double get right => paragraphDirection == ui.TextDirection.ltr
|
||||
? endOffset
|
||||
: lineWidth - startOffset;
|
||||
|
||||
/// The direction in which text inside this box flows.
|
||||
ui.TextDirection get direction;
|
||||
/// The distance from the left edge of the box to the right edge of the box.
|
||||
final double width;
|
||||
|
||||
/// The width of the line that this box belongs to.
|
||||
late final double lineWidth;
|
||||
|
||||
/// The text direction of the paragraph that this box belongs to.
|
||||
final ui.TextDirection paragraphDirection;
|
||||
|
||||
/// Indicates how this box flows among other boxes.
|
||||
///
|
||||
/// Example: In an LTR paragraph, the text "ABC hebrew_word 123 DEF" is shown
|
||||
/// visually in the following order:
|
||||
///
|
||||
/// +-------------------------------+
|
||||
/// | ABC | 123 | drow_werbeh | DEF |
|
||||
/// +-------------------------------+
|
||||
/// box direction: LTR RTL RTL LTR
|
||||
/// ----> <---- <------------ ---->
|
||||
///
|
||||
/// (In the above example, we are ignoring whitespace to simplify).
|
||||
final ui.TextDirection boxDirection;
|
||||
|
||||
/// Returns a [ui.TextBox] representing this range box in the given [line].
|
||||
///
|
||||
@ -385,27 +455,13 @@ class PlaceholderBox extends RangeBox {
|
||||
PlaceholderBox(
|
||||
this.placeholder, {
|
||||
required LineBreakResult index,
|
||||
required this.left,
|
||||
required this.direction,
|
||||
}) : start = index, end = index;
|
||||
required ui.TextDirection paragraphDirection,
|
||||
required ui.TextDirection boxDirection,
|
||||
}) : super(index, index, placeholder.width, paragraphDirection, boxDirection);
|
||||
|
||||
final PlaceholderSpan placeholder;
|
||||
|
||||
@override
|
||||
final LineBreakResult start;
|
||||
|
||||
@override
|
||||
final LineBreakResult end;
|
||||
|
||||
@override
|
||||
final double left;
|
||||
|
||||
@override
|
||||
double get right => left + placeholder.width;
|
||||
|
||||
@override
|
||||
final ui.TextDirection direction;
|
||||
|
||||
ui.TextBox toTextBox(EngineLineMetrics line) {
|
||||
final double left = line.left + this.left;
|
||||
final double right = line.left + this.right;
|
||||
@ -444,7 +500,7 @@ class PlaceholderBox extends RangeBox {
|
||||
top,
|
||||
right,
|
||||
top + placeholder.height,
|
||||
direction,
|
||||
paragraphDirection,
|
||||
);
|
||||
}
|
||||
|
||||
@ -463,27 +519,52 @@ class PlaceholderBox extends RangeBox {
|
||||
class SpanBox extends RangeBox {
|
||||
SpanBox(
|
||||
Spanometer spanometer, {
|
||||
required this.start,
|
||||
required this.end,
|
||||
required this.left,
|
||||
required this.direction,
|
||||
}) : this.spanometer = spanometer,
|
||||
required LineBreakResult start,
|
||||
required LineBreakResult end,
|
||||
required double width,
|
||||
required ui.TextDirection paragraphDirection,
|
||||
required ui.TextDirection boxDirection,
|
||||
required this.contentDirection,
|
||||
required this.isSpaceOnly,
|
||||
}) : this.spanometer = spanometer,
|
||||
span = spanometer.currentSpan,
|
||||
height = spanometer.height,
|
||||
baseline = spanometer.ascent,
|
||||
width = spanometer.measureIncludingSpace(start, end);
|
||||
super(start, end, width, paragraphDirection, boxDirection);
|
||||
|
||||
|
||||
final Spanometer spanometer;
|
||||
final FlatTextSpan span;
|
||||
final LineBreakResult start;
|
||||
final LineBreakResult end;
|
||||
|
||||
@override
|
||||
final double left;
|
||||
/// The direction of the text inside this box.
|
||||
///
|
||||
/// To illustrate the difference between [boxDirection] and [contentDirection]
|
||||
/// here's an example:
|
||||
///
|
||||
/// In an LTR paragraph, the text "ABC hebrew_word 123 DEF" is rendered as
|
||||
/// follows:
|
||||
///
|
||||
/// ----> <---- <------------ ---->
|
||||
/// box direction: LTR RTL RTL LTR
|
||||
/// +-------------------------------+
|
||||
/// | ABC | 123 | drow_werbeh | DEF |
|
||||
/// +-------------------------------+
|
||||
/// content direction: LTR LTR RTL LTR
|
||||
/// ----> ----> <------------ ---->
|
||||
///
|
||||
/// Notice the box containing "123" flows in the RTL direction (because it
|
||||
/// comes after an RTL box), while the content of the box flows in the LTR
|
||||
/// direction (i.e. the text is shown as "123" not "321").
|
||||
final ui.TextDirection contentDirection;
|
||||
|
||||
/// The distance from the left edge to the right edge of the box.
|
||||
final double width;
|
||||
/// Whether this box is made of only white space.
|
||||
final bool isSpaceOnly;
|
||||
|
||||
/// Whether the contents of this box flow in the left-to-right direction.
|
||||
bool get isContentLtr => contentDirection == ui.TextDirection.ltr;
|
||||
|
||||
/// Whether the contents of this box flow in the right-to-left direction.
|
||||
bool get isContentRtl => !isContentLtr;
|
||||
|
||||
/// The distance from the top edge to the bottom edge of the box.
|
||||
final double height;
|
||||
@ -492,12 +573,6 @@ class SpanBox extends RangeBox {
|
||||
/// the box.
|
||||
final double baseline;
|
||||
|
||||
@override
|
||||
final ui.TextDirection direction;
|
||||
|
||||
@override
|
||||
double get right => left + width;
|
||||
|
||||
/// Whether this box's range overlaps with the range from [startIndex] to
|
||||
/// [endIndex].
|
||||
bool overlapsWith(int startIndex, int endIndex) {
|
||||
@ -515,6 +590,7 @@ class SpanBox extends RangeBox {
|
||||
///
|
||||
/// The coordinates of the resulting [ui.TextBox] are relative to the
|
||||
/// paragraph, not to the line.
|
||||
@override
|
||||
ui.TextBox toTextBox(EngineLineMetrics line) {
|
||||
return intersect(line, start.index, end.index);
|
||||
}
|
||||
@ -526,41 +602,111 @@ class SpanBox extends RangeBox {
|
||||
/// paragraph, not to the line.
|
||||
ui.TextBox intersect(EngineLineMetrics line, int start, int end) {
|
||||
final double top = line.baseline - baseline;
|
||||
final double left, right;
|
||||
|
||||
final double before;
|
||||
if (start <= this.start.index) {
|
||||
left = this.left;
|
||||
before = 0.0;
|
||||
} else {
|
||||
spanometer.currentSpan = span;
|
||||
left = this.left + spanometer._measure(this.start.index, start);
|
||||
before = spanometer._measure(this.start.index, start);
|
||||
}
|
||||
|
||||
final double after;
|
||||
if (end >= this.end.indexWithoutTrailingNewlines) {
|
||||
right = this.right;
|
||||
after = 0.0;
|
||||
} else {
|
||||
spanometer.currentSpan = span;
|
||||
right = this.right -
|
||||
spanometer._measure(end, this.end.indexWithoutTrailingNewlines);
|
||||
after = spanometer._measure(end, this.end.indexWithoutTrailingNewlines);
|
||||
}
|
||||
|
||||
final double left, right;
|
||||
if (isContentLtr) {
|
||||
// Example: let's say the text is "Loremipsum" and we want to get the box
|
||||
// for "rem". In this case, `before` is the width of "Lo", and `after`
|
||||
// is the width of "ipsum".
|
||||
//
|
||||
// Here's how the measurements/coordinates look like:
|
||||
//
|
||||
// before after
|
||||
// |----| |----------|
|
||||
// +---------------------+
|
||||
// | L o r e m i p s u m |
|
||||
// +---------------------+
|
||||
// this.left ^ ^ this.right
|
||||
left = this.left + before;
|
||||
right = this.right - after;
|
||||
} else {
|
||||
// Example: let's say the text is "txet_werbeH" ("Hebrew_text" flowing from
|
||||
// right to left). Say we want to get the box for "brew". The `before` is
|
||||
// the width of "He", and `after` is the width of "_text".
|
||||
//
|
||||
// after before
|
||||
// |----------| |----|
|
||||
// +-----------------------+
|
||||
// | t x e t _ w e r b e H |
|
||||
// +-----------------------+
|
||||
// this.left ^ ^ this.right
|
||||
//
|
||||
// Notice how `before` and `after` are reversed in the RTL example. That's
|
||||
// because the text flows from right to left.
|
||||
left = this.left + after;
|
||||
right = this.right - before;
|
||||
}
|
||||
|
||||
// The [RangeBox]'s left and right edges are relative to the line. In order
|
||||
// to make them relative to the paragraph, we need to add the left edge of
|
||||
// the line.
|
||||
return ui.TextBox.fromLTRBD(
|
||||
left + line.left,
|
||||
line.left + left,
|
||||
top,
|
||||
right + line.left,
|
||||
line.left + right,
|
||||
top + height,
|
||||
direction,
|
||||
contentDirection,
|
||||
);
|
||||
}
|
||||
|
||||
/// Transforms the [x] coordinate to be relative to this box and matches the
|
||||
/// flow of content.
|
||||
///
|
||||
/// In LTR paragraphs, the [startOffset] and [endOffset] of an RTL box
|
||||
/// indicate the visual beginning and end of the box. But the text inside the
|
||||
/// box flows in the opposite direction (from [endOffset] to [startOffset]).
|
||||
///
|
||||
/// The X (input) is relative to the line, and always from left-to-right
|
||||
/// independent of paragraph and content direction.
|
||||
///
|
||||
/// Here's how it looks for a box with LTR content:
|
||||
///
|
||||
/// *------------------------lineWidth------------------*
|
||||
/// *---------------X (input)
|
||||
/// ┌───────────┬────────────────────────┬───────────────┐
|
||||
/// │ │ --content-direction--> │ │
|
||||
/// └───────────┴────────────────────────┴───────────────┘
|
||||
/// *---X' (output)
|
||||
/// *---left----*
|
||||
/// *---------------right----------------*
|
||||
///
|
||||
///
|
||||
/// And here's how it looks for a box with RTL content:
|
||||
///
|
||||
/// *------------------------lineWidth------------------*
|
||||
/// *----------------X (input)
|
||||
/// ┌───────────┬────────────────────────┬───────────────┐
|
||||
/// │ │ <--content-direction-- │ │
|
||||
/// └───────────┴────────────────────────┴───────────────┘
|
||||
/// (output) X'------------------*
|
||||
/// *---left----*
|
||||
/// *---------------right----------------*
|
||||
///
|
||||
double _makeXRelativeToContent(double x) {
|
||||
return isContentRtl ? right - x : x - left;
|
||||
}
|
||||
|
||||
@override
|
||||
ui.TextPosition getPositionForX(double x) {
|
||||
spanometer.currentSpan = span;
|
||||
|
||||
// Make `x` relative to this box.
|
||||
x -= left;
|
||||
x = _makeXRelativeToContent(x);
|
||||
|
||||
final int startIndex = start.index;
|
||||
final int endIndex = end.indexWithoutTrailingNewlines;
|
||||
@ -730,10 +876,7 @@ class LineBuilder {
|
||||
/// The horizontal offset necessary for the line to be correctly aligned.
|
||||
double get alignOffset {
|
||||
final double emptySpace = maxWidth - width;
|
||||
final ui.TextDirection textDirection =
|
||||
paragraph.paragraphStyle.effectiveTextDirection;
|
||||
final ui.TextAlign textAlign =
|
||||
paragraph.paragraphStyle.effectiveTextAlign;
|
||||
final ui.TextAlign textAlign = paragraph.paragraphStyle.effectiveTextAlign;
|
||||
|
||||
switch (textAlign) {
|
||||
case ui.TextAlign.center:
|
||||
@ -741,9 +884,9 @@ class LineBuilder {
|
||||
case ui.TextAlign.right:
|
||||
return emptySpace;
|
||||
case ui.TextAlign.start:
|
||||
return textDirection == ui.TextDirection.rtl ? emptySpace : 0.0;
|
||||
return _paragraphDirection == ui.TextDirection.rtl ? emptySpace : 0.0;
|
||||
case ui.TextAlign.end:
|
||||
return textDirection == ui.TextDirection.rtl ? 0.0 : emptySpace;
|
||||
return _paragraphDirection == ui.TextDirection.rtl ? 0.0 : emptySpace;
|
||||
default:
|
||||
return 0.0;
|
||||
}
|
||||
@ -767,12 +910,37 @@ class LineBuilder {
|
||||
return (_boxes.last is PlaceholderBox);
|
||||
}
|
||||
|
||||
ui.TextDirection get _paragraphDirection =>
|
||||
paragraph.paragraphStyle.effectiveTextDirection;
|
||||
|
||||
late ui.TextDirection _currentBoxDirection = _paragraphDirection;
|
||||
|
||||
late ui.TextDirection _currentContentDirection = _paragraphDirection;
|
||||
|
||||
bool _shouldCreateBoxBeforeExtendingTo(DirectionalPosition newEnd) {
|
||||
// When the direction changes, we need to make sure to put them in separate
|
||||
// boxes.
|
||||
return newEnd.isSpaceOnly || _currentBoxDirection != newEnd.textDirection || _currentContentDirection != newEnd.textDirection;
|
||||
}
|
||||
|
||||
/// Extends the line by setting a [newEnd].
|
||||
void extendTo(LineBreakResult newEnd) {
|
||||
void extendTo(DirectionalPosition newEnd) {
|
||||
ascent = math.max(ascent, spanometer.ascent);
|
||||
descent = math.max(descent, spanometer.descent);
|
||||
|
||||
_addSegment(_createSegment(newEnd));
|
||||
// When the direction changes, we need to make sure to put them in separate
|
||||
// boxes.
|
||||
if (_shouldCreateBoxBeforeExtendingTo(newEnd)) {
|
||||
createBox();
|
||||
}
|
||||
_currentBoxDirection = newEnd.textDirection ?? _currentBoxDirection;
|
||||
_currentContentDirection = newEnd.textDirection ?? ui.TextDirection.ltr;
|
||||
|
||||
_addSegment(_createSegment(newEnd.lineBreak));
|
||||
if (newEnd.isSpaceOnly) {
|
||||
// Whitespace sequences go in their own boxes.
|
||||
createBox(isSpaceOnly: true);
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the line to the end of the paragraph.
|
||||
@ -854,10 +1022,11 @@ class LineBuilder {
|
||||
// Add the placeholder box.
|
||||
_boxes.add(PlaceholderBox(
|
||||
placeholder,
|
||||
index: _boxStart,
|
||||
left: _boxLeft,
|
||||
direction: paragraph.paragraphStyle.effectiveTextDirection,
|
||||
index: _currentBoxStart,
|
||||
paragraphDirection: _paragraphDirection,
|
||||
boxDirection: _currentBoxDirection,
|
||||
));
|
||||
_currentBoxStartOffset = widthIncludingSpace;
|
||||
}
|
||||
|
||||
/// Creates a new segment to be appended to the end of this line.
|
||||
@ -944,7 +1113,7 @@ class LineBuilder {
|
||||
/// 2. The line doesn't have any line break opportunities and has to be
|
||||
/// force-broken.
|
||||
void forceBreak(
|
||||
LineBreakResult nextBreak, {
|
||||
DirectionalPosition nextBreak, {
|
||||
required bool allowEmpty,
|
||||
String? ellipsis,
|
||||
}) {
|
||||
@ -952,7 +1121,7 @@ class LineBuilder {
|
||||
final double availableWidth = maxWidth - widthIncludingSpace;
|
||||
final int breakingPoint = spanometer.forceBreak(
|
||||
end.index,
|
||||
nextBreak.indexWithoutTrailingSpaces,
|
||||
nextBreak.lineBreak.indexWithoutTrailingSpaces,
|
||||
availableWidth: availableWidth,
|
||||
allowEmpty: allowEmpty,
|
||||
);
|
||||
@ -961,15 +1130,13 @@ class LineBuilder {
|
||||
// 1. Next break is only one character away, with zero or many spaces. AND
|
||||
// 2. There isn't enough width to fit the single character. AND
|
||||
// 3. `allowEmpty` is false.
|
||||
if (breakingPoint == nextBreak.indexWithoutTrailingSpaces) {
|
||||
if (breakingPoint == nextBreak.lineBreak.indexWithoutTrailingSpaces) {
|
||||
// In this case, we just extend to `nextBreak` instead of creating a new
|
||||
// artificial break. It's safe (and better) to do so, because we don't
|
||||
// want the trailing white space to go to the next line.
|
||||
extendTo(nextBreak);
|
||||
} else {
|
||||
extendTo(
|
||||
LineBreakResult.sameIndex(breakingPoint, LineBreakType.prohibited),
|
||||
);
|
||||
extendTo(nextBreak.copyWithIndex(breakingPoint));
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -986,7 +1153,7 @@ class LineBuilder {
|
||||
final double availableWidth = maxWidth - ellipsisWidth;
|
||||
|
||||
// First, we create the new segment until `nextBreak`.
|
||||
LineSegment segmentToBreak = _createSegment(nextBreak);
|
||||
LineSegment segmentToBreak = _createSegment(nextBreak.lineBreak);
|
||||
|
||||
// Then, we keep popping until we find the segment that has to be broken.
|
||||
// After the loop ends, two things are correct:
|
||||
@ -1005,17 +1172,18 @@ class LineBuilder {
|
||||
availableWidth: availableWidthForSegment,
|
||||
allowEmpty: allowEmpty,
|
||||
);
|
||||
extendTo(
|
||||
LineBreakResult.sameIndex(breakingPoint, LineBreakType.prohibited));
|
||||
|
||||
// There's a possibility that the end of line has moved backwards, so we
|
||||
// need to remove some boxes in that case.
|
||||
while (_boxes.length > 0 && _boxes.last.end.index > breakingPoint) {
|
||||
_boxes.removeLast();
|
||||
}
|
||||
_currentBoxStartOffset = widthIncludingSpace;
|
||||
|
||||
extendTo(nextBreak.copyWithIndex(breakingPoint));
|
||||
}
|
||||
|
||||
LineBreakResult get _boxStart {
|
||||
LineBreakResult get _currentBoxStart {
|
||||
if (_boxes.isEmpty) {
|
||||
return start;
|
||||
}
|
||||
@ -1023,25 +1191,22 @@ class LineBuilder {
|
||||
return _boxes.last.end;
|
||||
}
|
||||
|
||||
double get _boxLeft {
|
||||
if (_boxes.isEmpty) {
|
||||
return 0.0;
|
||||
}
|
||||
return _boxes.last.right;
|
||||
}
|
||||
double _currentBoxStartOffset = 0.0;
|
||||
|
||||
ui.TextDirection get direction =>
|
||||
paragraph.paragraphStyle.effectiveTextDirection;
|
||||
double get _currentBoxWidth => widthIncludingSpace - _currentBoxStartOffset;
|
||||
|
||||
/// Cuts a new box in the line.
|
||||
///
|
||||
/// If this is the first box in the line, it'll start at the beginning of the
|
||||
/// line. Else, it'll start at the end of the last box.
|
||||
///
|
||||
/// A box should be cut whenever the end of line is reached, or when switching
|
||||
/// from one span to another.
|
||||
void createBox() {
|
||||
final LineBreakResult boxStart = _boxStart;
|
||||
/// A box should be cut whenever the end of line is reached, when switching
|
||||
/// from one span to another, or when switching text direction.
|
||||
///
|
||||
/// [isSpaceOnly] indicates that the box contains nothing but whitespace
|
||||
/// characters.
|
||||
void createBox({bool isSpaceOnly = false}) {
|
||||
final LineBreakResult boxStart = _currentBoxStart;
|
||||
final LineBreakResult boxEnd = end;
|
||||
// Avoid creating empty boxes. This could happen when the end of a span
|
||||
// coincides with the end of a line. In this case, `createBox` is called twice.
|
||||
@ -1053,15 +1218,21 @@ class LineBuilder {
|
||||
spanometer,
|
||||
start: boxStart,
|
||||
end: boxEnd,
|
||||
left: _boxLeft,
|
||||
direction: paragraph.paragraphStyle.effectiveTextDirection,
|
||||
width: _currentBoxWidth,
|
||||
paragraphDirection: _paragraphDirection,
|
||||
boxDirection: _currentBoxDirection,
|
||||
contentDirection: _currentContentDirection,
|
||||
isSpaceOnly: isSpaceOnly,
|
||||
));
|
||||
|
||||
_currentBoxStartOffset = widthIncludingSpace;
|
||||
}
|
||||
|
||||
/// Builds the [EngineLineMetrics] instance that represents this line.
|
||||
EngineLineMetrics build({String? ellipsis}) {
|
||||
// At the end of each line, we cut the last box of the line.
|
||||
createBox();
|
||||
_positionBoxes();
|
||||
|
||||
final double ellipsisWidth =
|
||||
ellipsis == null ? 0.0 : spanometer.measureText(ellipsis);
|
||||
@ -1091,9 +1262,112 @@ class LineBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
/// Positions the boxes and takes into account their directions, and the
|
||||
/// paragraph's direction.
|
||||
void _positionBoxes() {
|
||||
final List<RangeBox> boxes = _boxes;
|
||||
|
||||
int i = 0;
|
||||
double cumulativeWidth = 0.0;
|
||||
while (i < boxes.length) {
|
||||
final RangeBox box = boxes[i];
|
||||
if (box.boxDirection == _paragraphDirection) {
|
||||
// The box is in the same direction as the paragraph.
|
||||
box.startOffset = cumulativeWidth;
|
||||
box.lineWidth = width;
|
||||
|
||||
cumulativeWidth += box.width;
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// At this point, we found a box that has the opposite direction to the
|
||||
// paragraph. This could be a sequence of one or more boxes.
|
||||
//
|
||||
// These boxes should flow in the opposite direction. So we need to
|
||||
// position them in reverse order.
|
||||
//
|
||||
// If the last box in the sequence is a space-only box (contains only
|
||||
// whitespace characters), it should be excluded from the sequence.
|
||||
//
|
||||
// Example: an LTR paragraph with the contents:
|
||||
//
|
||||
// "ABC rtl1 rtl2 rtl3 XYZ"
|
||||
// ^ ^ ^ ^
|
||||
// SP1 SP2 SP3 SP4
|
||||
//
|
||||
//
|
||||
// box direction: LTR RTL LTR
|
||||
// |------>|<-----------------------|------>
|
||||
// +----------------------------------------+
|
||||
// | ABC | | rtl3 | | rtl2 | | rtl1 | | XYZ |
|
||||
// +----------------------------------------+
|
||||
// ^ ^ ^ ^
|
||||
// SP1 SP3 SP2 SP4
|
||||
//
|
||||
// Notice how SP2 and SP3 are flowing in the RTL direction because of the
|
||||
// surrounding RTL words. SP4 is also preceded by an RTL word, but it marks
|
||||
// the end of the RTL sequence, so it goes back to flowing in the paragraph
|
||||
// direction (LTR).
|
||||
|
||||
final int first = i;
|
||||
int lastNonSpaceBox = first;
|
||||
i++;
|
||||
while (i < boxes.length && boxes[i].boxDirection != _paragraphDirection) {
|
||||
final RangeBox box = boxes[i];
|
||||
if (box is SpanBox && box.isSpaceOnly) {
|
||||
// Do nothing.
|
||||
} else {
|
||||
lastNonSpaceBox = i;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
final int last = lastNonSpaceBox;
|
||||
i = lastNonSpaceBox + 1;
|
||||
|
||||
// The range (first:last) is the entire sequence of boxes that have the
|
||||
// opposite direction to the paragraph.
|
||||
final double sequenceWidth =
|
||||
_positionBoxesInReverse(boxes, first, last, startOffset: cumulativeWidth);
|
||||
cumulativeWidth += sequenceWidth;
|
||||
}
|
||||
}
|
||||
|
||||
/// Positions a sequence of boxes in the direction opposite to the paragraph
|
||||
/// text direction.
|
||||
///
|
||||
/// This is needed when a right-to-left sequence appears in the middle of a
|
||||
/// left-to-right paragraph, or vice versa.
|
||||
///
|
||||
/// Returns the total width of all the positioned boxes in the sequence.
|
||||
///
|
||||
/// [first] and [last] are expected to be inclusive.
|
||||
double _positionBoxesInReverse(
|
||||
List<RangeBox> boxes,
|
||||
int first,
|
||||
int last, {
|
||||
required double startOffset,
|
||||
}) {
|
||||
double cumulativeWidth = 0.0;
|
||||
for (int i = last; i >= first; i--) {
|
||||
// Update the visual position of each box.
|
||||
final RangeBox box = boxes[i];
|
||||
assert(box.boxDirection != _paragraphDirection);
|
||||
box.startOffset = startOffset + cumulativeWidth;
|
||||
box.lineWidth = width;
|
||||
|
||||
cumulativeWidth += box.width;
|
||||
}
|
||||
return cumulativeWidth;
|
||||
}
|
||||
|
||||
/// Finds the next line break after the end of this line.
|
||||
LineBreakResult findNextBreak(int maxEnd) {
|
||||
return nextLineBreak(paragraph.toPlainText(), end.index, maxEnd: maxEnd);
|
||||
DirectionalPosition findNextBreak() {
|
||||
final String text = paragraph.toPlainText();
|
||||
final int maxEnd = spanometer.currentSpan.end;
|
||||
final LineBreakResult result = nextLineBreak(text, end.index, maxEnd: maxEnd);
|
||||
// The current end of the line is the beginning of the next block.
|
||||
return getDirectionalBlockEnd(text, end, result);
|
||||
}
|
||||
|
||||
/// Creates a new [LineBuilder] to build the next line in the paragraph.
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import 'line_breaker.dart';
|
||||
import 'unicode_range.dart';
|
||||
|
||||
// This data was taken from the source code of the Closure library:
|
||||
//
|
||||
// - https://github.com/google/closure-library/blob/9d24a6c1809a671c2e54c328897ebeae15a6d172/closure/goog/i18n/bidi.js#L203-L234
|
||||
final UnicodePropertyLookup<ui.TextDirection?> _textDirectionLookup = UnicodePropertyLookup<ui.TextDirection?>(
|
||||
<UnicodeRange<ui.TextDirection>>[
|
||||
// LTR
|
||||
UnicodeRange<ui.TextDirection>(kChar_A, kChar_Z, ui.TextDirection.ltr),
|
||||
UnicodeRange<ui.TextDirection>(kChar_a, kChar_z, ui.TextDirection.ltr),
|
||||
UnicodeRange<ui.TextDirection>(0x00C0, 0x00D6, ui.TextDirection.ltr),
|
||||
UnicodeRange<ui.TextDirection>(0x00D8, 0x00F6, ui.TextDirection.ltr),
|
||||
UnicodeRange<ui.TextDirection>(0x00F8, 0x02B8, ui.TextDirection.ltr),
|
||||
UnicodeRange<ui.TextDirection>(0x0300, 0x0590, ui.TextDirection.ltr),
|
||||
// RTL
|
||||
UnicodeRange<ui.TextDirection>(0x0591, 0x06EF, ui.TextDirection.rtl),
|
||||
UnicodeRange<ui.TextDirection>(0x06FA, 0x08FF, ui.TextDirection.rtl),
|
||||
// LTR
|
||||
UnicodeRange<ui.TextDirection>(0x0900, 0x1FFF, ui.TextDirection.ltr),
|
||||
UnicodeRange<ui.TextDirection>(0x200E, 0x200E, ui.TextDirection.ltr),
|
||||
// RTL
|
||||
UnicodeRange<ui.TextDirection>(0x200F, 0x200F, ui.TextDirection.rtl),
|
||||
// LTR
|
||||
UnicodeRange<ui.TextDirection>(0x2C00, 0xD801, ui.TextDirection.ltr),
|
||||
// RTL
|
||||
UnicodeRange<ui.TextDirection>(0xD802, 0xD803, ui.TextDirection.rtl),
|
||||
// LTR
|
||||
UnicodeRange<ui.TextDirection>(0xD804, 0xD839, ui.TextDirection.ltr),
|
||||
// RTL
|
||||
UnicodeRange<ui.TextDirection>(0xD83A, 0xD83B, ui.TextDirection.rtl),
|
||||
// LTR
|
||||
UnicodeRange<ui.TextDirection>(0xD83C, 0xDBFF, ui.TextDirection.ltr),
|
||||
UnicodeRange<ui.TextDirection>(0xF900, 0xFB1C, ui.TextDirection.ltr),
|
||||
// RTL
|
||||
UnicodeRange<ui.TextDirection>(0xFB1D, 0xFDFF, ui.TextDirection.rtl),
|
||||
// LTR
|
||||
UnicodeRange<ui.TextDirection>(0xFE00, 0xFE6F, ui.TextDirection.ltr),
|
||||
// RTL
|
||||
UnicodeRange<ui.TextDirection>(0xFE70, 0xFEFC, ui.TextDirection.rtl),
|
||||
// LTR
|
||||
UnicodeRange<ui.TextDirection>(0xFEFD, 0xFFFF, ui.TextDirection.ltr),
|
||||
],
|
||||
null,
|
||||
);
|
||||
|
||||
/// Represents a block of text with a certain [ui.TextDirection].
|
||||
class DirectionalPosition {
|
||||
const DirectionalPosition(this.lineBreak, this.textDirection, this.isSpaceOnly);
|
||||
|
||||
final LineBreakResult lineBreak;
|
||||
|
||||
final ui.TextDirection? textDirection;
|
||||
|
||||
final bool isSpaceOnly;
|
||||
|
||||
LineBreakType get type => lineBreak.type;
|
||||
|
||||
/// Creates a copy of this [DirectionalPosition] with a different [index].
|
||||
///
|
||||
/// The type of the returned [DirectionalPosition] is set to
|
||||
/// [LineBreakType.prohibited].
|
||||
DirectionalPosition copyWithIndex(int index) {
|
||||
return DirectionalPosition(
|
||||
LineBreakResult.sameIndex(index, LineBreakType.prohibited),
|
||||
textDirection,
|
||||
isSpaceOnly,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Finds the end of the directional block of text that starts at [start] up
|
||||
/// until [end].
|
||||
///
|
||||
/// If the block goes beyond [end], the part after [end] is ignored.
|
||||
DirectionalPosition getDirectionalBlockEnd(
|
||||
String text,
|
||||
LineBreakResult start,
|
||||
LineBreakResult end,
|
||||
) {
|
||||
if (start.index == end.index) {
|
||||
return DirectionalPosition(end, null, false);
|
||||
}
|
||||
|
||||
// Check if we are in a space-only block.
|
||||
if (start.index == end.indexWithoutTrailingSpaces) {
|
||||
return DirectionalPosition(end, null, true);
|
||||
}
|
||||
|
||||
ui.TextDirection? blockDirection = _textDirectionLookup.find(text, start.index);
|
||||
int i = start.index + 1;
|
||||
|
||||
while (i < end.indexWithoutTrailingSpaces) {
|
||||
final ui.TextDirection? direction = _textDirectionLookup.find(text, i);
|
||||
if (direction != blockDirection) {
|
||||
// Reached the next block.
|
||||
break;
|
||||
}
|
||||
i++;
|
||||
}
|
||||
|
||||
if (i == end.indexWithoutTrailingNewlines) {
|
||||
// If all that remains before [end] is new lines, let's include them in the
|
||||
// block.
|
||||
return DirectionalPosition(end, blockDirection, false);
|
||||
}
|
||||
return DirectionalPosition(
|
||||
LineBreakResult.sameIndex(i, LineBreakType.prohibited),
|
||||
blockDirection,
|
||||
false,
|
||||
);
|
||||
}
|
||||
@ -2,13 +2,13 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
const int _kChar_0 = 48;
|
||||
const int _kChar_9 = 57;
|
||||
const int _kChar_A = 65;
|
||||
const int _kChar_Z = 90;
|
||||
const int _kChar_a = 97;
|
||||
const int _kChar_z = 122;
|
||||
const int _kCharBang = 33;
|
||||
const int kChar_0 = 48;
|
||||
const int kChar_9 = 57;
|
||||
const int kChar_A = 65;
|
||||
const int kChar_Z = 90;
|
||||
const int kChar_a = 97;
|
||||
const int kChar_z = 122;
|
||||
const int kCharBang = 33;
|
||||
|
||||
enum _ComparisonResult {
|
||||
inside,
|
||||
@ -210,7 +210,7 @@ List<UnicodeRange<P>> _unpackProperties<P>(
|
||||
i += 4;
|
||||
|
||||
int rangeEnd;
|
||||
if (packedData.codeUnitAt(i) == _kCharBang) {
|
||||
if (packedData.codeUnitAt(i) == kCharBang) {
|
||||
rangeEnd = rangeStart;
|
||||
i++;
|
||||
} else {
|
||||
@ -231,15 +231,15 @@ int _getEnumIndexFromPackedValue(int charCode) {
|
||||
// This has to stay in sync with [EnumValue.serialized] in
|
||||
// `tool/unicode_sync_script.dart`.
|
||||
|
||||
assert((charCode >= _kChar_A && charCode <= _kChar_Z) ||
|
||||
(charCode >= _kChar_a && charCode <= _kChar_z));
|
||||
assert((charCode >= kChar_A && charCode <= kChar_Z) ||
|
||||
(charCode >= kChar_a && charCode <= kChar_z));
|
||||
|
||||
// Uppercase letters were assigned to the first 26 enum values.
|
||||
if (charCode <= _kChar_Z) {
|
||||
return charCode - _kChar_A;
|
||||
if (charCode <= kChar_Z) {
|
||||
return charCode - kChar_A;
|
||||
}
|
||||
// Lowercase letters were assigned to enum values above 26.
|
||||
return 26 + charCode - _kChar_a;
|
||||
return 26 + charCode - kChar_a;
|
||||
}
|
||||
|
||||
int _consumeInt(String packedData, int index) {
|
||||
@ -261,12 +261,12 @@ int _consumeInt(String packedData, int index) {
|
||||
/// Does the same thing as [int.parse(str, 36)] but takes only a single
|
||||
/// character as a [charCode] integer.
|
||||
int _getIntFromCharCode(int charCode) {
|
||||
assert((charCode >= _kChar_0 && charCode <= _kChar_9) ||
|
||||
(charCode >= _kChar_a && charCode <= _kChar_z));
|
||||
assert((charCode >= kChar_0 && charCode <= kChar_9) ||
|
||||
(charCode >= kChar_a && charCode <= kChar_z));
|
||||
|
||||
if (charCode <= _kChar_9) {
|
||||
return charCode - _kChar_0;
|
||||
if (charCode <= kChar_9) {
|
||||
return charCode - kChar_0;
|
||||
}
|
||||
// "a" starts from 10 and remaining letters go up from there.
|
||||
return charCode - _kChar_a + 10;
|
||||
return charCode - kChar_a + 10;
|
||||
}
|
||||
|
||||
@ -0,0 +1,582 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/ui.dart' hide window;
|
||||
import 'package:ui/src/engine.dart';
|
||||
|
||||
import '../scuba.dart';
|
||||
import 'helper.dart';
|
||||
|
||||
typedef CanvasTest = FutureOr<void> Function(EngineCanvas canvas);
|
||||
|
||||
const String _rtlWord1 = 'واحد';
|
||||
const String _rtlWord2 = 'اثنان';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() async {
|
||||
setUpStableTestFonts();
|
||||
|
||||
void paintBasicBidiStartingWithLtr(
|
||||
EngineCanvas canvas,
|
||||
Rect bounds,
|
||||
double y,
|
||||
TextDirection textDirection,
|
||||
TextAlign textAlign,
|
||||
) {
|
||||
// The text starts with a left-to-right word.
|
||||
const String text = 'One 12 $_rtlWord1 $_rtlWord2 34 two 56';
|
||||
|
||||
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textDirection: textDirection,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
final CanvasParagraph paragraph = plain(
|
||||
paragraphStyle,
|
||||
text,
|
||||
textStyle: EngineTextStyle.only(color: blue),
|
||||
);
|
||||
final double maxWidth = bounds.width - 10;
|
||||
paragraph.layout(constrain(maxWidth));
|
||||
canvas.drawParagraph(paragraph, Offset(bounds.left + 5, bounds.top + y + 5));
|
||||
}
|
||||
|
||||
test('basic bidi starting with ltr', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 340, 600);
|
||||
final canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
const double height = 40;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 320, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(0, ltrBox.height + 10);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_start_ltr');
|
||||
});
|
||||
|
||||
test('basic bidi starting with ltr (DOM)', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 340, 600);
|
||||
final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
|
||||
|
||||
const double height = 40;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 320, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintBasicBidiStartingWithLtr(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(0, ltrBox.height + 10);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintBasicBidiStartingWithLtr(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_start_ltr_dom');
|
||||
});
|
||||
|
||||
void paintBasicBidiStartingWithRtl(
|
||||
EngineCanvas canvas,
|
||||
Rect bounds,
|
||||
double y,
|
||||
TextDirection textDirection,
|
||||
TextAlign textAlign,
|
||||
) {
|
||||
// The text starts with a right-to-left word.
|
||||
const String text = '$_rtlWord1 12 one 34 $_rtlWord2 56 two';
|
||||
|
||||
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textDirection: textDirection,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
final CanvasParagraph paragraph = plain(
|
||||
paragraphStyle,
|
||||
text,
|
||||
textStyle: EngineTextStyle.only(color: blue),
|
||||
);
|
||||
final double maxWidth = bounds.width - 10;
|
||||
paragraph.layout(constrain(maxWidth));
|
||||
canvas.drawParagraph(paragraph, Offset(bounds.left + 5, bounds.top + y + 5));
|
||||
}
|
||||
|
||||
test('basic bidi starting with rtl', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 340, 600);
|
||||
final canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
const double height = 40;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 320, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(0, ltrBox.height + 10);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_start_rtl');
|
||||
});
|
||||
|
||||
test('basic bidi starting with rtl (DOM)', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 340, 600);
|
||||
final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
|
||||
|
||||
const double height = 40;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 320, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintBasicBidiStartingWithRtl(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(0, ltrBox.height + 10);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintBasicBidiStartingWithRtl(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_start_rtl_dom');
|
||||
});
|
||||
|
||||
void paintMultilineBidi(
|
||||
EngineCanvas canvas,
|
||||
Rect bounds,
|
||||
double y,
|
||||
TextDirection textDirection,
|
||||
TextAlign textAlign,
|
||||
) {
|
||||
// '''
|
||||
// Lorem 12 $_rtlWord1
|
||||
// $_rtlWord2 34 ipsum
|
||||
// dolor 56
|
||||
// '''
|
||||
const String text = 'Lorem 12 $_rtlWord1 $_rtlWord2 34 ipsum dolor 56';
|
||||
|
||||
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textDirection: textDirection,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
final CanvasParagraph paragraph = plain(
|
||||
paragraphStyle,
|
||||
text,
|
||||
textStyle: EngineTextStyle.only(color: blue),
|
||||
);
|
||||
final double maxWidth = bounds.width - 10;
|
||||
paragraph.layout(constrain(maxWidth));
|
||||
canvas.drawParagraph(paragraph, Offset(bounds.left + 5, bounds.top + y + 5));
|
||||
}
|
||||
|
||||
test('multiline bidi', () {
|
||||
final Rect bounds = Rect.fromLTWH(0, 0, 400, 500);
|
||||
final canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
const double height = 95;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 150, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintMultilineBidi(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintMultilineBidi(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintMultilineBidi(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintMultilineBidi(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintMultilineBidi(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(ltrBox.width + 10, 0);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintMultilineBidi(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintMultilineBidi(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintMultilineBidi(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintMultilineBidi(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintMultilineBidi(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_multiline');
|
||||
});
|
||||
|
||||
test('multiline bidi (DOM)', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 500);
|
||||
final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
|
||||
|
||||
const double height = 95;
|
||||
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 150, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintMultilineBidi(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintMultilineBidi(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintMultilineBidi(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintMultilineBidi(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintMultilineBidi(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(ltrBox.width + 10, 0);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintMultilineBidi(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintMultilineBidi(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintMultilineBidi(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintMultilineBidi(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintMultilineBidi(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_multiline_dom');
|
||||
});
|
||||
|
||||
void paintMultSpanBidi(
|
||||
EngineCanvas canvas,
|
||||
Rect bounds,
|
||||
double y,
|
||||
TextDirection textDirection,
|
||||
TextAlign textAlign,
|
||||
) {
|
||||
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textDirection: textDirection,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
// '''
|
||||
// Lorem 12 $_rtlWord1
|
||||
// $_rtlWord2 34 ipsum
|
||||
// dolor 56
|
||||
// '''
|
||||
final CanvasParagraph paragraph = rich(paragraphStyle, (builder) {
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('Lorem ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: green));
|
||||
builder.addText('12 ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: red));
|
||||
builder.addText('$_rtlWord1 ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: black));
|
||||
builder.addText('$_rtlWord2 ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('34 ipsum ');
|
||||
builder.pushStyle(EngineTextStyle.only(color: green));
|
||||
builder.addText('dolor 56 ');
|
||||
});
|
||||
final double maxWidth = bounds.width - 10;
|
||||
paragraph.layout(constrain(maxWidth));
|
||||
canvas.drawParagraph(paragraph, Offset(bounds.left + 5, bounds.top + y + 5));
|
||||
}
|
||||
|
||||
test('multi span bidi', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 900);
|
||||
final canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
const double height = 95;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 150, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintMultSpanBidi(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintMultSpanBidi(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintMultSpanBidi(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintMultSpanBidi(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintMultSpanBidi(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(ltrBox.width + 10, 0);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintMultSpanBidi(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintMultSpanBidi(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintMultSpanBidi(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintMultSpanBidi(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintMultSpanBidi(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_multispan');
|
||||
});
|
||||
|
||||
test('multi span bidi (DOM)', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 900);
|
||||
final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
|
||||
|
||||
const double height = 95;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 150, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintMultSpanBidi(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintMultSpanBidi(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintMultSpanBidi(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintMultSpanBidi(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintMultSpanBidi(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(ltrBox.width + 10, 0);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintMultSpanBidi(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintMultSpanBidi(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintMultSpanBidi(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintMultSpanBidi(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintMultSpanBidi(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_multispan_dom');
|
||||
});
|
||||
|
||||
void paintBidiWithSelection(
|
||||
EngineCanvas canvas,
|
||||
Rect bounds,
|
||||
double y,
|
||||
TextDirection textDirection,
|
||||
TextAlign textAlign,
|
||||
) {
|
||||
// '''
|
||||
// Lorem 12 $_rtlWord1
|
||||
// $_rtlWord2 34 ipsum
|
||||
// dolor 56
|
||||
// '''
|
||||
const String text = 'Lorem 12 $_rtlWord1 $_rtlWord2 34 ipsum dolor 56';
|
||||
|
||||
final EngineParagraphStyle paragraphStyle = EngineParagraphStyle(
|
||||
fontFamily: 'Roboto',
|
||||
fontSize: 20.0,
|
||||
textDirection: textDirection,
|
||||
textAlign: textAlign,
|
||||
);
|
||||
final CanvasParagraph paragraph = plain(
|
||||
paragraphStyle,
|
||||
text,
|
||||
textStyle: EngineTextStyle.only(color: blue),
|
||||
);
|
||||
|
||||
final double maxWidth = bounds.width - 10;
|
||||
paragraph.layout(constrain(maxWidth));
|
||||
|
||||
final Offset offset = Offset(bounds.left + 5, bounds.top + y + 5);
|
||||
|
||||
// Range for "em 12 " and the first character of `_rtlWord1`.
|
||||
paintBoxes(canvas, offset, paragraph.getBoxesForRange(3, 10), lightBlue);
|
||||
// Range for the second half of `_rtlWord1` and all of `_rtlWord2` and " 3".
|
||||
paintBoxes(canvas, offset, paragraph.getBoxesForRange(11, 21), lightPurple);
|
||||
// Range for "psum dolo".
|
||||
paintBoxes(canvas, offset, paragraph.getBoxesForRange(24, 33), green);
|
||||
|
||||
canvas.drawParagraph(paragraph, offset);
|
||||
}
|
||||
|
||||
test('bidi with selection', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 500);
|
||||
final canvas = BitmapCanvas(bounds, RenderStrategy());
|
||||
|
||||
const double height = 95;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 150, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintBidiWithSelection(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintBidiWithSelection(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintBidiWithSelection(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintBidiWithSelection(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintBidiWithSelection(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(ltrBox.width + 10, 0);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintBidiWithSelection(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintBidiWithSelection(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintBidiWithSelection(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintBidiWithSelection(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintBidiWithSelection(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_selection');
|
||||
});
|
||||
|
||||
test('bidi with selection (DOM)', () {
|
||||
const Rect bounds = Rect.fromLTWH(0, 0, 400, 500);
|
||||
final canvas = DomCanvas(domRenderer.createElement('flt-picture'));
|
||||
|
||||
const double height = 95;
|
||||
|
||||
// Border for ltr paragraphs.
|
||||
final Rect ltrBox = Rect.fromLTWH(0, 0, 150, 5 * height).inflate(5).translate(10, 10);
|
||||
canvas.drawRect(
|
||||
ltrBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// LTR with different text align values:
|
||||
paintBidiWithSelection(canvas, ltrBox, 0 * height, TextDirection.ltr, TextAlign.left);
|
||||
paintBidiWithSelection(canvas, ltrBox, 1 * height, TextDirection.ltr, TextAlign.right);
|
||||
paintBidiWithSelection(canvas, ltrBox, 2 * height, TextDirection.ltr, TextAlign.center);
|
||||
paintBidiWithSelection(canvas, ltrBox, 3 * height, TextDirection.ltr, TextAlign.start);
|
||||
paintBidiWithSelection(canvas, ltrBox, 4 * height, TextDirection.ltr, TextAlign.end);
|
||||
|
||||
// Border for rtl paragraphs.
|
||||
final Rect rtlBox = ltrBox.translate(ltrBox.width + 10, 0);
|
||||
canvas.drawRect(
|
||||
rtlBox,
|
||||
SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke,
|
||||
);
|
||||
// RTL with different text align values:
|
||||
paintBidiWithSelection(canvas, rtlBox, 0 * height, TextDirection.rtl, TextAlign.left);
|
||||
paintBidiWithSelection(canvas, rtlBox, 1 * height, TextDirection.rtl, TextAlign.right);
|
||||
paintBidiWithSelection(canvas, rtlBox, 2 * height, TextDirection.rtl, TextAlign.center);
|
||||
paintBidiWithSelection(canvas, rtlBox, 3 * height, TextDirection.rtl, TextAlign.start);
|
||||
paintBidiWithSelection(canvas, rtlBox, 4 * height, TextDirection.rtl, TextAlign.end);
|
||||
|
||||
return takeScreenshot(canvas, bounds, 'canvas_paragraph_bidi_selection_dom');
|
||||
});
|
||||
}
|
||||
|
||||
void paintBoxes(EngineCanvas canvas, Offset offset, List<TextBox> boxes, Color color) {
|
||||
for (final TextBox box in boxes) {
|
||||
final Rect rect = box.toRect().shift(offset);
|
||||
canvas.drawRect(rect, SurfacePaintData()..color = color);
|
||||
}
|
||||
}
|
||||
@ -11,14 +11,30 @@ import 'package:web_engine_tester/golden_tester.dart';
|
||||
const Color white = Color(0xFFFFFFFF);
|
||||
const Color black = Color(0xFF000000);
|
||||
const Color red = Color(0xFFFF0000);
|
||||
const Color lightGreen = Color(0xFFDCEDC8);
|
||||
const Color green = Color(0xFF00FF00);
|
||||
const Color lightBlue = Color(0xFFB3E5FC);
|
||||
const Color blue = Color(0xFF0000FF);
|
||||
const Color yellow = Color(0xFFFFEB3B);
|
||||
const Color lightPurple = Color(0xFFE1BEE7);
|
||||
|
||||
ParagraphConstraints constrain(double width) {
|
||||
return ParagraphConstraints(width: width);
|
||||
}
|
||||
|
||||
CanvasParagraph plain(
|
||||
EngineParagraphStyle style,
|
||||
String text, {
|
||||
EngineTextStyle? textStyle,
|
||||
}) {
|
||||
final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style);
|
||||
if (textStyle != null) {
|
||||
builder.pushStyle(textStyle);
|
||||
}
|
||||
builder.addText(text);
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
CanvasParagraph rich(
|
||||
EngineParagraphStyle style,
|
||||
void Function(CanvasParagraphBuilder) callback,
|
||||
|
||||
@ -84,7 +84,10 @@ void testMain() async {
|
||||
// "Lorem "
|
||||
paragraph.getBoxesForRange(0, 6),
|
||||
<ui.TextBox>[
|
||||
box(0, 0, 60, 10),
|
||||
// "Lorem"
|
||||
box(0, 0, 50, 10),
|
||||
// " "
|
||||
box(50, 0, 60, 10),
|
||||
],
|
||||
);
|
||||
|
||||
@ -101,7 +104,10 @@ void testMain() async {
|
||||
// "um "
|
||||
paragraph.getBoxesForRange(9, 12),
|
||||
<ui.TextBox>[
|
||||
box(90, 0, 120, 10),
|
||||
// "um"
|
||||
box(90, 0, 110, 10),
|
||||
// " "
|
||||
box(110, 0, 120, 10),
|
||||
],
|
||||
);
|
||||
|
||||
@ -111,7 +117,11 @@ void testMain() async {
|
||||
// "rem ipsum"
|
||||
paragraph.getBoxesForRange(2, 11),
|
||||
<ui.TextBox>[
|
||||
box(20, 0, 60, 10),
|
||||
// "rem"
|
||||
box(20, 0, 50, 10),
|
||||
// " "
|
||||
box(50, 0, 60, 10),
|
||||
// "ipsum"
|
||||
box(60, 0, 110, 10),
|
||||
],
|
||||
);
|
||||
@ -119,11 +129,18 @@ void testMain() async {
|
||||
// Across all spans "Lorem ", "ipsum ", ".".
|
||||
|
||||
expect(
|
||||
// "Lorem ipsum."
|
||||
// "Lorem ipsum ."
|
||||
paragraph.getBoxesForRange(0, 13),
|
||||
<ui.TextBox>[
|
||||
box(0, 0, 60, 10),
|
||||
box(60, 0, 120, 10),
|
||||
// "Lorem"
|
||||
box(0, 0, 50, 10),
|
||||
// " "
|
||||
box(50, 0, 60, 10),
|
||||
// "ipsum"
|
||||
box(60, 0, 110, 10),
|
||||
// " "
|
||||
box(110, 0, 120, 10),
|
||||
// "."
|
||||
box(120, 0, 130, 10),
|
||||
],
|
||||
);
|
||||
@ -155,7 +172,10 @@ void testMain() async {
|
||||
// "Lorem "
|
||||
paragraph.getBoxesForRange(0, 6),
|
||||
<ui.TextBox>[
|
||||
box(0, 0, 60, 10),
|
||||
// "Lorem"
|
||||
box(0, 0, 50, 10),
|
||||
// " "
|
||||
box(50, 0, 60, 10),
|
||||
],
|
||||
);
|
||||
|
||||
@ -165,7 +185,10 @@ void testMain() async {
|
||||
// "psum "
|
||||
paragraph.getBoxesForRange(7, 12),
|
||||
<ui.TextBox>[
|
||||
box(10, 10, 60, 20),
|
||||
// "psum"
|
||||
box(10, 10, 50, 20),
|
||||
// " "
|
||||
box(50, 10, 60, 20),
|
||||
],
|
||||
);
|
||||
|
||||
@ -177,9 +200,20 @@ void testMain() async {
|
||||
// "dolor s"
|
||||
paragraph.getBoxesForRange(3, 19),
|
||||
<ui.TextBox>[
|
||||
box(30, 0, 60, 10),
|
||||
box(0, 10, 60, 20),
|
||||
box(0, 20, 70, 30),
|
||||
// "em"
|
||||
box(30, 0, 50, 10),
|
||||
// " "
|
||||
box(50, 0, 60, 10),
|
||||
// "ipsum"
|
||||
box(0, 10, 50, 20),
|
||||
// " "
|
||||
box(50, 10, 60, 20),
|
||||
// "dolor"
|
||||
box(0, 20, 50, 30),
|
||||
// " "
|
||||
box(50, 20, 60, 30),
|
||||
// "s"
|
||||
box(60, 20, 70, 30),
|
||||
],
|
||||
);
|
||||
});
|
||||
@ -213,7 +247,10 @@ void testMain() async {
|
||||
// "Lorem "
|
||||
paragraph.getBoxesForRange(0, 6),
|
||||
<ui.TextBox>[
|
||||
box(0, 0, 60, 10),
|
||||
// "Lorem"
|
||||
box(0, 0, 50, 10),
|
||||
// " "
|
||||
box(50, 0, 60, 10),
|
||||
],
|
||||
);
|
||||
|
||||
@ -223,7 +260,10 @@ void testMain() async {
|
||||
// "psum "
|
||||
paragraph.getBoxesForRange(7, 12),
|
||||
<ui.TextBox>[
|
||||
box(10, 10, 60, 20),
|
||||
// "psum"
|
||||
box(10, 10, 50, 20),
|
||||
// " "
|
||||
box(50, 10, 60, 20),
|
||||
],
|
||||
);
|
||||
|
||||
@ -233,7 +273,11 @@ void testMain() async {
|
||||
// "lor sit"
|
||||
paragraph.getBoxesForRange(14, 21),
|
||||
<ui.TextBox>[
|
||||
box(20, 20, 60, 30),
|
||||
// "lor"
|
||||
box(20, 20, 50, 30),
|
||||
// " "
|
||||
box(50, 20, 60, 30),
|
||||
// "sit"
|
||||
box(60, 20, 90, 30),
|
||||
],
|
||||
);
|
||||
@ -246,9 +290,19 @@ void testMain() async {
|
||||
// "dolor s"
|
||||
paragraph.getBoxesForRange(3, 19),
|
||||
<ui.TextBox>[
|
||||
box(30, 0, 60, 10),
|
||||
box(0, 10, 60, 20),
|
||||
box(0, 20, 60, 30),
|
||||
// "em"
|
||||
box(30, 0, 50, 10),
|
||||
// " "
|
||||
box(50, 0, 60, 10),
|
||||
// "ipsum"
|
||||
box(0, 10, 50, 20),
|
||||
// " "
|
||||
box(50, 10, 60, 20),
|
||||
// "dolor"
|
||||
box(0, 20, 50, 30),
|
||||
// " "
|
||||
box(50, 20, 60, 30),
|
||||
// "s"
|
||||
box(60, 20, 70, 30),
|
||||
],
|
||||
);
|
||||
@ -287,8 +341,15 @@ void testMain() async {
|
||||
// "em ipsum dol"
|
||||
paragraph.getBoxesForRange(3, 15),
|
||||
<ui.TextBox>[
|
||||
box(60, 16, 120, 36),
|
||||
box(120, 0, 360, 40),
|
||||
// "em"
|
||||
box(60, 16, 100, 36),
|
||||
// " "
|
||||
box(100, 16, 120, 36),
|
||||
// "ipsum"
|
||||
box(120, 0, 320, 40),
|
||||
// " "
|
||||
box(320, 0, 360, 40),
|
||||
// "dol"
|
||||
box(360, 24, 390, 34),
|
||||
],
|
||||
);
|
||||
@ -298,9 +359,19 @@ void testMain() async {
|
||||
// "sit amet"
|
||||
paragraph.getBoxesForRange(8, 26),
|
||||
<ui.TextBox>[
|
||||
box(200, 0, 360, 40),
|
||||
box(360, 24, 420, 34),
|
||||
box(0, 40, 120, 70),
|
||||
// "sum"
|
||||
box(200, 0, 320, 40),
|
||||
// " "
|
||||
box(320, 0, 360, 40),
|
||||
// "dolor"
|
||||
box(360, 24, 410, 34),
|
||||
// " "
|
||||
box(410, 24, 420, 34),
|
||||
// "sit"
|
||||
box(0, 40, 90, 70),
|
||||
// " "
|
||||
box(90, 40, 120, 70),
|
||||
// "amet"
|
||||
box(120, 48, 200, 68),
|
||||
],
|
||||
);
|
||||
|
||||
126
engine/src/flutter/lib/web_ui/test/text/text_direction_test.dart
Normal file
126
engine/src/flutter/lib/web_ui/test/text/text_direction_test.dart
Normal file
@ -0,0 +1,126 @@
|
||||
// Copyright 2013 The Flutter Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
|
||||
import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
|
||||
// Two RTL strings, 5 characters each, to match the length of "$rtl1" and "$rtl2".
|
||||
const String rtl1 = 'واحدة';
|
||||
const String rtl2 = 'ثنتان';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() async {
|
||||
group('$getDirectionalBlockEnd', () {
|
||||
|
||||
test('basic cases', () {
|
||||
const String text = 'Lorem 12 $rtl1 ipsum34';
|
||||
const LineBreakResult start = LineBreakResult.sameIndex(0, LineBreakType.prohibited);
|
||||
const LineBreakResult end = LineBreakResult.sameIndex(text.length, LineBreakType.endOfText);
|
||||
const LineBreakResult loremMiddle = LineBreakResult.sameIndex(3, LineBreakType.prohibited);
|
||||
const LineBreakResult loremEnd = LineBreakResult.sameIndex(5, LineBreakType.prohibited);
|
||||
const LineBreakResult twelveStart = LineBreakResult(6, 6, 5, LineBreakType.opportunity);
|
||||
const LineBreakResult twelveEnd = LineBreakResult.sameIndex(8, LineBreakType.prohibited);
|
||||
const LineBreakResult rtl1Start = LineBreakResult(9, 9, 8, LineBreakType.opportunity);
|
||||
const LineBreakResult rtl1End = LineBreakResult.sameIndex(14, LineBreakType.prohibited);
|
||||
const LineBreakResult ipsumStart = LineBreakResult(17, 17, 15, LineBreakType.opportunity);
|
||||
const LineBreakResult ipsumEnd = LineBreakResult.sameIndex(22, LineBreakType.prohibited);
|
||||
|
||||
DirectionalPosition blockEnd;
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, start, end);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, loremEnd);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, start, loremMiddle);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, loremMiddle);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, loremMiddle, loremEnd);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, loremEnd);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, loremEnd, twelveStart);
|
||||
expect(blockEnd.isSpaceOnly, isTrue);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, twelveStart);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, twelveStart, rtl1Start);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, twelveEnd);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, rtl1Start, end);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.rtl);
|
||||
expect(blockEnd.lineBreak, rtl1End);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, ipsumStart, end);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, ipsumEnd);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, ipsumEnd, end);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, end);
|
||||
});
|
||||
|
||||
test('handles new lines', () {
|
||||
const String text = 'Lorem\n12\nipsum \n';
|
||||
const LineBreakResult start = LineBreakResult.sameIndex(0, LineBreakType.prohibited);
|
||||
const LineBreakResult end = LineBreakResult(
|
||||
text.length,
|
||||
text.length - 1,
|
||||
text.length - 3,
|
||||
LineBreakType.mandatory,
|
||||
);
|
||||
const LineBreakResult loremEnd = LineBreakResult.sameIndex(5, LineBreakType.prohibited);
|
||||
const LineBreakResult twelveStart = LineBreakResult(6, 5, 5, LineBreakType.mandatory);
|
||||
const LineBreakResult twelveEnd = LineBreakResult.sameIndex(8, LineBreakType.prohibited);
|
||||
const LineBreakResult ipsumStart = LineBreakResult(9, 8, 8, LineBreakType.mandatory);
|
||||
const LineBreakResult ipsumEnd = LineBreakResult.sameIndex(14, LineBreakType.prohibited);
|
||||
|
||||
DirectionalPosition blockEnd;
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, start, twelveStart);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, twelveStart);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, loremEnd, twelveStart);
|
||||
expect(blockEnd.isSpaceOnly, isTrue);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, twelveStart);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, twelveStart, ipsumStart);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, ipsumStart);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, twelveEnd, ipsumStart);
|
||||
expect(blockEnd.isSpaceOnly, isTrue);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, ipsumStart);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, ipsumStart, end);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, ipsumEnd);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, ipsumEnd, end);
|
||||
expect(blockEnd.isSpaceOnly, isTrue);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, end);
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user