mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
[web] Separate text fragmenting from layout (flutter/engine#34085)
This commit is contained in:
parent
c2fed6dadf
commit
5ae7bb2623
@ -1926,6 +1926,8 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/svg.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/test_embedding.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/canvas_paragraph.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/font_collection.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/fragmenter.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/layout_fragmenter.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/layout_service.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_break_properties.dart
|
||||
FILE: ../../../flutter/lib/web_ui/lib/src/engine/text/line_breaker.dart
|
||||
|
||||
@ -148,6 +148,8 @@ export 'engine/svg.dart';
|
||||
export 'engine/test_embedding.dart';
|
||||
export 'engine/text/canvas_paragraph.dart';
|
||||
export 'engine/text/font_collection.dart';
|
||||
export 'engine/text/fragmenter.dart';
|
||||
export 'engine/text/layout_fragmenter.dart';
|
||||
export 'engine/text/layout_service.dart';
|
||||
export 'engine/text/line_break_properties.dart';
|
||||
export 'engine/text/line_breaker.dart';
|
||||
|
||||
@ -634,6 +634,8 @@ extension DomCanvasRenderingContext2DExtension on DomCanvasRenderingContext2D {
|
||||
external set fillStyle(Object? style);
|
||||
external String get font;
|
||||
external set font(String value);
|
||||
external String get direction;
|
||||
external set direction(String value);
|
||||
external set lineWidth(num? value);
|
||||
external set strokeStyle(Object? value);
|
||||
external Object? get strokeStyle;
|
||||
|
||||
@ -913,9 +913,11 @@ class BitmapCanvas extends EngineCanvas {
|
||||
_cachedLastCssFont = null;
|
||||
}
|
||||
|
||||
void setCssFont(String cssFont) {
|
||||
void setCssFont(String cssFont, ui.TextDirection textDirection) {
|
||||
final DomCanvasRenderingContext2D ctx = _canvasPool.context;
|
||||
ctx.direction = textDirection == ui.TextDirection.ltr ? 'ltr' : 'rtl';
|
||||
|
||||
if (cssFont != _cachedLastCssFont) {
|
||||
final DomCanvasRenderingContext2D ctx = _canvasPool.context;
|
||||
ctx.font = cssFont;
|
||||
_cachedLastCssFont = cssFont;
|
||||
}
|
||||
|
||||
@ -9,6 +9,7 @@ import '../embedder.dart';
|
||||
import '../html/bitmap_canvas.dart';
|
||||
import '../profiler.dart';
|
||||
import '../util.dart';
|
||||
import 'layout_fragmenter.dart';
|
||||
import 'layout_service.dart';
|
||||
import 'paint_service.dart';
|
||||
import 'paragraph.dart';
|
||||
@ -16,6 +17,8 @@ import 'word_breaker.dart';
|
||||
|
||||
const ui.Color _defaultTextColor = ui.Color(0xFFFF0000);
|
||||
|
||||
final String _placeholderChar = String.fromCharCode(0xFFFC);
|
||||
|
||||
/// A paragraph made up of a flat list of text spans and placeholders.
|
||||
///
|
||||
/// [CanvasParagraph] doesn't use a DOM element to represent the structure of
|
||||
@ -32,7 +35,7 @@ class CanvasParagraph implements ui.Paragraph {
|
||||
required this.plainText,
|
||||
required this.placeholderCount,
|
||||
required this.canDrawOnCanvas,
|
||||
});
|
||||
}) : assert(spans.isNotEmpty);
|
||||
|
||||
/// The flat list of spans that make up this paragraph.
|
||||
final List<ParagraphSpan> spans;
|
||||
@ -168,38 +171,28 @@ class CanvasParagraph implements ui.Paragraph {
|
||||
|
||||
// 2. Append all spans to the paragraph.
|
||||
|
||||
DomHTMLElement? lastSpanElement;
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
final ParagraphLine line = lines[i];
|
||||
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) {
|
||||
lastSpanElement = domDocument.createElement('flt-span') as
|
||||
DomHTMLElement;
|
||||
applyTextStyleToElement(
|
||||
element: lastSpanElement,
|
||||
style: box.span.style,
|
||||
isSpan: true,
|
||||
);
|
||||
_positionSpanElement(lastSpanElement, line, box);
|
||||
lastSpanElement.appendText(box.toText());
|
||||
rootElement.append(lastSpanElement);
|
||||
buffer.write(box.toText());
|
||||
} else if (box is PlaceholderBox) {
|
||||
lastSpanElement = null;
|
||||
} else {
|
||||
throw UnimplementedError('Unknown box type: ${box.runtimeType}');
|
||||
for (final LayoutFragment fragment in line.fragments) {
|
||||
if (fragment.isPlaceholder) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
final String? ellipsis = line.ellipsis;
|
||||
if (ellipsis != null) {
|
||||
(lastSpanElement ?? rootElement).appendText(ellipsis);
|
||||
final String text = fragment.getText(this);
|
||||
if (text.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final DomHTMLElement spanElement = domDocument.createElement('flt-span') as DomHTMLElement;
|
||||
applyTextStyleToElement(
|
||||
element: spanElement,
|
||||
style: fragment.style,
|
||||
isSpan: true,
|
||||
);
|
||||
_positionSpanElement(spanElement, line, fragment);
|
||||
|
||||
spanElement.appendText(text);
|
||||
rootElement.append(spanElement);
|
||||
}
|
||||
}
|
||||
|
||||
@ -283,8 +276,8 @@ class CanvasParagraph implements ui.Paragraph {
|
||||
}
|
||||
}
|
||||
|
||||
void _positionSpanElement(DomElement element, ParagraphLine line, RangeBox box) {
|
||||
final ui.Rect boxRect = box.toTextBox(line, forPainting: true).toRect();
|
||||
void _positionSpanElement(DomElement element, ParagraphLine line, LayoutFragment fragment) {
|
||||
final ui.Rect boxRect = fragment.toPaintingTextBox().toRect();
|
||||
element.style
|
||||
..position = 'absolute'
|
||||
..top = '${boxRect.top}px'
|
||||
@ -304,6 +297,9 @@ abstract class ParagraphSpan {
|
||||
|
||||
/// The index of the end of the range of text represented by this span.
|
||||
int get end;
|
||||
|
||||
/// The resolved style of the span.
|
||||
EngineTextStyle get style;
|
||||
}
|
||||
|
||||
/// Represent a span of text in the paragraph.
|
||||
@ -323,7 +319,7 @@ class FlatTextSpan implements ParagraphSpan {
|
||||
required this.end,
|
||||
});
|
||||
|
||||
/// The resolved style of the span.
|
||||
@override
|
||||
final EngineTextStyle style;
|
||||
|
||||
@override
|
||||
@ -341,14 +337,24 @@ class FlatTextSpan implements ParagraphSpan {
|
||||
|
||||
class PlaceholderSpan extends ParagraphPlaceholder implements ParagraphSpan {
|
||||
PlaceholderSpan(
|
||||
int index,
|
||||
super.width,
|
||||
super.height,
|
||||
super.alignment, {
|
||||
required super.baselineOffset,
|
||||
required super.baseline,
|
||||
}) : start = index,
|
||||
end = index;
|
||||
this.style,
|
||||
this.start,
|
||||
this.end,
|
||||
double width,
|
||||
double height,
|
||||
ui.PlaceholderAlignment alignment, {
|
||||
required double baselineOffset,
|
||||
required ui.TextBaseline baseline,
|
||||
}) : super(
|
||||
width,
|
||||
height,
|
||||
alignment,
|
||||
baselineOffset: baselineOffset,
|
||||
baseline: baseline,
|
||||
);
|
||||
|
||||
@override
|
||||
final EngineTextStyle style;
|
||||
|
||||
@override
|
||||
final int start;
|
||||
@ -624,10 +630,19 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
|
||||
alignment == ui.PlaceholderAlignment.belowBaseline ||
|
||||
alignment == ui.PlaceholderAlignment.baseline) || baseline != null);
|
||||
|
||||
final int start = _plainTextBuffer.length;
|
||||
_plainTextBuffer.write(_placeholderChar);
|
||||
final int end = _plainTextBuffer.length;
|
||||
|
||||
final EngineTextStyle style = _currentStyleNode.resolveStyle();
|
||||
_updateCanDrawOnCanvas(style);
|
||||
|
||||
_placeholderCount++;
|
||||
_placeholderScales.add(scale);
|
||||
_spans.add(PlaceholderSpan(
|
||||
_plainTextBuffer.length,
|
||||
style,
|
||||
start,
|
||||
end,
|
||||
width * scale,
|
||||
height * scale,
|
||||
alignment,
|
||||
@ -652,37 +667,54 @@ class CanvasParagraphBuilder implements ui.ParagraphBuilder {
|
||||
|
||||
@override
|
||||
void addText(String text) {
|
||||
final EngineTextStyle style = _currentStyleNode.resolveStyle();
|
||||
final int start = _plainTextBuffer.length;
|
||||
_plainTextBuffer.write(text);
|
||||
final int end = _plainTextBuffer.length;
|
||||
|
||||
if (_canDrawOnCanvas) {
|
||||
final ui.TextDecoration? decoration = style.decoration;
|
||||
if (decoration != null && decoration != ui.TextDecoration.none) {
|
||||
_canDrawOnCanvas = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_canDrawOnCanvas) {
|
||||
final List<ui.FontFeature>? fontFeatures = style.fontFeatures;
|
||||
if (fontFeatures != null && fontFeatures.isNotEmpty) {
|
||||
_canDrawOnCanvas = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (_canDrawOnCanvas) {
|
||||
final List<ui.FontVariation>? fontVariations = style.fontVariations;
|
||||
if (fontVariations != null && fontVariations.isNotEmpty) {
|
||||
_canDrawOnCanvas = false;
|
||||
}
|
||||
}
|
||||
final EngineTextStyle style = _currentStyleNode.resolveStyle();
|
||||
_updateCanDrawOnCanvas(style);
|
||||
|
||||
_spans.add(FlatTextSpan(style: style, start: start, end: end));
|
||||
}
|
||||
|
||||
void _updateCanDrawOnCanvas(EngineTextStyle style) {
|
||||
if (!_canDrawOnCanvas) {
|
||||
return;
|
||||
}
|
||||
|
||||
final ui.TextDecoration? decoration = style.decoration;
|
||||
if (decoration != null && decoration != ui.TextDecoration.none) {
|
||||
_canDrawOnCanvas = false;
|
||||
return;
|
||||
}
|
||||
|
||||
final List<ui.FontFeature>? fontFeatures = style.fontFeatures;
|
||||
if (fontFeatures != null && fontFeatures.isNotEmpty) {
|
||||
_canDrawOnCanvas = false;
|
||||
return;
|
||||
}
|
||||
|
||||
final List<ui.FontVariation>? fontVariations = style.fontVariations;
|
||||
if (fontVariations != null && fontVariations.isNotEmpty) {
|
||||
_canDrawOnCanvas = false;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
CanvasParagraph build() {
|
||||
if (_spans.isEmpty) {
|
||||
// In case `addText` and `addPlaceholder` were never called.
|
||||
//
|
||||
// We want the paragraph to always have a non-empty list of spans to match
|
||||
// the expectations of the [LayoutFragmenter].
|
||||
_spans.add(FlatTextSpan(
|
||||
style: _rootStyleNode.resolveStyle(),
|
||||
start: 0,
|
||||
end: 0,
|
||||
));
|
||||
}
|
||||
|
||||
return CanvasParagraph(
|
||||
_spans,
|
||||
paragraphStyle: _paragraphStyle,
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
// 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.
|
||||
|
||||
/// Splits [text] into a list of [TextFragment]s.
|
||||
///
|
||||
/// Various subclasses can perform the fragmenting based on their own criteria.
|
||||
///
|
||||
/// See:
|
||||
///
|
||||
/// - [LineBreakFragmenter]: Fragments text based on line break opportunities.
|
||||
/// - [BidiFragmenter]: Fragments text based on directionality.
|
||||
abstract class TextFragmenter {
|
||||
const TextFragmenter(this.text);
|
||||
|
||||
/// The text to be fragmented.
|
||||
final String text;
|
||||
|
||||
/// Performs the fragmenting of [text] and returns a list of [TextFragment]s.
|
||||
List<TextFragment> fragment();
|
||||
}
|
||||
|
||||
/// Represents a fragment produced by [TextFragmenter].
|
||||
abstract class TextFragment {
|
||||
const TextFragment(this.start, this.end);
|
||||
|
||||
final int start;
|
||||
final int end;
|
||||
|
||||
/// Whether this fragment's range overlaps with the range from [start] to [end].
|
||||
bool overlapsWith(int start, int end) {
|
||||
return start < this.end && this.start < end;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,628 @@
|
||||
// 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:math' as math;
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import '../util.dart';
|
||||
import 'canvas_paragraph.dart';
|
||||
import 'fragmenter.dart';
|
||||
import 'layout_service.dart';
|
||||
import 'line_breaker.dart';
|
||||
import 'paragraph.dart';
|
||||
import 'text_direction.dart';
|
||||
|
||||
/// Splits [text] into fragments that are ready to be laid out by
|
||||
/// [TextLayoutService].
|
||||
///
|
||||
/// This fragmenter takes into account line breaks, directionality and styles.
|
||||
class LayoutFragmenter extends TextFragmenter {
|
||||
const LayoutFragmenter(super.text, this.paragraphSpans);
|
||||
|
||||
final List<ParagraphSpan> paragraphSpans;
|
||||
|
||||
@override
|
||||
List<LayoutFragment> fragment() {
|
||||
final List<LayoutFragment> fragments = <LayoutFragment>[];
|
||||
|
||||
int fragmentStart = 0;
|
||||
|
||||
final Iterator<LineBreakFragment> lineBreakFragments = LineBreakFragmenter(text).fragment().iterator..moveNext();
|
||||
final Iterator<BidiFragment> bidiFragments = BidiFragmenter(text).fragment().iterator..moveNext();
|
||||
final Iterator<ParagraphSpan> spans = paragraphSpans.iterator..moveNext();
|
||||
|
||||
LineBreakFragment currentLineBreakFragment = lineBreakFragments.current;
|
||||
BidiFragment currentBidiFragment = bidiFragments.current;
|
||||
ParagraphSpan currentSpan = spans.current;
|
||||
|
||||
while (true) {
|
||||
final int fragmentEnd = math.min(
|
||||
currentLineBreakFragment.end,
|
||||
math.min(
|
||||
currentBidiFragment.end,
|
||||
currentSpan.end,
|
||||
),
|
||||
);
|
||||
|
||||
final int distanceFromLineBreak = currentLineBreakFragment.end - fragmentEnd;
|
||||
|
||||
final LineBreakType lineBreakType = distanceFromLineBreak == 0
|
||||
? currentLineBreakFragment.type
|
||||
: LineBreakType.prohibited;
|
||||
|
||||
final int trailingNewlines = currentLineBreakFragment.trailingNewlines - distanceFromLineBreak;
|
||||
final int trailingSpaces = currentLineBreakFragment.trailingSpaces - distanceFromLineBreak;
|
||||
|
||||
final int fragmentLength = fragmentEnd - fragmentStart;
|
||||
fragments.add(LayoutFragment(
|
||||
fragmentStart,
|
||||
fragmentEnd,
|
||||
lineBreakType,
|
||||
currentBidiFragment.textDirection,
|
||||
currentBidiFragment.fragmentFlow,
|
||||
currentSpan,
|
||||
trailingNewlines: clampInt(trailingNewlines, 0, fragmentLength),
|
||||
trailingSpaces: clampInt(trailingSpaces, 0, fragmentLength),
|
||||
));
|
||||
|
||||
fragmentStart = fragmentEnd;
|
||||
|
||||
bool moved = false;
|
||||
if (currentLineBreakFragment.end == fragmentEnd) {
|
||||
if (lineBreakFragments.moveNext()) {
|
||||
moved = true;
|
||||
currentLineBreakFragment = lineBreakFragments.current;
|
||||
}
|
||||
}
|
||||
if (currentBidiFragment.end == fragmentEnd) {
|
||||
if (bidiFragments.moveNext()) {
|
||||
moved = true;
|
||||
currentBidiFragment = bidiFragments.current;
|
||||
}
|
||||
}
|
||||
if (currentSpan.end == fragmentEnd) {
|
||||
if (spans.moveNext()) {
|
||||
moved = true;
|
||||
currentSpan = spans.current;
|
||||
}
|
||||
}
|
||||
|
||||
// Once we reached the end of all fragments, exit the loop.
|
||||
if (!moved) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return fragments;
|
||||
}
|
||||
}
|
||||
|
||||
abstract class _CombinedFragment extends TextFragment {
|
||||
_CombinedFragment(
|
||||
super.start,
|
||||
super.end,
|
||||
this.type,
|
||||
this._textDirection,
|
||||
this.fragmentFlow,
|
||||
this.span, {
|
||||
required this.trailingNewlines,
|
||||
required this.trailingSpaces,
|
||||
}) : assert(trailingNewlines >= 0),
|
||||
assert(trailingSpaces >= trailingNewlines);
|
||||
|
||||
final LineBreakType type;
|
||||
|
||||
ui.TextDirection? get textDirection => _textDirection;
|
||||
ui.TextDirection? _textDirection;
|
||||
|
||||
final FragmentFlow fragmentFlow;
|
||||
|
||||
final ParagraphSpan span;
|
||||
|
||||
final int trailingNewlines;
|
||||
|
||||
final int trailingSpaces;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
start,
|
||||
end,
|
||||
type,
|
||||
textDirection,
|
||||
fragmentFlow,
|
||||
span,
|
||||
trailingNewlines,
|
||||
trailingSpaces,
|
||||
);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is LayoutFragment &&
|
||||
other.start == start &&
|
||||
other.end == end &&
|
||||
other.type == type &&
|
||||
other.textDirection == textDirection &&
|
||||
other.fragmentFlow == fragmentFlow &&
|
||||
other.span == span &&
|
||||
other.trailingNewlines == trailingNewlines &&
|
||||
other.trailingSpaces == trailingSpaces;
|
||||
}
|
||||
}
|
||||
|
||||
class LayoutFragment extends _CombinedFragment with _FragmentMetrics, _FragmentPosition, _FragmentBox {
|
||||
LayoutFragment(
|
||||
super.start,
|
||||
super.end,
|
||||
super.type,
|
||||
super.textDirection,
|
||||
super.fragmentFlow,
|
||||
super.span, {
|
||||
required super.trailingNewlines,
|
||||
required super.trailingSpaces,
|
||||
});
|
||||
|
||||
int get length => end - start;
|
||||
bool get isSpaceOnly => length == trailingSpaces;
|
||||
bool get isPlaceholder => span is PlaceholderSpan;
|
||||
bool get isBreak => type != LineBreakType.prohibited;
|
||||
bool get isHardBreak => type == LineBreakType.mandatory || type == LineBreakType.endOfText;
|
||||
EngineTextStyle get style => span.style;
|
||||
|
||||
/// Returns the substring from [paragraph] that corresponds to this fragment,
|
||||
/// excluding new line characters.
|
||||
String getText(CanvasParagraph paragraph) {
|
||||
return paragraph.plainText.substring(start, end - trailingNewlines);
|
||||
}
|
||||
|
||||
/// Splits this fragment into two fragments with the split point being the
|
||||
/// given [index].
|
||||
// TODO(mdebbar): If we ever get multiple return values in Dart, we should use it!
|
||||
// See: https://github.com/dart-lang/language/issues/68
|
||||
List<LayoutFragment?> split(int index) {
|
||||
assert(start <= index);
|
||||
assert(index <= end);
|
||||
|
||||
if (start == index) {
|
||||
return <LayoutFragment?>[null, this];
|
||||
}
|
||||
|
||||
if (end == index) {
|
||||
return <LayoutFragment?>[this, null];
|
||||
}
|
||||
|
||||
// The length of the second fragment after the split.
|
||||
final int secondLength = end - index;
|
||||
|
||||
// Trailing spaces/new lines go to the second fragment. Any left over goes
|
||||
// to the first fragment.
|
||||
final int secondTrailingNewlines = math.min(trailingNewlines, secondLength);
|
||||
final int secondTrailingSpaces = math.min(trailingSpaces, secondLength);
|
||||
|
||||
return <LayoutFragment>[
|
||||
LayoutFragment(
|
||||
start,
|
||||
index,
|
||||
LineBreakType.prohibited,
|
||||
textDirection,
|
||||
fragmentFlow,
|
||||
span,
|
||||
trailingNewlines: trailingNewlines - secondTrailingNewlines,
|
||||
trailingSpaces: trailingSpaces - secondTrailingSpaces,
|
||||
),
|
||||
LayoutFragment(
|
||||
index,
|
||||
end,
|
||||
type,
|
||||
textDirection,
|
||||
fragmentFlow,
|
||||
span,
|
||||
trailingNewlines: secondTrailingNewlines,
|
||||
trailingSpaces: secondTrailingSpaces,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$LayoutFragment($start, $end, $type, $textDirection)';
|
||||
}
|
||||
}
|
||||
|
||||
mixin _FragmentMetrics on _CombinedFragment {
|
||||
late Spanometer _spanometer;
|
||||
|
||||
/// The rise from the baseline as calculated from the font and style for this text.
|
||||
double get ascent => _ascent;
|
||||
late double _ascent;
|
||||
|
||||
/// The drop from the baseline as calculated from the font and style for this text.
|
||||
double get descent => _descent;
|
||||
late double _descent;
|
||||
|
||||
/// The width of the measured text, not including trailing spaces.
|
||||
double get widthExcludingTrailingSpaces => _widthExcludingTrailingSpaces;
|
||||
late double _widthExcludingTrailingSpaces;
|
||||
|
||||
/// The width of the measured text, including any trailing spaces.
|
||||
double get widthIncludingTrailingSpaces => _widthIncludingTrailingSpaces + _extraWidthForJustification;
|
||||
late double _widthIncludingTrailingSpaces;
|
||||
|
||||
double _extraWidthForJustification = 0.0;
|
||||
|
||||
/// The total height as calculated from the font and style for this text.
|
||||
double get height => ascent + descent;
|
||||
|
||||
double get widthOfTrailingSpaces => widthIncludingTrailingSpaces - widthExcludingTrailingSpaces;
|
||||
|
||||
/// Set measurement values for the fragment.
|
||||
void setMetrics(Spanometer spanometer, {
|
||||
required double ascent,
|
||||
required double descent,
|
||||
required double widthExcludingTrailingSpaces,
|
||||
required double widthIncludingTrailingSpaces,
|
||||
}) {
|
||||
_spanometer = spanometer;
|
||||
_ascent = ascent;
|
||||
_descent = descent;
|
||||
_widthExcludingTrailingSpaces = widthExcludingTrailingSpaces;
|
||||
_widthIncludingTrailingSpaces = widthIncludingTrailingSpaces;
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates positioning of the fragment relative to the line.
|
||||
///
|
||||
/// The 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 fragment.
|
||||
///
|
||||
/// This is what the various measurements/coordinates look like for a fragment
|
||||
/// in an LTR paragraph:
|
||||
///
|
||||
/// *------------------------line.width-----------------*
|
||||
/// *---width----*
|
||||
/// ┌─────────────────┬────────────┬────────────────────┐
|
||||
/// │ │--FRAGMENT--│ │
|
||||
/// └─────────────────┴────────────┴────────────────────┘
|
||||
/// *---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:
|
||||
///
|
||||
/// *------------------------line.width-----------------*
|
||||
/// *---width----*
|
||||
/// ┌─────────────────┬────────────┬────────────────────┐
|
||||
/// │ │--FRAGMENT--│ │
|
||||
/// └─────────────────┴────────────┴────────────────────┘
|
||||
/// *----startOffset-----*
|
||||
/// *------left-------*
|
||||
/// *-----------endOffset-------------*
|
||||
/// *----------right---------------*
|
||||
///
|
||||
mixin _FragmentPosition on _CombinedFragment, _FragmentMetrics {
|
||||
/// The distance from the beginning of the line to the beginning of the fragment.
|
||||
double get startOffset => _startOffset;
|
||||
late double _startOffset;
|
||||
|
||||
/// The width of the line that contains this fragment.
|
||||
late ParagraphLine line;
|
||||
|
||||
/// The distance from the beginning of the line to the end of the fragment.
|
||||
double get endOffset => startOffset + widthIncludingTrailingSpaces;
|
||||
|
||||
/// The distance from the left edge of the line to the left edge of the fragment.
|
||||
double get left => line.textDirection == ui.TextDirection.ltr
|
||||
? startOffset
|
||||
: line.width - endOffset;
|
||||
|
||||
/// The distance from the left edge of the line to the right edge of the fragment.
|
||||
double get right => line.textDirection == ui.TextDirection.ltr
|
||||
? endOffset
|
||||
: line.width - startOffset;
|
||||
|
||||
/// Set the horizontal position of this fragment relative to the [line] that
|
||||
/// contains it.
|
||||
void setPosition({
|
||||
required double startOffset,
|
||||
required ui.TextDirection textDirection,
|
||||
}) {
|
||||
_startOffset = startOffset;
|
||||
_textDirection ??= textDirection;
|
||||
}
|
||||
|
||||
/// Adjust the width of this fragment for paragraph justification.
|
||||
void justifyTo({required double paragraphWidth}) {
|
||||
// Only justify this fragment if it's not a trailing space in the line.
|
||||
if (end > line.endIndex - line.trailingSpaces) {
|
||||
// Don't justify fragments that are part of trailing spaces of the line.
|
||||
return;
|
||||
}
|
||||
|
||||
if (trailingSpaces == 0) {
|
||||
// If this fragment has no spaces, there's nothing to justify.
|
||||
return;
|
||||
}
|
||||
|
||||
final double justificationTotal = paragraphWidth - line.width;
|
||||
final double justificationPerSpace = justificationTotal / line.nonTrailingSpaces;
|
||||
_extraWidthForJustification = justificationPerSpace * trailingSpaces;
|
||||
}
|
||||
}
|
||||
|
||||
/// Encapsulates calculations related to the bounding box of the fragment
|
||||
/// relative to the paragraph.
|
||||
mixin _FragmentBox on _CombinedFragment, _FragmentMetrics, _FragmentPosition {
|
||||
double get top => line.baseline - ascent;
|
||||
double get bottom => line.baseline + descent;
|
||||
|
||||
late final ui.TextBox _textBoxIncludingTrailingSpaces = ui.TextBox.fromLTRBD(
|
||||
line.left + left,
|
||||
top,
|
||||
line.left + right,
|
||||
bottom,
|
||||
textDirection!,
|
||||
);
|
||||
|
||||
/// Whether or not the trailing spaces of this fragment are part of trailing
|
||||
/// spaces of the line containing the fragment.
|
||||
bool get _isPartOfTrailingSpacesInLine => end > line.endIndex - line.trailingSpaces;
|
||||
|
||||
/// Returns a [ui.TextBox] for the purpose of painting this fragment.
|
||||
///
|
||||
/// The coordinates of the resulting [ui.TextBox] are relative to the
|
||||
/// paragraph, not to the line.
|
||||
///
|
||||
/// Trailing spaces in each line aren't painted on the screen, so they are
|
||||
/// excluded from the resulting text box.
|
||||
ui.TextBox toPaintingTextBox() {
|
||||
if (_isPartOfTrailingSpacesInLine) {
|
||||
// For painting, we exclude the width of trailing spaces from the box.
|
||||
return textDirection! == ui.TextDirection.ltr
|
||||
? ui.TextBox.fromLTRBD(
|
||||
line.left + left,
|
||||
top,
|
||||
line.left + right - widthOfTrailingSpaces,
|
||||
bottom,
|
||||
textDirection!,
|
||||
)
|
||||
: ui.TextBox.fromLTRBD(
|
||||
line.left + left + widthOfTrailingSpaces,
|
||||
top,
|
||||
line.left + right,
|
||||
bottom,
|
||||
textDirection!,
|
||||
);
|
||||
}
|
||||
return _textBoxIncludingTrailingSpaces;
|
||||
}
|
||||
|
||||
/// Returns a [ui.TextBox] representing this fragment.
|
||||
///
|
||||
/// The coordinates of the resulting [ui.TextBox] are relative to the
|
||||
/// paragraph, not to the line.
|
||||
///
|
||||
/// As opposed to [toPaintingTextBox], the resulting text box from this method
|
||||
/// includes trailing spaces of the fragment.
|
||||
ui.TextBox toTextBox({
|
||||
int? start,
|
||||
int? end,
|
||||
}) {
|
||||
start ??= this.start;
|
||||
end ??= this.end;
|
||||
|
||||
if (start <= this.start && end >= this.end - trailingNewlines) {
|
||||
return _textBoxIncludingTrailingSpaces;
|
||||
}
|
||||
return _intersect(start, end);
|
||||
}
|
||||
|
||||
/// Performs the intersection of this fragment with the range given by [start] and
|
||||
/// [end] indices, and returns a [ui.TextBox] representing that intersection.
|
||||
///
|
||||
/// The coordinates of the resulting [ui.TextBox] are relative to the
|
||||
/// paragraph, not to the line.
|
||||
ui.TextBox _intersect(int start, int end) {
|
||||
// `_intersect` should only be called when there's an actual intersection.
|
||||
assert(start > this.start || end < this.end);
|
||||
|
||||
final double before;
|
||||
if (start <= this.start) {
|
||||
before = 0.0;
|
||||
} else {
|
||||
_spanometer.currentSpan = span;
|
||||
before = _spanometer.measureRange(this.start, start);
|
||||
}
|
||||
|
||||
final double after;
|
||||
if (end >= this.end - trailingNewlines) {
|
||||
after = 0.0;
|
||||
} else {
|
||||
_spanometer.currentSpan = span;
|
||||
after = _spanometer.measureRange(end, this.end - trailingNewlines);
|
||||
}
|
||||
|
||||
final double left, right;
|
||||
if (textDirection! == ui.TextDirection.ltr) {
|
||||
// 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 fragment'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(
|
||||
line.left + left,
|
||||
top,
|
||||
line.left + right,
|
||||
bottom,
|
||||
textDirection!,
|
||||
);
|
||||
}
|
||||
|
||||
/// Returns the text position within this fragment's range that's closest to
|
||||
/// the given [x] offset.
|
||||
///
|
||||
/// The [x] offset is expected to be relative to the left edge of the fragment.
|
||||
ui.TextPosition getPositionForX(double x) {
|
||||
x = _makeXDirectionAgnostic(x);
|
||||
|
||||
final int startIndex = start;
|
||||
final int endIndex = end - trailingNewlines;
|
||||
|
||||
// Check some special cases to return the result quicker.
|
||||
|
||||
final int length = endIndex - startIndex;
|
||||
if (length == 0) {
|
||||
return ui.TextPosition(offset: startIndex);
|
||||
}
|
||||
if (length == 1) {
|
||||
// Find out if `x` is closer to `startIndex` or `endIndex`.
|
||||
final double distanceFromStart = x;
|
||||
final double distanceFromEnd = widthIncludingTrailingSpaces - x;
|
||||
return distanceFromStart < distanceFromEnd
|
||||
? ui.TextPosition(offset: startIndex)
|
||||
: ui.TextPosition(offset: endIndex, affinity: ui.TextAffinity.upstream,);
|
||||
}
|
||||
|
||||
_spanometer.currentSpan = span;
|
||||
// The resulting `cutoff` is the index of the character where the `x` offset
|
||||
// falls. We should return the text position of either `cutoff` or
|
||||
// `cutoff + 1` depending on which one `x` is closer to.
|
||||
//
|
||||
// offset x
|
||||
// ↓
|
||||
// "A B C D E F"
|
||||
// ↑
|
||||
// cutoff
|
||||
final int cutoff = _spanometer.forceBreak(
|
||||
startIndex,
|
||||
endIndex,
|
||||
availableWidth: x,
|
||||
allowEmpty: true,
|
||||
);
|
||||
|
||||
if (cutoff == endIndex) {
|
||||
return ui.TextPosition(
|
||||
offset: cutoff,
|
||||
affinity: ui.TextAffinity.upstream,
|
||||
);
|
||||
}
|
||||
|
||||
final double lowWidth = _spanometer.measureRange(startIndex, cutoff);
|
||||
final double highWidth = _spanometer.measureRange(startIndex, cutoff + 1);
|
||||
|
||||
// See if `x` is closer to `cutoff` or `cutoff + 1`.
|
||||
if (x - lowWidth < highWidth - x) {
|
||||
// The offset is closer to cutoff.
|
||||
return ui.TextPosition(offset: cutoff);
|
||||
} else {
|
||||
// The offset is closer to cutoff + 1.
|
||||
return ui.TextPosition(
|
||||
offset: cutoff + 1,
|
||||
affinity: ui.TextAffinity.upstream,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Transforms the [x] coordinate to be direction-agnostic.
|
||||
///
|
||||
/// The X (input) is relative to the [left] edge of the fragment, and this
|
||||
/// method returns an X' (output) that's relative to beginning of the text.
|
||||
///
|
||||
/// Here's how it looks for a fragment with LTR content:
|
||||
///
|
||||
/// *------------------------line width------------------*
|
||||
/// *-----X (input)
|
||||
/// ┌───────────┬────────────────────────┬───────────────┐
|
||||
/// │ │ ---text-direction----> │ │
|
||||
/// └───────────┴────────────────────────┴───────────────┘
|
||||
/// *-----X' (output)
|
||||
/// *---left----*
|
||||
/// *---------------right----------------*
|
||||
///
|
||||
///
|
||||
/// And here's how it looks for a fragment with RTL content:
|
||||
///
|
||||
/// *------------------------line width------------------*
|
||||
/// *-----X (input)
|
||||
/// ┌───────────┬────────────────────────┬───────────────┐
|
||||
/// │ │ <---text-direction---- │ │
|
||||
/// └───────────┴────────────────────────┴───────────────┘
|
||||
/// (output) X'-----------------*
|
||||
/// *---left----*
|
||||
/// *---------------right----------------*
|
||||
///
|
||||
double _makeXDirectionAgnostic(double x) {
|
||||
if (textDirection == ui.TextDirection.rtl) {
|
||||
return widthIncludingTrailingSpaces - x;
|
||||
}
|
||||
return x;
|
||||
}
|
||||
}
|
||||
|
||||
class EllipsisFragment extends LayoutFragment {
|
||||
EllipsisFragment(
|
||||
int index,
|
||||
ParagraphSpan span,
|
||||
) : super(
|
||||
index,
|
||||
index,
|
||||
LineBreakType.endOfText,
|
||||
null,
|
||||
// The ellipsis is always at the end of the line, so it can't be
|
||||
// sandwiched. This means it'll always follow the paragraph direction.
|
||||
FragmentFlow.sandwich,
|
||||
span,
|
||||
trailingNewlines: 0,
|
||||
trailingSpaces: 0,
|
||||
);
|
||||
|
||||
@override
|
||||
bool get isSpaceOnly => false;
|
||||
|
||||
@override
|
||||
bool get isPlaceholder => false;
|
||||
|
||||
@override
|
||||
String getText(CanvasParagraph paragraph) {
|
||||
return paragraph.paragraphStyle.ellipsis!;
|
||||
}
|
||||
|
||||
@override
|
||||
List<LayoutFragment> split(int index) {
|
||||
throw Exception('Cannot split an EllipsisFragment');
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -2,9 +2,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' as math;
|
||||
|
||||
import '../util.dart';
|
||||
import 'fragmenter.dart';
|
||||
import 'line_break_properties.dart';
|
||||
import 'unicode_range.dart';
|
||||
|
||||
@ -26,96 +24,42 @@ enum LineBreakType {
|
||||
endOfText,
|
||||
}
|
||||
|
||||
/// Acts as a tuple that encapsulates information about a line break.
|
||||
///
|
||||
/// It contains multiple indices that are helpful when it comes to measuring the
|
||||
/// width of a line of text.
|
||||
///
|
||||
/// [indexWithoutTrailingSpaces] <= [indexWithoutTrailingNewlines] <= [index]
|
||||
///
|
||||
/// Example: for the string "foo \nbar " here are the indices:
|
||||
/// ```
|
||||
/// f o o \n b a r
|
||||
/// ^ ^ ^ ^ ^ ^ ^ ^ ^ ^
|
||||
/// 0 1 2 3 4 5 6 7 8 9
|
||||
/// ```
|
||||
/// It contains two line breaks:
|
||||
/// ```
|
||||
/// // The first line break:
|
||||
/// LineBreakResult(5, 4, 3, LineBreakType.mandatory)
|
||||
///
|
||||
/// // Second line break:
|
||||
/// LineBreakResult(9, 9, 8, LineBreakType.mandatory)
|
||||
/// ```
|
||||
class LineBreakResult {
|
||||
const LineBreakResult(
|
||||
this.index,
|
||||
this.indexWithoutTrailingNewlines,
|
||||
this.indexWithoutTrailingSpaces,
|
||||
this.type,
|
||||
): assert(indexWithoutTrailingSpaces <= indexWithoutTrailingNewlines),
|
||||
assert(indexWithoutTrailingNewlines <= index);
|
||||
|
||||
/// Creates a [LineBreakResult] where all indices are the same (i.e. there are
|
||||
/// no trailing spaces or new lines).
|
||||
const LineBreakResult.sameIndex(this.index, this.type)
|
||||
: indexWithoutTrailingNewlines = index,
|
||||
indexWithoutTrailingSpaces = index;
|
||||
|
||||
/// The true index at which the line break should occur, including all spaces
|
||||
/// and new lines.
|
||||
final int index;
|
||||
|
||||
/// The index of the line break excluding any trailing new lines.
|
||||
final int indexWithoutTrailingNewlines;
|
||||
|
||||
/// The index of the line break excluding any trailing spaces.
|
||||
final int indexWithoutTrailingSpaces;
|
||||
|
||||
/// The type of line break is useful to determine the behavior in text
|
||||
/// measurement.
|
||||
///
|
||||
/// For example, a mandatory line break always causes a line break regardless
|
||||
/// of width constraints. But a line break opportunity requires further checks
|
||||
/// to decide whether to take the line break or not.
|
||||
final LineBreakType type;
|
||||
|
||||
bool get isHard =>
|
||||
type == LineBreakType.mandatory || type == LineBreakType.endOfText;
|
||||
/// Splits [text] into fragments based on line breaks.
|
||||
class LineBreakFragmenter extends TextFragmenter {
|
||||
const LineBreakFragmenter(super.text);
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
index,
|
||||
indexWithoutTrailingNewlines,
|
||||
indexWithoutTrailingSpaces,
|
||||
type,
|
||||
);
|
||||
List<LineBreakFragment> fragment() {
|
||||
return _computeLineBreakFragments(text);
|
||||
}
|
||||
}
|
||||
|
||||
class LineBreakFragment extends TextFragment {
|
||||
const LineBreakFragment(super.start, super.end, this.type, {
|
||||
required this.trailingNewlines,
|
||||
required this.trailingSpaces,
|
||||
});
|
||||
|
||||
final LineBreakType type;
|
||||
final int trailingNewlines;
|
||||
final int trailingSpaces;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(start, end, type, trailingNewlines, trailingSpaces);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
if (other.runtimeType != runtimeType) {
|
||||
return false;
|
||||
}
|
||||
return other is LineBreakResult &&
|
||||
other.index == index &&
|
||||
other.indexWithoutTrailingNewlines == indexWithoutTrailingNewlines &&
|
||||
other.indexWithoutTrailingSpaces == indexWithoutTrailingSpaces &&
|
||||
other.type == type;
|
||||
return other is LineBreakFragment &&
|
||||
other.start == start &&
|
||||
other.end == end &&
|
||||
other.type == type &&
|
||||
other.trailingNewlines == trailingNewlines &&
|
||||
other.trailingSpaces == trailingSpaces;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
if (assertionsEnabled) {
|
||||
return 'LineBreakResult(index: $index, '
|
||||
'without new lines: $indexWithoutTrailingNewlines, '
|
||||
'without spaces: $indexWithoutTrailingSpaces, '
|
||||
'type: $type)';
|
||||
} else {
|
||||
return super.toString();
|
||||
}
|
||||
return 'LineBreakFragment($start, $end, $type)';
|
||||
}
|
||||
}
|
||||
|
||||
@ -151,6 +95,10 @@ bool _hasEastAsianWidthFWH(int charCode) {
|
||||
(charCode >= 0xFE17 && charCode <= 0xFF62);
|
||||
}
|
||||
|
||||
bool _isSurrogatePair(int? codePoint) {
|
||||
return codePoint != null && codePoint > 0xFFFF;
|
||||
}
|
||||
|
||||
/// Finds the next line break in the given [text] starting from [index].
|
||||
///
|
||||
/// We think about indices as pointing between characters, and they go all the
|
||||
@ -163,100 +111,105 @@ bool _hasEastAsianWidthFWH(int charCode) {
|
||||
/// 0 1 2 3 4 5 6 7
|
||||
/// ```
|
||||
///
|
||||
/// This way the indices work well with [String.substring()].
|
||||
/// This way the indices work well with [String.substring].
|
||||
///
|
||||
/// Useful resources:
|
||||
///
|
||||
/// * https://www.unicode.org/reports/tr14/tr14-45.html#Algorithm
|
||||
/// * https://www.unicode.org/Public/11.0.0/ucd/LineBreak.txt
|
||||
LineBreakResult nextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
final LineBreakResult unsafeResult = _unsafeNextLineBreak(text, index, maxEnd: maxEnd);
|
||||
if (maxEnd != null && unsafeResult.index > maxEnd) {
|
||||
return LineBreakResult(
|
||||
maxEnd,
|
||||
math.min(maxEnd, unsafeResult.indexWithoutTrailingNewlines),
|
||||
math.min(maxEnd, unsafeResult.indexWithoutTrailingSpaces),
|
||||
LineBreakType.prohibited,
|
||||
);
|
||||
}
|
||||
return unsafeResult;
|
||||
}
|
||||
|
||||
LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
int? codePoint = getCodePoint(text, index);
|
||||
LineCharProperty curr = lineLookup.findForChar(codePoint);
|
||||
|
||||
LineCharProperty? prev1;
|
||||
List<LineBreakFragment> _computeLineBreakFragments(String text) {
|
||||
final List<LineBreakFragment> fragments = <LineBreakFragment>[];
|
||||
|
||||
// Keeps track of the character two positions behind.
|
||||
LineCharProperty? prev2;
|
||||
LineCharProperty? prev1;
|
||||
|
||||
// When there's a sequence of spaces or combining marks, this variable
|
||||
// contains the base property i.e. the property of the character before the
|
||||
// sequence.
|
||||
LineCharProperty? baseOfSpaceSequence;
|
||||
int? codePoint = getCodePoint(text, 0);
|
||||
LineCharProperty? curr = lineLookup.findForChar(codePoint);
|
||||
|
||||
/// The index of the last character that wasn't a space.
|
||||
int lastNonSpaceIndex = index;
|
||||
// When there's a sequence of spaces, this variable contains the base property
|
||||
// i.e. the property of the character preceding the sequence.
|
||||
LineCharProperty baseOfSpaceSequence = LineCharProperty.WJ;
|
||||
|
||||
/// The index of the last character that wasn't a new line.
|
||||
int lastNonNewlineIndex = index;
|
||||
// When there's a sequence of combining marks, this variable contains the base
|
||||
// property i.e. the property of the character preceding the sequence.
|
||||
LineCharProperty baseOfCombiningMarks = LineCharProperty.AL;
|
||||
|
||||
// When the text/line starts with SP, we should treat the beginning of text/line
|
||||
// as if it were a WJ (word joiner).
|
||||
if (curr == LineCharProperty.SP) {
|
||||
baseOfSpaceSequence = LineCharProperty.WJ;
|
||||
int index = 0;
|
||||
int trailingNewlines = 0;
|
||||
int trailingSpaces = 0;
|
||||
|
||||
int fragmentStart = 0;
|
||||
|
||||
void setBreak(LineBreakType type, int debugRuleNumber) {
|
||||
final int fragmentEnd =
|
||||
type == LineBreakType.endOfText ? text.length : index;
|
||||
assert(fragmentEnd >= fragmentStart);
|
||||
|
||||
if (prev1 == LineCharProperty.SP) {
|
||||
trailingSpaces++;
|
||||
} else if (_isHardBreak(prev1) || prev1 == LineCharProperty.CR) {
|
||||
trailingNewlines++;
|
||||
trailingSpaces++;
|
||||
}
|
||||
|
||||
if (type == LineBreakType.prohibited) {
|
||||
// Don't create a fragment.
|
||||
return;
|
||||
}
|
||||
|
||||
fragments.add(LineBreakFragment(
|
||||
fragmentStart,
|
||||
fragmentEnd,
|
||||
type,
|
||||
trailingNewlines: trailingNewlines,
|
||||
trailingSpaces: trailingSpaces,
|
||||
));
|
||||
|
||||
fragmentStart = index;
|
||||
|
||||
// Reset trailing spaces/newlines counter after a new fragment.
|
||||
trailingNewlines = 0;
|
||||
trailingSpaces = 0;
|
||||
|
||||
prev1 = prev2 = null;
|
||||
}
|
||||
|
||||
bool isCurrZWJ = curr == LineCharProperty.ZWJ;
|
||||
// Never break at the start of text.
|
||||
// LB2: sot ×
|
||||
setBreak(LineBreakType.prohibited, 2);
|
||||
|
||||
// LB10: Treat any remaining combining mark or ZWJ as AL.
|
||||
// This catches the case where a CM is the first character on the line.
|
||||
if (curr == LineCharProperty.CM || curr == LineCharProperty.ZWJ) {
|
||||
curr = LineCharProperty.AL;
|
||||
}
|
||||
// Never break at the start of text.
|
||||
// LB2: sot ×
|
||||
//
|
||||
// Skip index 0 because a line break can't exist at the start of text.
|
||||
index++;
|
||||
|
||||
int regionalIndicatorCount = 0;
|
||||
|
||||
// Always break at the end of text.
|
||||
// LB3: ! eot
|
||||
while (index < text.length) {
|
||||
if (maxEnd != null && index > maxEnd) {
|
||||
return LineBreakResult(
|
||||
maxEnd,
|
||||
math.min(maxEnd, lastNonNewlineIndex),
|
||||
math.min(maxEnd, lastNonSpaceIndex),
|
||||
LineBreakType.prohibited,
|
||||
);
|
||||
}
|
||||
|
||||
// Keep count of the RI (regional indicator) sequence.
|
||||
if (curr == LineCharProperty.RI) {
|
||||
regionalIndicatorCount++;
|
||||
} else {
|
||||
regionalIndicatorCount = 0;
|
||||
}
|
||||
|
||||
if (codePoint != null && codePoint > 0xFFFF) {
|
||||
// Advance `index` one extra step when handling a surrogate pair in the
|
||||
// string.
|
||||
index++;
|
||||
}
|
||||
index++;
|
||||
// We need to go until `text.length` in order to handle the case where the
|
||||
// paragraph ends with a hard break. In this case, there will be an empty line
|
||||
// at the end.
|
||||
for (; index <= text.length; index++) {
|
||||
prev2 = prev1;
|
||||
prev1 = curr;
|
||||
|
||||
final bool isPrevZWJ = isCurrZWJ;
|
||||
|
||||
// Reset the base when we are past the space sequence.
|
||||
if (prev1 != LineCharProperty.SP) {
|
||||
baseOfSpaceSequence = null;
|
||||
if (_isSurrogatePair(codePoint)) {
|
||||
// Can't break in the middle of a surrogate pair.
|
||||
setBreak(LineBreakType.prohibited, -1);
|
||||
// Advance `index` one extra step to skip the tail of the surrogate pair.
|
||||
index++;
|
||||
}
|
||||
|
||||
codePoint = getCodePoint(text, index);
|
||||
curr = lineLookup.findForChar(codePoint);
|
||||
|
||||
isCurrZWJ = curr == LineCharProperty.ZWJ;
|
||||
// Keep count of the RI (regional indicator) sequence.
|
||||
if (prev1 == LineCharProperty.RI) {
|
||||
regionalIndicatorCount++;
|
||||
} else {
|
||||
regionalIndicatorCount = 0;
|
||||
}
|
||||
|
||||
// Always break after hard line breaks.
|
||||
// LB4: BK !
|
||||
@ -265,69 +218,44 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// LB5: LF !
|
||||
// NL !
|
||||
if (_isHardBreak(prev1)) {
|
||||
return LineBreakResult(
|
||||
index,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.mandatory,
|
||||
);
|
||||
setBreak(LineBreakType.mandatory, 5);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (prev1 == LineCharProperty.CR) {
|
||||
if (curr == LineCharProperty.LF) {
|
||||
// LB5: CR × LF
|
||||
continue;
|
||||
setBreak(LineBreakType.prohibited, 5);
|
||||
} else {
|
||||
// LB5: CR !
|
||||
return LineBreakResult(
|
||||
index,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.mandatory,
|
||||
);
|
||||
setBreak(LineBreakType.mandatory, 5);
|
||||
}
|
||||
}
|
||||
|
||||
// At this point, we know for sure the prev character wasn't a new line.
|
||||
lastNonNewlineIndex = index;
|
||||
if (prev1 != LineCharProperty.SP) {
|
||||
lastNonSpaceIndex = index;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break before hard line breaks.
|
||||
// LB6: × ( BK | CR | LF | NL )
|
||||
if (_isHardBreak(curr) || curr == LineCharProperty.CR) {
|
||||
setBreak(LineBreakType.prohibited, 6);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Always break at the end of text.
|
||||
// LB3: ! eot
|
||||
if (index >= text.length) {
|
||||
return LineBreakResult(
|
||||
text.length,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.endOfText,
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
// Establish the base for the space sequence.
|
||||
if (prev1 != LineCharProperty.SP) {
|
||||
// When the text/line starts with SP, we should treat the beginning of text/line
|
||||
// as if it were a WJ (word joiner).
|
||||
baseOfSpaceSequence = prev1 ?? LineCharProperty.WJ;
|
||||
}
|
||||
|
||||
// Do not break before spaces or zero width space.
|
||||
// LB7: × SP
|
||||
if (curr == LineCharProperty.SP) {
|
||||
// When we encounter SP, we preserve the property of the previous
|
||||
// character so we can later apply the indirect breaking rules.
|
||||
if (prev1 == LineCharProperty.SP) {
|
||||
// If we are in the middle of a space sequence, a base should've
|
||||
// already been set.
|
||||
assert(baseOfSpaceSequence != null);
|
||||
} else {
|
||||
// We are at the beginning of a space sequence, establish the base.
|
||||
baseOfSpaceSequence = prev1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// LB7: × ZW
|
||||
if (curr == LineCharProperty.ZW) {
|
||||
// × ZW
|
||||
if (curr == LineCharProperty.SP || curr == LineCharProperty.ZW) {
|
||||
setBreak(LineBreakType.prohibited, 7);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -336,53 +264,61 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// LB8: ZW SP* ÷
|
||||
if (prev1 == LineCharProperty.ZW ||
|
||||
baseOfSpaceSequence == LineCharProperty.ZW) {
|
||||
return LineBreakResult(
|
||||
index,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.opportunity,
|
||||
);
|
||||
setBreak(LineBreakType.opportunity, 8);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break after a zero width joiner.
|
||||
// LB8a: ZWJ ×
|
||||
if (prev1 == LineCharProperty.ZWJ) {
|
||||
setBreak(LineBreakType.prohibited, 8);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Establish the base for the sequences of combining marks.
|
||||
if (prev1 != LineCharProperty.CM && prev1 != LineCharProperty.ZWJ) {
|
||||
baseOfCombiningMarks = prev1 ?? LineCharProperty.AL;
|
||||
}
|
||||
|
||||
// Do not break a combining character sequence; treat it as if it has the
|
||||
// line breaking class of the base character in all of the following rules.
|
||||
// Treat ZWJ as if it were CM.
|
||||
// LB9: Treat X (CM | ZWJ)* as if it were X
|
||||
// where X is any line break class except BK, NL, LF, CR, SP, or ZW.
|
||||
if (curr == LineCharProperty.CM || curr == LineCharProperty.ZWJ) {
|
||||
// Other properties: BK, NL, LF, CR, ZW would've already generated a line
|
||||
// break, so we won't find them in `prev`.
|
||||
if (prev1 == LineCharProperty.SP) {
|
||||
if (baseOfCombiningMarks == LineCharProperty.SP) {
|
||||
// LB10: Treat any remaining combining mark or ZWJ as AL.
|
||||
curr = LineCharProperty.AL;
|
||||
} else {
|
||||
if (prev1 == LineCharProperty.RI) {
|
||||
// LB9: Treat X (CM | ZWJ)* as if it were X
|
||||
// where X is any line break class except BK, NL, LF, CR, SP, or ZW.
|
||||
curr = baseOfCombiningMarks;
|
||||
if (curr == LineCharProperty.RI) {
|
||||
// Prevent the previous RI from being double-counted.
|
||||
regionalIndicatorCount--;
|
||||
}
|
||||
// Preserve the property of the previous character to treat the sequence
|
||||
// as if it were X.
|
||||
curr = prev1;
|
||||
setBreak(LineBreakType.prohibited, 9);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Do not break after a zero width joiner.
|
||||
// LB8a: ZWJ ×
|
||||
if (isPrevZWJ) {
|
||||
continue;
|
||||
// In certain situations (e.g. CM immediately following a hard break), we
|
||||
// need to also check if the previous character was CM/ZWJ. That's because
|
||||
// hard breaks caused the previous iteration to short-circuit, which leads
|
||||
// to `baseOfCombiningMarks` not being updated properly.
|
||||
if (prev1 == LineCharProperty.CM || prev1 == LineCharProperty.ZWJ) {
|
||||
prev1 = baseOfCombiningMarks;
|
||||
}
|
||||
|
||||
// Do not break before or after Word joiner and related characters.
|
||||
// LB11: × WJ
|
||||
// WJ ×
|
||||
if (curr == LineCharProperty.WJ || prev1 == LineCharProperty.WJ) {
|
||||
setBreak(LineBreakType.prohibited, 11);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break after NBSP and related characters.
|
||||
// LB12: GL ×
|
||||
if (prev1 == LineCharProperty.GL) {
|
||||
setBreak(LineBreakType.prohibited, 12);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -393,6 +329,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
prev1 == LineCharProperty.BA ||
|
||||
prev1 == LineCharProperty.HY) &&
|
||||
curr == LineCharProperty.GL) {
|
||||
setBreak(LineBreakType.prohibited, 12);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -412,6 +349,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
curr == LineCharProperty.EX ||
|
||||
curr == LineCharProperty.IS ||
|
||||
curr == LineCharProperty.SY)) {
|
||||
setBreak(LineBreakType.prohibited, 13);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -421,6 +359,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// The above is a quote from unicode.org. In our implementation, we did the
|
||||
// following modification: Allow breaks when there are spaces.
|
||||
if (prev1 == LineCharProperty.OP) {
|
||||
setBreak(LineBreakType.prohibited, 14);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -430,6 +369,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// The above is a quote from unicode.org. In our implementation, we did the
|
||||
// following modification: Allow breaks when there are spaces.
|
||||
if (prev1 == LineCharProperty.QU && curr == LineCharProperty.OP) {
|
||||
setBreak(LineBreakType.prohibited, 15);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -441,6 +381,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
prev1 == LineCharProperty.CP ||
|
||||
baseOfSpaceSequence == LineCharProperty.CP) &&
|
||||
curr == LineCharProperty.NS) {
|
||||
setBreak(LineBreakType.prohibited, 16);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -449,37 +390,34 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
if ((prev1 == LineCharProperty.B2 ||
|
||||
baseOfSpaceSequence == LineCharProperty.B2) &&
|
||||
curr == LineCharProperty.B2) {
|
||||
setBreak(LineBreakType.prohibited, 17);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break after spaces.
|
||||
// LB18: SP ÷
|
||||
if (prev1 == LineCharProperty.SP) {
|
||||
return LineBreakResult(
|
||||
index,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.opportunity,
|
||||
);
|
||||
setBreak(LineBreakType.opportunity, 18);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break before or after quotation marks, such as ‘”’.
|
||||
// LB19: × QU
|
||||
// QU ×
|
||||
if (prev1 == LineCharProperty.QU || curr == LineCharProperty.QU) {
|
||||
setBreak(LineBreakType.prohibited, 19);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break before and after unresolved CB.
|
||||
// LB20: ÷ CB
|
||||
// CB ÷
|
||||
//
|
||||
// In flutter web, we use this as an object-replacement character for
|
||||
// placeholders.
|
||||
if (prev1 == LineCharProperty.CB || curr == LineCharProperty.CB) {
|
||||
return LineBreakResult(
|
||||
index,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.opportunity,
|
||||
);
|
||||
setBreak(LineBreakType.opportunity, 20);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break before hyphen-minus, other hyphens, fixed-width spaces,
|
||||
@ -492,6 +430,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
curr == LineCharProperty.HY ||
|
||||
curr == LineCharProperty.NS ||
|
||||
prev1 == LineCharProperty.BB) {
|
||||
setBreak(LineBreakType.prohibited, 21);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -499,18 +438,21 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// LB21a: HL (HY | BA) ×
|
||||
if (prev2 == LineCharProperty.HL &&
|
||||
(prev1 == LineCharProperty.HY || prev1 == LineCharProperty.BA)) {
|
||||
setBreak(LineBreakType.prohibited, 21);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Don’t break between Solidus and Hebrew letters.
|
||||
// LB21b: SY × HL
|
||||
if (prev1 == LineCharProperty.SY && curr == LineCharProperty.HL) {
|
||||
setBreak(LineBreakType.prohibited, 21);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break before ellipses.
|
||||
// LB22: × IN
|
||||
if (curr == LineCharProperty.IN) {
|
||||
setBreak(LineBreakType.prohibited, 22);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -519,6 +461,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// NU × (AL | HL)
|
||||
if ((_isALorHL(prev1) && curr == LineCharProperty.NU) ||
|
||||
(prev1 == LineCharProperty.NU && _isALorHL(curr))) {
|
||||
setBreak(LineBreakType.prohibited, 23);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -529,6 +472,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
(curr == LineCharProperty.ID ||
|
||||
curr == LineCharProperty.EB ||
|
||||
curr == LineCharProperty.EM)) {
|
||||
setBreak(LineBreakType.prohibited, 23);
|
||||
continue;
|
||||
}
|
||||
// LB23a: (ID | EB | EM) × PO
|
||||
@ -536,6 +480,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
prev1 == LineCharProperty.EB ||
|
||||
prev1 == LineCharProperty.EM) &&
|
||||
curr == LineCharProperty.PO) {
|
||||
setBreak(LineBreakType.prohibited, 23);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -544,11 +489,13 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// LB24: (PR | PO) × (AL | HL)
|
||||
if ((prev1 == LineCharProperty.PR || prev1 == LineCharProperty.PO) &&
|
||||
_isALorHL(curr)) {
|
||||
setBreak(LineBreakType.prohibited, 24);
|
||||
continue;
|
||||
}
|
||||
// LB24: (AL | HL) × (PR | PO)
|
||||
if (_isALorHL(prev1) &&
|
||||
(curr == LineCharProperty.PR || curr == LineCharProperty.PO)) {
|
||||
setBreak(LineBreakType.prohibited, 24);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -558,11 +505,13 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
prev1 == LineCharProperty.CP ||
|
||||
prev1 == LineCharProperty.NU) &&
|
||||
(curr == LineCharProperty.PO || curr == LineCharProperty.PR)) {
|
||||
setBreak(LineBreakType.prohibited, 25);
|
||||
continue;
|
||||
}
|
||||
// LB25: (PO | PR) × OP
|
||||
if ((prev1 == LineCharProperty.PO || prev1 == LineCharProperty.PR) &&
|
||||
curr == LineCharProperty.OP) {
|
||||
setBreak(LineBreakType.prohibited, 25);
|
||||
continue;
|
||||
}
|
||||
// LB25: (PO | PR | HY | IS | NU | SY) × NU
|
||||
@ -573,6 +522,7 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
prev1 == LineCharProperty.NU ||
|
||||
prev1 == LineCharProperty.SY) &&
|
||||
curr == LineCharProperty.NU) {
|
||||
setBreak(LineBreakType.prohibited, 25);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -583,38 +533,45 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
curr == LineCharProperty.JV ||
|
||||
curr == LineCharProperty.H2 ||
|
||||
curr == LineCharProperty.H3)) {
|
||||
setBreak(LineBreakType.prohibited, 26);
|
||||
continue;
|
||||
}
|
||||
// LB26: (JV | H2) × (JV | JT)
|
||||
if ((prev1 == LineCharProperty.JV || prev1 == LineCharProperty.H2) &&
|
||||
(curr == LineCharProperty.JV || curr == LineCharProperty.JT)) {
|
||||
setBreak(LineBreakType.prohibited, 26);
|
||||
continue;
|
||||
}
|
||||
// LB26: (JT | H3) × JT
|
||||
if ((prev1 == LineCharProperty.JT || prev1 == LineCharProperty.H3) &&
|
||||
curr == LineCharProperty.JT) {
|
||||
setBreak(LineBreakType.prohibited, 26);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Treat a Korean Syllable Block the same as ID.
|
||||
// LB27: (JL | JV | JT | H2 | H3) × PO
|
||||
if (_isKoreanSyllable(prev1) && curr == LineCharProperty.PO) {
|
||||
setBreak(LineBreakType.prohibited, 27);
|
||||
continue;
|
||||
}
|
||||
// LB27: PR × (JL | JV | JT | H2 | H3)
|
||||
if (prev1 == LineCharProperty.PR && _isKoreanSyllable(curr)) {
|
||||
setBreak(LineBreakType.prohibited, 27);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break between alphabetics.
|
||||
// LB28: (AL | HL) × (AL | HL)
|
||||
if (_isALorHL(prev1) && _isALorHL(curr)) {
|
||||
setBreak(LineBreakType.prohibited, 28);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break between numeric punctuation and alphabetics (“e.g.”).
|
||||
// LB29: IS × (AL | HL)
|
||||
if (prev1 == LineCharProperty.IS && _isALorHL(curr)) {
|
||||
setBreak(LineBreakType.prohibited, 29);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -627,12 +584,14 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
if ((_isALorHL(prev1) || prev1 == LineCharProperty.NU) &&
|
||||
curr == LineCharProperty.OP &&
|
||||
!_hasEastAsianWidthFWH(text.codeUnitAt(index))) {
|
||||
setBreak(LineBreakType.prohibited, 30);
|
||||
continue;
|
||||
}
|
||||
// LB30: CP × (AL | HL | NU)
|
||||
if (prev1 == LineCharProperty.CP &&
|
||||
!_hasEastAsianWidthFWH(text.codeUnitAt(index - 1)) &&
|
||||
(_isALorHL(curr) || curr == LineCharProperty.NU)) {
|
||||
setBreak(LineBreakType.prohibited, 30);
|
||||
continue;
|
||||
}
|
||||
|
||||
@ -642,37 +601,29 @@ LineBreakResult _unsafeNextLineBreak(String text, int index, {int? maxEnd}) {
|
||||
// [^RI] (RI RI)* RI × RI
|
||||
if (curr == LineCharProperty.RI) {
|
||||
if (regionalIndicatorCount.isOdd) {
|
||||
continue;
|
||||
setBreak(LineBreakType.prohibited, 30);
|
||||
} else {
|
||||
return LineBreakResult(
|
||||
index,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.opportunity,
|
||||
);
|
||||
setBreak(LineBreakType.opportunity, 30);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Do not break between an emoji base and an emoji modifier.
|
||||
// LB30b: EB × EM
|
||||
if (prev1 == LineCharProperty.EB && curr == LineCharProperty.EM) {
|
||||
setBreak(LineBreakType.prohibited, 30);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Break everywhere else.
|
||||
// LB31: ALL ÷
|
||||
// ÷ ALL
|
||||
return LineBreakResult(
|
||||
index,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.opportunity,
|
||||
);
|
||||
setBreak(LineBreakType.opportunity, 31);
|
||||
}
|
||||
return LineBreakResult(
|
||||
text.length,
|
||||
lastNonNewlineIndex,
|
||||
lastNonSpaceIndex,
|
||||
LineBreakType.endOfText,
|
||||
);
|
||||
|
||||
// Always break at the end of text.
|
||||
// LB3: ! eot
|
||||
setBreak(LineBreakType.endOfText, 3);
|
||||
|
||||
return fragments;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import '../dom.dart';
|
||||
import '../html/bitmap_canvas.dart';
|
||||
import '../html/painting.dart';
|
||||
import 'canvas_paragraph.dart';
|
||||
import 'layout_service.dart';
|
||||
import 'layout_fragmenter.dart';
|
||||
import 'paragraph.dart';
|
||||
|
||||
/// Responsible for painting a [CanvasParagraph] on a [BitmapCanvas].
|
||||
@ -18,31 +18,15 @@ class TextPaintService {
|
||||
final CanvasParagraph paragraph;
|
||||
|
||||
void paint(BitmapCanvas canvas, ui.Offset offset) {
|
||||
// Loop through all the lines, for each line, loop through all the boxes and
|
||||
// paint them. The boxes have enough information so they can be painted
|
||||
// Loop through all the lines, for each line, loop through all fragments and
|
||||
// paint them. The fragment objects have enough information to be painted
|
||||
// individually.
|
||||
final List<ParagraphLine> lines = paragraph.lines;
|
||||
|
||||
if (lines.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (final ParagraphLine line in lines) {
|
||||
if (line.boxes.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final RangeBox lastBox = line.boxes.last;
|
||||
|
||||
for (final RangeBox box in line.boxes) {
|
||||
final bool isTrailingSpaceBox =
|
||||
box == lastBox && box is SpanBox && box.isSpaceOnly;
|
||||
|
||||
// Don't paint background for the trailing space in the line.
|
||||
if (!isTrailingSpaceBox) {
|
||||
_paintBackground(canvas, offset, line, box);
|
||||
}
|
||||
_paintText(canvas, offset, line, box);
|
||||
for (final LayoutFragment fragment in line.fragments) {
|
||||
_paintBackground(canvas, offset, fragment);
|
||||
_paintText(canvas, offset, line, fragment);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -50,17 +34,17 @@ class TextPaintService {
|
||||
void _paintBackground(
|
||||
BitmapCanvas canvas,
|
||||
ui.Offset offset,
|
||||
ParagraphLine line,
|
||||
RangeBox box,
|
||||
LayoutFragment fragment,
|
||||
) {
|
||||
if (box is SpanBox) {
|
||||
final FlatTextSpan span = box.span;
|
||||
|
||||
final ParagraphSpan span = fragment.span;
|
||||
if (span is FlatTextSpan) {
|
||||
// Paint the background of the box, if the span has a background.
|
||||
final SurfacePaint? background = span.style.background as SurfacePaint?;
|
||||
if (background != null) {
|
||||
final ui.Rect rect = box.toTextBox(line, forPainting: true).toRect().shift(offset);
|
||||
canvas.drawRect(rect, background.paintData);
|
||||
final ui.Rect rect = fragment.toPaintingTextBox().toRect();
|
||||
if (!rect.isEmpty) {
|
||||
canvas.drawRect(rect.shift(offset), background.paintData);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -69,63 +53,63 @@ class TextPaintService {
|
||||
BitmapCanvas canvas,
|
||||
ui.Offset offset,
|
||||
ParagraphLine line,
|
||||
RangeBox box,
|
||||
LayoutFragment fragment,
|
||||
) {
|
||||
// There's no text to paint in placeholder spans.
|
||||
if (box is SpanBox) {
|
||||
final FlatTextSpan span = box.span;
|
||||
|
||||
_applySpanStyleToCanvas(span, canvas);
|
||||
final double x = offset.dx + line.left + box.left;
|
||||
final double y = offset.dy + line.baseline;
|
||||
|
||||
// Don't paint the text for space-only boxes. This is just an
|
||||
// optimization, it doesn't have any effect on the output.
|
||||
if (!box.isSpaceOnly) {
|
||||
final String text = paragraph.toPlainText().substring(
|
||||
box.start.index,
|
||||
box.end.indexWithoutTrailingNewlines,
|
||||
);
|
||||
final double? letterSpacing = span.style.letterSpacing;
|
||||
if (letterSpacing == null || letterSpacing == 0.0) {
|
||||
canvas.drawText(text, x, y,
|
||||
style: span.style.foreground?.style, shadows: span.style.shadows);
|
||||
} else {
|
||||
// TODO(mdebbar): Implement letter-spacing on canvas more efficiently:
|
||||
// https://github.com/flutter/flutter/issues/51234
|
||||
double charX = x;
|
||||
final int len = text.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
final String char = text[i];
|
||||
canvas.drawText(char, charX.roundToDouble(), y,
|
||||
style: span.style.foreground?.style,
|
||||
shadows: span.style.shadows);
|
||||
charX += letterSpacing + canvas.measureText(char).width!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Paint the ellipsis using the same span styles.
|
||||
final String? ellipsis = line.ellipsis;
|
||||
if (ellipsis != null && box == line.boxes.last) {
|
||||
final double x = offset.dx + line.left + box.right;
|
||||
canvas.drawText(ellipsis, x, y, style: span.style.foreground?.style);
|
||||
}
|
||||
|
||||
canvas.tearDownPaint();
|
||||
if (fragment.isPlaceholder) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Don't paint the text for space-only boxes. This is just an
|
||||
// optimization, it doesn't have any effect on the output.
|
||||
if (fragment.isSpaceOnly) {
|
||||
return;
|
||||
}
|
||||
|
||||
_prepareCanvasForFragment(canvas, fragment);
|
||||
final double fragmentX = fragment.textDirection! == ui.TextDirection.ltr
|
||||
? fragment.left
|
||||
: fragment.right;
|
||||
|
||||
final double x = offset.dx + line.left + fragmentX;
|
||||
final double y = offset.dy + line.baseline;
|
||||
|
||||
final EngineTextStyle style = fragment.style;
|
||||
|
||||
final String text = fragment.getText(paragraph);
|
||||
final double? letterSpacing = style.letterSpacing;
|
||||
if (letterSpacing == null || letterSpacing == 0.0) {
|
||||
canvas.drawText(text, x, y,
|
||||
style: style.foreground?.style, shadows: style.shadows);
|
||||
} else {
|
||||
// TODO(mdebbar): Implement letter-spacing on canvas more efficiently:
|
||||
// https://github.com/flutter/flutter/issues/51234
|
||||
double charX = x;
|
||||
final int len = text.length;
|
||||
for (int i = 0; i < len; i++) {
|
||||
final String char = text[i];
|
||||
canvas.drawText(char, charX.roundToDouble(), y,
|
||||
style: style.foreground?.style,
|
||||
shadows: style.shadows);
|
||||
charX += letterSpacing + canvas.measureText(char).width!;
|
||||
}
|
||||
}
|
||||
|
||||
canvas.tearDownPaint();
|
||||
}
|
||||
|
||||
void _applySpanStyleToCanvas(FlatTextSpan span, BitmapCanvas canvas) {
|
||||
void _prepareCanvasForFragment(BitmapCanvas canvas, LayoutFragment fragment) {
|
||||
final EngineTextStyle style = fragment.style;
|
||||
|
||||
final SurfacePaint? paint;
|
||||
final ui.Paint? foreground = span.style.foreground;
|
||||
final ui.Paint? foreground = style.foreground;
|
||||
if (foreground != null) {
|
||||
paint = foreground as SurfacePaint;
|
||||
} else {
|
||||
paint = (ui.Paint()..color = span.style.color!) as SurfacePaint;
|
||||
paint = (ui.Paint()..color = style.color!) as SurfacePaint;
|
||||
}
|
||||
|
||||
canvas.setCssFont(span.style.cssFontString);
|
||||
canvas.setCssFont(style.cssFontString, fragment.textDirection!);
|
||||
canvas.setUpPaint(paint.paintData, null);
|
||||
}
|
||||
}
|
||||
|
||||
@ -10,7 +10,8 @@ import '../browser_detection.dart';
|
||||
import '../dom.dart';
|
||||
import '../embedder.dart';
|
||||
import '../util.dart';
|
||||
import 'layout_service.dart';
|
||||
import 'canvas_paragraph.dart';
|
||||
import 'layout_fragmenter.dart';
|
||||
import 'ruler.dart';
|
||||
|
||||
class EngineLineMetrics implements ui.LineMetrics {
|
||||
@ -114,16 +115,17 @@ class ParagraphLine {
|
||||
required double left,
|
||||
required double baseline,
|
||||
required int lineNumber,
|
||||
required this.ellipsis,
|
||||
required this.startIndex,
|
||||
required this.endIndex,
|
||||
required this.endIndexWithoutNewlines,
|
||||
required this.trailingNewlines,
|
||||
required this.trailingSpaces,
|
||||
required this.spaceCount,
|
||||
required this.widthWithTrailingSpaces,
|
||||
required this.boxes,
|
||||
required this.spaceBoxCount,
|
||||
required this.trailingSpaceBoxCount,
|
||||
required this.fragments,
|
||||
required this.textDirection,
|
||||
this.displayText,
|
||||
}) : lineMetrics = EngineLineMetrics(
|
||||
}) : assert(trailingNewlines <= endIndex - startIndex),
|
||||
lineMetrics = EngineLineMetrics(
|
||||
hardBreak: hardBreak,
|
||||
ascent: ascent,
|
||||
descent: descent,
|
||||
@ -138,12 +140,6 @@ class ParagraphLine {
|
||||
/// Metrics for this line of the paragraph.
|
||||
final EngineLineMetrics lineMetrics;
|
||||
|
||||
/// The string to be displayed as an overflow indicator.
|
||||
///
|
||||
/// When the value is non-null, it means this line is overflowing and the
|
||||
/// [ellipsis] needs to be displayed at the end of it.
|
||||
final String? ellipsis;
|
||||
|
||||
/// The index (inclusive) in the text where this line begins.
|
||||
final int startIndex;
|
||||
|
||||
@ -153,9 +149,14 @@ class ParagraphLine {
|
||||
/// the text and doesn't stop at the overflow cutoff.
|
||||
final int endIndex;
|
||||
|
||||
/// The index (exclusive) in the text where this line ends, ignoring newline
|
||||
/// characters.
|
||||
final int endIndexWithoutNewlines;
|
||||
/// The number of new line characters at the end of the line.
|
||||
final int trailingNewlines;
|
||||
|
||||
/// The number of spaces at the end of the line.
|
||||
final int trailingSpaces;
|
||||
|
||||
/// The number of space characters in the entire line.
|
||||
final int spaceCount;
|
||||
|
||||
/// The full width of the line including all trailing space but not new lines.
|
||||
///
|
||||
@ -169,21 +170,17 @@ class ParagraphLine {
|
||||
/// spaces so [widthWithTrailingSpaces] is more suitable.
|
||||
final double widthWithTrailingSpaces;
|
||||
|
||||
/// The list of boxes representing the entire line, possibly across multiple
|
||||
/// spans.
|
||||
final List<RangeBox> boxes;
|
||||
/// The fragments that make up this line.
|
||||
final List<LayoutFragment> fragments;
|
||||
|
||||
/// The number of boxes that are space-only.
|
||||
final int spaceBoxCount;
|
||||
|
||||
/// The number of trailing boxes that are space-only.
|
||||
final int trailingSpaceBoxCount;
|
||||
/// The text direction of this line, which is the same as the paragraph's.
|
||||
final ui.TextDirection textDirection;
|
||||
|
||||
/// The text to be rendered on the screen representing this line.
|
||||
final String? displayText;
|
||||
|
||||
/// The number of space-only boxes excluding trailing spaces.
|
||||
int get nonTrailingSpaceBoxCount => spaceBoxCount - trailingSpaceBoxCount;
|
||||
/// The number of space characters in the line excluding trailing spaces.
|
||||
int get nonTrailingSpaces => spaceCount - trailingSpaces;
|
||||
|
||||
// Convenient getters for line metrics properties.
|
||||
|
||||
@ -201,17 +198,25 @@ class ParagraphLine {
|
||||
return startIndex < this.endIndex && this.startIndex < endIndex;
|
||||
}
|
||||
|
||||
String getText(CanvasParagraph paragraph) {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
for (final LayoutFragment fragment in fragments) {
|
||||
buffer.write(fragment.getText(paragraph));
|
||||
}
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(
|
||||
lineMetrics,
|
||||
ellipsis,
|
||||
startIndex,
|
||||
endIndex,
|
||||
endIndexWithoutNewlines,
|
||||
trailingNewlines,
|
||||
trailingSpaces,
|
||||
spaceCount,
|
||||
widthWithTrailingSpaces,
|
||||
boxes,
|
||||
spaceBoxCount,
|
||||
trailingSpaceBoxCount,
|
||||
fragments,
|
||||
textDirection,
|
||||
displayText,
|
||||
);
|
||||
|
||||
@ -225,16 +230,21 @@ class ParagraphLine {
|
||||
}
|
||||
return other is ParagraphLine &&
|
||||
other.lineMetrics == lineMetrics &&
|
||||
other.ellipsis == ellipsis &&
|
||||
other.startIndex == startIndex &&
|
||||
other.endIndex == endIndex &&
|
||||
other.endIndexWithoutNewlines == endIndexWithoutNewlines &&
|
||||
other.trailingNewlines == trailingNewlines &&
|
||||
other.trailingSpaces == trailingSpaces &&
|
||||
other.spaceCount == spaceCount &&
|
||||
other.widthWithTrailingSpaces == widthWithTrailingSpaces &&
|
||||
other.boxes == boxes &&
|
||||
other.spaceBoxCount == spaceBoxCount &&
|
||||
other.trailingSpaceBoxCount == trailingSpaceBoxCount &&
|
||||
other.fragments == fragments &&
|
||||
other.textDirection == textDirection &&
|
||||
other.displayText == displayText;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '$ParagraphLine($startIndex, $endIndex, $lineMetrics)';
|
||||
}
|
||||
}
|
||||
|
||||
/// The web implementation of [ui.ParagraphStyle].
|
||||
@ -496,6 +506,52 @@ class EngineTextStyle implements ui.TextStyle {
|
||||
);
|
||||
}
|
||||
|
||||
EngineTextStyle copyWith({
|
||||
ui.Color? color,
|
||||
ui.TextDecoration? decoration,
|
||||
ui.Color? decorationColor,
|
||||
ui.TextDecorationStyle? decorationStyle,
|
||||
double? decorationThickness,
|
||||
ui.FontWeight? fontWeight,
|
||||
ui.FontStyle? fontStyle,
|
||||
ui.TextBaseline? textBaseline,
|
||||
String? fontFamily,
|
||||
List<String>? fontFamilyFallback,
|
||||
double? fontSize,
|
||||
double? letterSpacing,
|
||||
double? wordSpacing,
|
||||
double? height,
|
||||
ui.Locale? locale,
|
||||
ui.Paint? background,
|
||||
ui.Paint? foreground,
|
||||
List<ui.Shadow>? shadows,
|
||||
List<ui.FontFeature>? fontFeatures,
|
||||
List<ui.FontVariation>? fontVariations,
|
||||
}) {
|
||||
return EngineTextStyle(
|
||||
color: color ?? this.color,
|
||||
decoration: decoration ?? this.decoration,
|
||||
decorationColor: decorationColor ?? this.decorationColor,
|
||||
decorationStyle: decorationStyle ?? this.decorationStyle,
|
||||
decorationThickness: decorationThickness ?? this.decorationThickness,
|
||||
fontWeight: fontWeight ?? this.fontWeight,
|
||||
fontStyle: fontStyle ?? this.fontStyle,
|
||||
textBaseline: textBaseline ?? this.textBaseline,
|
||||
fontFamily: fontFamily ?? this.fontFamily,
|
||||
fontFamilyFallback: fontFamilyFallback ?? this.fontFamilyFallback,
|
||||
fontSize: fontSize ?? this.fontSize,
|
||||
letterSpacing: letterSpacing ?? this.letterSpacing,
|
||||
wordSpacing: wordSpacing ?? this.wordSpacing,
|
||||
height: height ?? this.height,
|
||||
locale: locale ?? this.locale,
|
||||
background: background ?? this.background,
|
||||
foreground: foreground ?? this.foreground,
|
||||
shadows: shadows ?? this.shadows,
|
||||
fontFeatures: fontFeatures ?? this.fontFeatures,
|
||||
fontVariations: fontVariations ?? this.fontVariations,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
|
||||
@ -4,9 +4,63 @@
|
||||
|
||||
import 'package:ui/ui.dart' as ui;
|
||||
|
||||
import 'line_breaker.dart';
|
||||
import 'fragmenter.dart';
|
||||
import 'unicode_range.dart';
|
||||
|
||||
enum FragmentFlow {
|
||||
/// The fragment flows from left to right regardless of its surroundings.
|
||||
ltr,
|
||||
/// The fragment flows from right to left regardless of its surroundings.
|
||||
rtl,
|
||||
/// The fragment flows the same as the previous fragment.
|
||||
///
|
||||
/// If it's the first fragment in a line, then it flows the same as the
|
||||
/// paragraph direction.
|
||||
///
|
||||
/// E.g. digits.
|
||||
previous,
|
||||
/// If the previous and next fragments flow in the same direction, then this
|
||||
/// fragment flows in that same direction. Otherwise, it flows the same as the
|
||||
/// paragraph direction.
|
||||
///
|
||||
/// E.g. spaces, symbols.
|
||||
sandwich,
|
||||
}
|
||||
|
||||
/// Splits [text] into fragments based on directionality.
|
||||
class BidiFragmenter extends TextFragmenter {
|
||||
const BidiFragmenter(super.text);
|
||||
|
||||
@override
|
||||
List<BidiFragment> fragment() {
|
||||
return _computeBidiFragments(text);
|
||||
}
|
||||
}
|
||||
|
||||
class BidiFragment extends TextFragment {
|
||||
const BidiFragment(super.start, super.end, this.textDirection, this.fragmentFlow);
|
||||
|
||||
final ui.TextDirection? textDirection;
|
||||
final FragmentFlow fragmentFlow;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(start, end, textDirection, fragmentFlow);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is BidiFragment &&
|
||||
other.start == start &&
|
||||
other.end == end &&
|
||||
other.textDirection == textDirection &&
|
||||
other.fragmentFlow == fragmentFlow;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'BidiFragment($start, $end, $textDirection)';
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
@ -50,69 +104,83 @@ final UnicodePropertyLookup<ui.TextDirection?> _textDirectionLookup = UnicodePro
|
||||
null,
|
||||
);
|
||||
|
||||
/// Represents a block of text with a certain [ui.TextDirection].
|
||||
class DirectionalPosition {
|
||||
const DirectionalPosition(this.lineBreak, this.textDirection, this.isSpaceOnly);
|
||||
List<BidiFragment> _computeBidiFragments(String text) {
|
||||
final List<BidiFragment> fragments = <BidiFragment>[];
|
||||
|
||||
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);
|
||||
if (text.isEmpty) {
|
||||
fragments.add(const BidiFragment(0, 0, null, FragmentFlow.previous));
|
||||
return fragments;
|
||||
}
|
||||
|
||||
// Check if we are in a space-only block.
|
||||
if (start.index == end.indexWithoutTrailingSpaces) {
|
||||
return DirectionalPosition(end, null, true);
|
||||
}
|
||||
int fragmentStart = 0;
|
||||
ui.TextDirection? textDirection = _getTextDirection(text, 0);
|
||||
FragmentFlow fragmentFlow = _getFragmentFlow(text, 0);
|
||||
|
||||
final ui.TextDirection? blockDirection = _textDirectionLookup.find(text, start.index);
|
||||
int i = start.index + 1;
|
||||
for (int i = 1; i < text.length; i++) {
|
||||
final ui.TextDirection? charTextDirection = _getTextDirection(text, i);
|
||||
|
||||
while (i < end.indexWithoutTrailingSpaces) {
|
||||
final ui.TextDirection? direction = _textDirectionLookup.find(text, i);
|
||||
if (direction != blockDirection) {
|
||||
// Reached the next block.
|
||||
break;
|
||||
if (charTextDirection != textDirection) {
|
||||
// We've reached the end of a text direction fragment.
|
||||
fragments.add(BidiFragment(fragmentStart, i, textDirection, fragmentFlow));
|
||||
fragmentStart = i;
|
||||
textDirection = charTextDirection;
|
||||
|
||||
fragmentFlow = _getFragmentFlow(text, i);
|
||||
} else {
|
||||
// This code handles the case of a sequence of digits followed by a sequence
|
||||
// of LTR characters with no space in between.
|
||||
if (fragmentFlow == FragmentFlow.previous) {
|
||||
fragmentFlow = _getFragmentFlow(text, i);
|
||||
}
|
||||
}
|
||||
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,
|
||||
);
|
||||
fragments.add(BidiFragment(fragmentStart, text.length, textDirection, fragmentFlow));
|
||||
return fragments;
|
||||
}
|
||||
|
||||
ui.TextDirection? _getTextDirection(String text, int i) {
|
||||
final int codePoint = getCodePoint(text, i)!;
|
||||
if (_isDigit(codePoint) || _isMashriqiDigit(codePoint)) {
|
||||
// A sequence of regular digits or Mashriqi digits always goes from left to
|
||||
// regardless of their fragment flow direction.
|
||||
return ui.TextDirection.ltr;
|
||||
}
|
||||
|
||||
final ui.TextDirection? textDirection = _textDirectionLookup.findForChar(codePoint);
|
||||
if (textDirection != null) {
|
||||
return textDirection;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
FragmentFlow _getFragmentFlow(String text, int i) {
|
||||
final int codePoint = getCodePoint(text, i)!;
|
||||
if (_isDigit(codePoint)) {
|
||||
return FragmentFlow.previous;
|
||||
}
|
||||
if (_isMashriqiDigit(codePoint)) {
|
||||
return FragmentFlow.rtl;
|
||||
}
|
||||
|
||||
final ui.TextDirection? textDirection = _textDirectionLookup.findForChar(codePoint);
|
||||
switch (textDirection) {
|
||||
case ui.TextDirection.ltr:
|
||||
return FragmentFlow.ltr;
|
||||
|
||||
case ui.TextDirection.rtl:
|
||||
return FragmentFlow.rtl;
|
||||
|
||||
case null:
|
||||
return FragmentFlow.sandwich;
|
||||
}
|
||||
}
|
||||
|
||||
bool _isDigit(int codePoint) {
|
||||
return codePoint >= kChar_0 && codePoint <= kChar_9;
|
||||
}
|
||||
|
||||
bool _isMashriqiDigit(int codePoint) {
|
||||
return codePoint >= kMashriqi_0 && codePoint <= kMashriqi_9;
|
||||
}
|
||||
|
||||
@ -3,12 +3,14 @@
|
||||
// found in the LICENSE file.
|
||||
|
||||
const int kChar_0 = 48;
|
||||
const int kChar_9 = 57;
|
||||
const int kChar_9 = kChar_0 + 9;
|
||||
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 kMashriqi_0 = 0x660;
|
||||
const int kMashriqi_9 = kMashriqi_0 + 9;
|
||||
|
||||
enum _ComparisonResult {
|
||||
inside,
|
||||
|
||||
@ -10,6 +10,7 @@ import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
import 'paragraph/helper.dart';
|
||||
import 'screenshot.dart';
|
||||
|
||||
void main() {
|
||||
@ -225,8 +226,13 @@ Future<void> testMain() async {
|
||||
..lineTo(-r, 0)
|
||||
..close()).shift(const Offset(250, 250));
|
||||
|
||||
final SurfacePaintData borderPaint = SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
canvas.drawPath(path, pathPaint);
|
||||
canvas.drawParagraph(paragraph, const Offset(180, 50));
|
||||
canvas.drawRect(Rect.fromLTWH(180, 50, paragraph.width, paragraph.height), borderPaint);
|
||||
|
||||
expect(
|
||||
canvas.rootElement.querySelectorAll('flt-paragraph').map<String?>((DomElement e) => e.text).toList(),
|
||||
|
||||
@ -6,6 +6,22 @@ import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
import 'package:web_engine_tester/golden_tester.dart';
|
||||
|
||||
const LineBreakType prohibited = LineBreakType.prohibited;
|
||||
const LineBreakType opportunity = LineBreakType.opportunity;
|
||||
const LineBreakType mandatory = LineBreakType.mandatory;
|
||||
const LineBreakType endOfText = LineBreakType.endOfText;
|
||||
|
||||
const TextDirection ltr = TextDirection.ltr;
|
||||
const TextDirection rtl = TextDirection.rtl;
|
||||
|
||||
const FragmentFlow ffLtr = FragmentFlow.ltr;
|
||||
const FragmentFlow ffRtl = FragmentFlow.rtl;
|
||||
const FragmentFlow ffPrevious = FragmentFlow.previous;
|
||||
const FragmentFlow ffSandwich = FragmentFlow.sandwich;
|
||||
|
||||
const String rtlWord1 = 'واحدة';
|
||||
const String rtlWord2 = 'ثنتان';
|
||||
|
||||
const Color white = Color(0xFFFFFFFF);
|
||||
const Color black = Color(0xFF000000);
|
||||
const Color red = Color(0xFFFF0000);
|
||||
|
||||
@ -28,6 +28,9 @@ Future<void> testMain() async {
|
||||
const double fontSize = 22.0;
|
||||
const double width = 126.0;
|
||||
const double padding = 20.0;
|
||||
final SurfacePaintData borderPaint = SurfacePaintData()
|
||||
..color = black
|
||||
..style = PaintingStyle.stroke;
|
||||
|
||||
paragraph = rich(
|
||||
EngineParagraphStyle(fontFamily: 'Roboto', fontSize: fontSize, ellipsis: '...'),
|
||||
@ -39,6 +42,7 @@ Future<void> testMain() async {
|
||||
},
|
||||
)..layout(constrain(width));
|
||||
canvas.drawParagraph(paragraph, offset);
|
||||
canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, width, paragraph.height), borderPaint);
|
||||
offset = offset.translate(0, paragraph.height + padding);
|
||||
|
||||
paragraph = rich(
|
||||
@ -53,6 +57,7 @@ Future<void> testMain() async {
|
||||
},
|
||||
)..layout(constrain(width));
|
||||
canvas.drawParagraph(paragraph, offset);
|
||||
canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, width, paragraph.height), borderPaint);
|
||||
offset = offset.translate(0, paragraph.height + padding);
|
||||
|
||||
paragraph = rich(
|
||||
@ -83,6 +88,7 @@ Future<void> testMain() async {
|
||||
},
|
||||
)..layout(constrain(width));
|
||||
canvas.drawParagraph(paragraph, offset);
|
||||
canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, width, paragraph.height), borderPaint);
|
||||
offset = offset.translate(0, paragraph.height + padding);
|
||||
|
||||
paragraph = rich(
|
||||
@ -91,9 +97,9 @@ Future<void> testMain() async {
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
builder.addText('Lorem');
|
||||
builder.pushStyle(EngineTextStyle.only(color: green));
|
||||
builder.addText('ipsum');
|
||||
builder.addText('ipsu');
|
||||
builder.pushStyle(EngineTextStyle.only(color: red));
|
||||
builder.addText('dolor');
|
||||
builder.addText('mdolor');
|
||||
builder.pushStyle(EngineTextStyle.only(color: black));
|
||||
builder.addText('sit');
|
||||
builder.pushStyle(EngineTextStyle.only(color: blue));
|
||||
@ -103,6 +109,7 @@ Future<void> testMain() async {
|
||||
},
|
||||
)..layout(constrain(width));
|
||||
canvas.drawParagraph(paragraph, offset);
|
||||
canvas.drawRect(Rect.fromLTWH(offset.dx, offset.dy, width, paragraph.height), borderPaint);
|
||||
offset = offset.translate(0, paragraph.height + padding);
|
||||
}
|
||||
|
||||
|
||||
@ -7,6 +7,8 @@ import 'package:test/test.dart';
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
|
||||
import '../html/paragraph/helper.dart';
|
||||
|
||||
bool get isIosSafari =>
|
||||
browserEngine == BrowserEngine.webkit &&
|
||||
operatingSystem == OperatingSystem.iOs;
|
||||
@ -35,6 +37,28 @@ void main() {
|
||||
Future<void> testMain() async {
|
||||
await initializeTestFlutterViewEmbedder();
|
||||
|
||||
test('empty paragraph', () {
|
||||
final CanvasParagraph paragraph1 = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {},
|
||||
);
|
||||
expect(paragraph1.plainText, '');
|
||||
expect(paragraph1.spans, hasLength(1));
|
||||
expect(paragraph1.spans.single.start, 0);
|
||||
expect(paragraph1.spans.single.end, 0);
|
||||
|
||||
final CanvasParagraph paragraph2 = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.addText('');
|
||||
},
|
||||
);
|
||||
expect(paragraph2.plainText, '');
|
||||
expect(paragraph2.spans, hasLength(1));
|
||||
expect(paragraph2.spans.single.start, 0);
|
||||
expect(paragraph2.spans.single.end, 0);
|
||||
});
|
||||
|
||||
test('Builds a text-only canvas paragraph', () {
|
||||
final EngineParagraphStyle style = EngineParagraphStyle(fontSize: 13.0);
|
||||
final CanvasParagraphBuilder builder = CanvasParagraphBuilder(style);
|
||||
@ -142,7 +166,10 @@ Future<void> testMain() async {
|
||||
paragraph,
|
||||
'<flt-paragraph style="${paragraphStyle()}">'
|
||||
'<flt-span style="${spanStyle(top: 0, left: 0, width: 14*4)}">'
|
||||
'Hell...'
|
||||
'Hell'
|
||||
'</flt-span>'
|
||||
'<flt-span style="${spanStyle(top: 0, left: 14*4, width: 14*3)}">'
|
||||
'...'
|
||||
'</flt-span>'
|
||||
'</flt-paragraph>',
|
||||
ignorePositions: !isBlink,
|
||||
@ -356,10 +383,10 @@ Future<void> testMain() async {
|
||||
'<flt-span style="${spanStyle(top: 13, left: 0, width: 13*6, fontSize: 13)}">'
|
||||
'Second'
|
||||
'</flt-span>'
|
||||
'<flt-span style="${spanStyle(top: 13, left: 78, width: 13*1, fontSize: 13)}">'
|
||||
'<flt-span style="${spanStyle(top: 13, left: 13*6, width: 13*1, fontSize: 13)}">'
|
||||
' '
|
||||
'</flt-span>'
|
||||
'<flt-span style="${spanStyle(top: 13, left: 91, width: 13*13, fontSize: 13, fontStyle: 'italic')}">'
|
||||
'<flt-span style="${spanStyle(top: 13, left: 13*7, width: 13*13, fontSize: 13, fontStyle: 'italic')}">'
|
||||
'ThirdLongLine'
|
||||
'</flt-span>'
|
||||
'</flt-paragraph>',
|
||||
@ -377,8 +404,7 @@ Future<void> testMain() async {
|
||||
'<flt-span style="${spanStyle(top: 13, left: 0, width: 13*6, fontSize: 13)}">'
|
||||
'Second'
|
||||
'</flt-span>'
|
||||
// Trailing space.
|
||||
'<flt-span style="${spanStyle(top: 13, left: 78, width: 0, fontSize: 13)}">'
|
||||
'<flt-span style="${spanStyle(top: 13, left: 13*6, width: 0, fontSize: 13)}">'
|
||||
' '
|
||||
'</flt-span>'
|
||||
'<flt-span style="${spanStyle(top: 26, left: 0, width: 13*13, fontSize: 13, fontStyle: 'italic')}">'
|
||||
|
||||
@ -370,7 +370,7 @@ Future<void> testMain() async {
|
||||
);
|
||||
});
|
||||
|
||||
test('pops boxes when segments are popped', () {
|
||||
test('reverts to last line break opportunity', () {
|
||||
final CanvasParagraph paragraph = rich(ahemStyle, (ui.ParagraphBuilder builder) {
|
||||
// Lines:
|
||||
// "AAA "
|
||||
@ -383,6 +383,10 @@ Future<void> testMain() async {
|
||||
builder.addText('DD');
|
||||
});
|
||||
|
||||
String getTextForFragment(LayoutFragment fragment) {
|
||||
return paragraph.plainText.substring(fragment.start, fragment.end);
|
||||
}
|
||||
|
||||
// The layout algorithm will keep appending segments to the line builder
|
||||
// until it reaches: "AAA B_". At that point, it'll try to add the "C" but
|
||||
// realizes there isn't enough width in the line. Since the line already
|
||||
@ -398,29 +402,32 @@ Future<void> testMain() async {
|
||||
final ParagraphLine firstLine = paragraph.lines[0];
|
||||
final ParagraphLine secondLine = paragraph.lines[1];
|
||||
|
||||
// There should be no "B" in the first line's boxes.
|
||||
expect(firstLine.boxes, hasLength(2));
|
||||
// There should be no "B" in the first line's fragments.
|
||||
expect(firstLine.fragments, hasLength(2));
|
||||
|
||||
expect((firstLine.boxes[0] as SpanBox).toText(), 'AAA');
|
||||
expect((firstLine.boxes[0] as SpanBox).left, 0.0);
|
||||
expect(getTextForFragment(firstLine.fragments[0]), 'AAA');
|
||||
expect(firstLine.fragments[0].left, 0.0);
|
||||
|
||||
expect((firstLine.boxes[1] as SpanBox).toText(), ' ');
|
||||
expect((firstLine.boxes[1] as SpanBox).left, 30.0);
|
||||
expect(getTextForFragment(firstLine.fragments[1]), ' ');
|
||||
expect(firstLine.fragments[1].left, 30.0);
|
||||
|
||||
// Make sure the second line isn't missing any boxes.
|
||||
expect(secondLine.boxes, hasLength(4));
|
||||
// Make sure the second line isn't missing any fragments.
|
||||
expect(secondLine.fragments, hasLength(5));
|
||||
|
||||
expect((secondLine.boxes[0] as SpanBox).toText(), 'B');
|
||||
expect((secondLine.boxes[0] as SpanBox).left, 0.0);
|
||||
expect(getTextForFragment(secondLine.fragments[0]), 'B');
|
||||
expect(secondLine.fragments[0].left, 0.0);
|
||||
|
||||
expect((secondLine.boxes[1] as SpanBox).toText(), '_C');
|
||||
expect((secondLine.boxes[1] as SpanBox).left, 10.0);
|
||||
expect(getTextForFragment(secondLine.fragments[1]), '_');
|
||||
expect(secondLine.fragments[1].left, 10.0);
|
||||
|
||||
expect((secondLine.boxes[2] as SpanBox).toText(), ' ');
|
||||
expect((secondLine.boxes[2] as SpanBox).left, 30.0);
|
||||
expect(getTextForFragment(secondLine.fragments[2]), 'C');
|
||||
expect(secondLine.fragments[2].left, 20.0);
|
||||
|
||||
expect((secondLine.boxes[3] as SpanBox).toText(), 'DD');
|
||||
expect((secondLine.boxes[3] as SpanBox).left, 40.0);
|
||||
expect(getTextForFragment(secondLine.fragments[3]), ' ');
|
||||
expect(secondLine.fragments[3].left, 30.0);
|
||||
|
||||
expect(getTextForFragment(secondLine.fragments[4]), 'DD');
|
||||
expect(secondLine.fragments[4].left, 40.0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -0,0 +1,293 @@
|
||||
// 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';
|
||||
|
||||
import '../html/paragraph/helper.dart';
|
||||
|
||||
final EngineTextStyle defaultStyle = EngineTextStyle.only(
|
||||
color: const Color(0xFFFF0000),
|
||||
fontFamily: FlutterViewEmbedder.defaultFontFamily,
|
||||
fontSize: FlutterViewEmbedder.defaultFontSize,
|
||||
);
|
||||
final EngineTextStyle style1 = defaultStyle.copyWith(fontSize: 20);
|
||||
final EngineTextStyle style2 = defaultStyle.copyWith(color: blue);
|
||||
final EngineTextStyle style3 = defaultStyle.copyWith(fontFamily: 'Roboto');
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
group('$LayoutFragmenter', () {
|
||||
test('empty paragraph', () {
|
||||
final CanvasParagraph paragraph1 = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {},
|
||||
);
|
||||
expect(split(paragraph1), <_Fragment>[
|
||||
_Fragment('', endOfText, null, ffPrevious, defaultStyle),
|
||||
]);
|
||||
|
||||
final CanvasParagraph paragraph2 = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.addText('');
|
||||
},
|
||||
);
|
||||
expect(split(paragraph2), <_Fragment>[
|
||||
_Fragment('', endOfText, null, ffPrevious, defaultStyle),
|
||||
]);
|
||||
|
||||
final CanvasParagraph paragraph3 = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(style1);
|
||||
builder.addText('');
|
||||
},
|
||||
);
|
||||
expect(split(paragraph3), <_Fragment>[
|
||||
_Fragment('', endOfText, null, ffPrevious, style1),
|
||||
]);
|
||||
});
|
||||
|
||||
test('single span', () {
|
||||
final CanvasParagraph paragraph =
|
||||
plain(EngineParagraphStyle(), 'Lorem 12 $rtlWord1 ipsum34');
|
||||
expect(split(paragraph), <_Fragment>[
|
||||
_Fragment('Lorem', prohibited, ltr, ffLtr, defaultStyle),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, defaultStyle, sp: 1),
|
||||
_Fragment('12', prohibited, ltr, ffPrevious, defaultStyle),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, defaultStyle, sp: 1),
|
||||
_Fragment(rtlWord1, prohibited, rtl, ffRtl, defaultStyle),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, defaultStyle, sp: 3),
|
||||
_Fragment('ipsum34', endOfText, ltr, ffLtr, defaultStyle),
|
||||
]);
|
||||
});
|
||||
|
||||
test('multi span', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(style1);
|
||||
builder.addText('Lorem');
|
||||
builder.pop();
|
||||
builder.pushStyle(style2);
|
||||
builder.addText(' ipsum 12 ');
|
||||
builder.pop();
|
||||
builder.pushStyle(style3);
|
||||
builder.addText(' $rtlWord1 foo.');
|
||||
builder.pop();
|
||||
},
|
||||
);
|
||||
|
||||
expect(split(paragraph), <_Fragment>[
|
||||
_Fragment('Lorem', prohibited, ltr, ffLtr, style1),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style2, sp: 1),
|
||||
_Fragment('ipsum', prohibited, ltr, ffLtr, style2),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style2, sp: 1),
|
||||
_Fragment('12', prohibited, ltr, ffPrevious, style2),
|
||||
_Fragment(' ', prohibited, null, ffSandwich, style2, sp: 1),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style3, sp: 1),
|
||||
_Fragment(rtlWord1, prohibited, rtl, ffRtl, style3),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style3, sp: 1),
|
||||
_Fragment('foo', prohibited, ltr, ffLtr, style3),
|
||||
_Fragment('.', endOfText, null, ffSandwich, style3),
|
||||
]);
|
||||
});
|
||||
|
||||
test('new lines', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(style1);
|
||||
builder.addText('Lor\nem \n');
|
||||
builder.pop();
|
||||
builder.pushStyle(style2);
|
||||
builder.addText(' \n ipsum 12 ');
|
||||
builder.pop();
|
||||
builder.pushStyle(style3);
|
||||
builder.addText(' $rtlWord1 fo');
|
||||
builder.pop();
|
||||
builder.pushStyle(style1);
|
||||
builder.addText('o.');
|
||||
builder.pop();
|
||||
},
|
||||
);
|
||||
|
||||
expect(split(paragraph), <_Fragment>[
|
||||
_Fragment('Lor', prohibited, ltr, ffLtr, style1),
|
||||
_Fragment('\n', mandatory, null, ffSandwich, style1, nl: 1, sp: 1),
|
||||
_Fragment('em', prohibited, ltr, ffLtr, style1),
|
||||
_Fragment(' \n', mandatory, null, ffSandwich, style1, nl: 1, sp: 2),
|
||||
_Fragment(' \n', mandatory, null, ffSandwich, style2, nl: 1, sp: 2),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style2, sp: 2),
|
||||
_Fragment('ipsum', prohibited, ltr, ffLtr, style2),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style2, sp: 1),
|
||||
_Fragment('12', prohibited, ltr, ffPrevious, style2),
|
||||
_Fragment(' ', prohibited, null, ffSandwich, style2, sp: 1),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style3, sp: 1),
|
||||
_Fragment(rtlWord1, prohibited, rtl, ffRtl, style3),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style3, sp: 1),
|
||||
_Fragment('fo', prohibited, ltr, ffLtr, style3),
|
||||
_Fragment('o', prohibited, ltr, ffLtr, style1),
|
||||
_Fragment('.', endOfText, null, ffSandwich, style1),
|
||||
]);
|
||||
});
|
||||
|
||||
test('last line is empty', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(style1);
|
||||
builder.addText('Lorem \n');
|
||||
builder.pop();
|
||||
builder.pushStyle(style2);
|
||||
builder.addText(' \n ipsum \n');
|
||||
builder.pop();
|
||||
},
|
||||
);
|
||||
|
||||
expect(split(paragraph), <_Fragment>[
|
||||
_Fragment('Lorem', prohibited, ltr, ffLtr, style1),
|
||||
_Fragment(' \n', mandatory, null, ffSandwich, style1, nl: 1, sp: 2),
|
||||
_Fragment(' \n', mandatory, null, ffSandwich, style2, nl: 1, sp: 2),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style2, sp: 2),
|
||||
_Fragment('ipsum', prohibited, ltr, ffLtr, style2),
|
||||
_Fragment(' \n', mandatory, null, ffSandwich, style2, nl: 1, sp: 2),
|
||||
_Fragment('', endOfText, null, ffSandwich, style2),
|
||||
]);
|
||||
});
|
||||
|
||||
test('space-only spans', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.addText('Lorem ');
|
||||
builder.pushStyle(style1);
|
||||
builder.addText(' ');
|
||||
builder.pop();
|
||||
builder.pushStyle(style2);
|
||||
builder.addText(' ');
|
||||
builder.pop();
|
||||
builder.addText('ipsum');
|
||||
},
|
||||
);
|
||||
|
||||
expect(split(paragraph), <_Fragment>[
|
||||
_Fragment('Lorem', prohibited, ltr, ffLtr, defaultStyle),
|
||||
_Fragment(' ', prohibited, null, ffSandwich, defaultStyle, sp: 1),
|
||||
_Fragment(' ', prohibited, null, ffSandwich, style1, sp: 3),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style2, sp: 2),
|
||||
_Fragment('ipsum', endOfText, ltr, ffLtr, defaultStyle),
|
||||
]);
|
||||
});
|
||||
|
||||
test('placeholders', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.pushStyle(style1);
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('Lorem');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('ipsum\n');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.pop();
|
||||
builder.pushStyle(style2);
|
||||
builder.addText('$rtlWord1 ');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('\nsit');
|
||||
builder.pop();
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
},
|
||||
);
|
||||
|
||||
final String placeholderChar = String.fromCharCode(0xFFFC);
|
||||
|
||||
expect(split(paragraph), <_Fragment>[
|
||||
_Fragment(placeholderChar, opportunity, ltr, ffLtr, null),
|
||||
_Fragment('Lorem', opportunity, ltr, ffLtr, style1),
|
||||
_Fragment(placeholderChar, opportunity, ltr, ffLtr, null),
|
||||
_Fragment('ipsum', prohibited, ltr, ffLtr, style1),
|
||||
_Fragment('\n', mandatory, null, ffSandwich, style1, nl: 1, sp: 1),
|
||||
_Fragment(placeholderChar, opportunity, ltr, ffLtr, null),
|
||||
_Fragment(rtlWord1, prohibited, rtl, ffRtl, style2),
|
||||
_Fragment(' ', opportunity, null, ffSandwich, style2, sp: 1),
|
||||
_Fragment(placeholderChar, prohibited, ltr, ffLtr, null),
|
||||
_Fragment('\n', mandatory, null, ffSandwich, style2, nl: 1, sp: 1),
|
||||
_Fragment('sit', opportunity, ltr, ffLtr, style2),
|
||||
_Fragment(placeholderChar, endOfText, ltr, ffLtr, null),
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Holds information about how a fragment.
|
||||
class _Fragment {
|
||||
_Fragment(this.text, this.type, this.textDirection, this.fragmentFlow, this.style, {
|
||||
this.nl = 0,
|
||||
this.sp = 0,
|
||||
});
|
||||
|
||||
factory _Fragment._fromLayoutFragment(String text, LayoutFragment layoutFragment) {
|
||||
final ParagraphSpan span = layoutFragment.span;
|
||||
return _Fragment(
|
||||
text.substring(layoutFragment.start, layoutFragment.end),
|
||||
layoutFragment.type,
|
||||
layoutFragment.textDirection,
|
||||
layoutFragment.fragmentFlow,
|
||||
span is FlatTextSpan ? span.style : null,
|
||||
nl: layoutFragment.trailingNewlines,
|
||||
sp: layoutFragment.trailingSpaces,
|
||||
);
|
||||
}
|
||||
|
||||
final String text;
|
||||
final LineBreakType type;
|
||||
final TextDirection? textDirection;
|
||||
final FragmentFlow fragmentFlow;
|
||||
final EngineTextStyle? style;
|
||||
|
||||
/// The number of trailing new line characters.
|
||||
final int nl;
|
||||
|
||||
/// The number of trailing spaces.
|
||||
final int sp;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(text, type, textDirection, fragmentFlow, style, nl, sp);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _Fragment &&
|
||||
other.text == text &&
|
||||
other.type == type &&
|
||||
other.textDirection == textDirection &&
|
||||
other.fragmentFlow == fragmentFlow &&
|
||||
other.style == style &&
|
||||
other.nl == nl &&
|
||||
other.sp == sp;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '"$text" ($type, $textDirection, $fragmentFlow, nl: $nl, sp: $sp)';
|
||||
}
|
||||
}
|
||||
|
||||
List<_Fragment> split(CanvasParagraph paragraph) {
|
||||
return <_Fragment>[
|
||||
for (final LayoutFragment layoutFragment
|
||||
in computeLayoutFragments(paragraph))
|
||||
_Fragment._fromLayoutFragment(paragraph.plainText, layoutFragment)
|
||||
];
|
||||
}
|
||||
|
||||
List<LayoutFragment> computeLayoutFragments(CanvasParagraph paragraph) {
|
||||
return LayoutFragmenter(paragraph.plainText, paragraph.spans).fragment();
|
||||
}
|
||||
@ -10,7 +10,6 @@ TestLine l(
|
||||
String? displayText,
|
||||
int? startIndex,
|
||||
int? endIndex, {
|
||||
int? endIndexWithoutNewlines,
|
||||
bool? hardBreak,
|
||||
double? height,
|
||||
double? width,
|
||||
@ -22,7 +21,6 @@ TestLine l(
|
||||
displayText: displayText,
|
||||
startIndex: startIndex,
|
||||
endIndex: endIndex,
|
||||
endIndexWithoutNewlines: endIndexWithoutNewlines,
|
||||
hardBreak: hardBreak,
|
||||
height: height,
|
||||
width: width,
|
||||
@ -33,7 +31,6 @@ TestLine l(
|
||||
}
|
||||
|
||||
void expectLines(CanvasParagraph paragraph, List<TestLine> expectedLines) {
|
||||
final String text = paragraph.toPlainText();
|
||||
final List<ParagraphLine> lines = paragraph.lines;
|
||||
expect(lines, hasLength(expectedLines.length));
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
@ -46,11 +43,7 @@ void expectLines(CanvasParagraph paragraph, List<TestLine> expectedLines) {
|
||||
reason: 'line #$i had the wrong `lineNumber`. Expected: $i. Actual: ${line.lineNumber}',
|
||||
);
|
||||
if (expectedLine.displayText != null) {
|
||||
String displayText =
|
||||
text.substring(line.startIndex, line.endIndexWithoutNewlines);
|
||||
if (line.ellipsis != null) {
|
||||
displayText += line.ellipsis!;
|
||||
}
|
||||
final String displayText = line.getText(paragraph);
|
||||
expect(
|
||||
displayText,
|
||||
expectedLine.displayText,
|
||||
@ -74,14 +67,6 @@ void expectLines(CanvasParagraph paragraph, List<TestLine> expectedLines) {
|
||||
'line #$i had a different `endIndex` value: "${line.endIndex}" vs. "${expectedLine.endIndex}"',
|
||||
);
|
||||
}
|
||||
if (expectedLine.endIndexWithoutNewlines != null) {
|
||||
expect(
|
||||
line.endIndexWithoutNewlines,
|
||||
expectedLine.endIndexWithoutNewlines,
|
||||
reason:
|
||||
'line #$i had a different `endIndexWithoutNewlines` value: "${line.endIndexWithoutNewlines}" vs. "${expectedLine.endIndexWithoutNewlines}"',
|
||||
);
|
||||
}
|
||||
if (expectedLine.hardBreak != null) {
|
||||
expect(
|
||||
line.hardBreak,
|
||||
@ -130,7 +115,6 @@ class TestLine {
|
||||
this.displayText,
|
||||
this.startIndex,
|
||||
this.endIndex,
|
||||
this.endIndexWithoutNewlines,
|
||||
this.hardBreak,
|
||||
this.height,
|
||||
this.width,
|
||||
@ -142,7 +126,6 @@ class TestLine {
|
||||
final String? displayText;
|
||||
final int? startIndex;
|
||||
final int? endIndex;
|
||||
final int? endIndexWithoutNewlines;
|
||||
final bool? hardBreak;
|
||||
final double? height;
|
||||
final double? width;
|
||||
|
||||
@ -46,8 +46,10 @@ Future<void> testMain() async {
|
||||
|
||||
expect(paragraph.maxIntrinsicWidth, 0);
|
||||
expect(paragraph.minIntrinsicWidth, 0);
|
||||
expect(paragraph.height, 0);
|
||||
expect(paragraph.computeLineMetrics(), isEmpty);
|
||||
expect(paragraph.height, 10);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('', 0, 0, width: 0.0, height: 10.0, baseline: 8.0),
|
||||
]);
|
||||
});
|
||||
|
||||
test('preserves whitespace when measuring', () {
|
||||
@ -422,7 +424,7 @@ Future<void> testMain() async {
|
||||
expect(longText.maxIntrinsicWidth, 480);
|
||||
expect(longText.height, 10);
|
||||
expectLines(longText, <TestLine>[
|
||||
l('AA...', 0, 2, hardBreak: false, width: 50.0, left: 0.0),
|
||||
l('AA...', 0, 2, hardBreak: true, width: 50.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// The short prefix should make the text break into two lines, but the
|
||||
@ -436,7 +438,7 @@ Future<void> testMain() async {
|
||||
expect(longTextShortPrefix.height, 20);
|
||||
expectLines(longTextShortPrefix, <TestLine>[
|
||||
l('AAA', 0, 4, hardBreak: true, width: 30.0, left: 0.0),
|
||||
l('AA...', 4, 6, hardBreak: false, width: 50.0, left: 0.0),
|
||||
l('AA...', 4, 6, hardBreak: true, width: 50.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// Constraints only enough to fit "AA" with the ellipsis, but not the
|
||||
@ -447,7 +449,7 @@ Future<void> testMain() async {
|
||||
expect(trailingSpace.maxIntrinsicWidth, 60);
|
||||
expect(trailingSpace.height, 10);
|
||||
expectLines(trailingSpace, <TestLine>[
|
||||
l('AA...', 0, 2, hardBreak: false, width: 50.0, left: 0.0),
|
||||
l('AA...', 0, 2, hardBreak: true, width: 50.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// Tiny constraints.
|
||||
@ -457,7 +459,7 @@ Future<void> testMain() async {
|
||||
expect(paragraph.maxIntrinsicWidth, 40);
|
||||
expect(paragraph.height, 10);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('...', 0, 0, hardBreak: false, width: 30.0, left: 0.0),
|
||||
l('...', 0, 0, hardBreak: true, width: 30.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// Tinier constraints (not enough for the ellipsis).
|
||||
@ -471,7 +473,7 @@ Future<void> testMain() async {
|
||||
// l('.', 0, 0, hardBreak: false, width: 10.0, left: 0.0),
|
||||
// ]);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('...', 0, 0, hardBreak: false, width: 30.0, left: 0.0),
|
||||
l('...', 0, 0, hardBreak: true, width: 30.0, left: 0.0),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -550,14 +552,14 @@ Future<void> testMain() async {
|
||||
paragraph = plain(onelineStyle, 'abcd efg')..layout(constrain(60.0));
|
||||
expect(paragraph.height, 10);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('abc...', 0, 3, hardBreak: false, width: 60.0, left: 0.0),
|
||||
l('abc...', 0, 3, hardBreak: true, width: 60.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// Another simple overflow case.
|
||||
paragraph = plain(onelineStyle, 'a bcde fgh')..layout(constrain(60.0));
|
||||
expect(paragraph.height, 10);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('a b...', 0, 3, hardBreak: false, width: 60.0, left: 0.0),
|
||||
l('a b...', 0, 3, hardBreak: true, width: 60.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// The ellipsis is supposed to go on the second line, but because the
|
||||
@ -574,7 +576,7 @@ Future<void> testMain() async {
|
||||
expect(paragraph.height, 20);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('abcd ', 0, 5, hardBreak: false, width: 40.0, left: 0.0),
|
||||
l('efg...', 5, 8, hardBreak: false, width: 60.0, left: 0.0),
|
||||
l('efg...', 5, 8, hardBreak: true, width: 60.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// Even if the second line can be broken, we don't break it, we just
|
||||
@ -584,7 +586,7 @@ Future<void> testMain() async {
|
||||
expect(paragraph.height, 20);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('abcde ', 0, 6, hardBreak: false, width: 50.0, left: 0.0),
|
||||
l('f g...', 6, 9, hardBreak: false, width: 60.0, left: 0.0),
|
||||
l('f g...', 6, 9, hardBreak: true, width: 60.0, left: 0.0),
|
||||
]);
|
||||
|
||||
// First line overflows but second line doesn't.
|
||||
@ -601,7 +603,7 @@ Future<void> testMain() async {
|
||||
expect(paragraph.height, 20);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('abcdef', 0, 6, hardBreak: false, width: 60.0, left: 0.0),
|
||||
l('g h...', 6, 9, hardBreak: false, width: 60.0, left: 0.0),
|
||||
l('g h...', 6, 9, hardBreak: true, width: 60.0, left: 0.0),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,8 @@ import 'package:ui/ui.dart' as ui;
|
||||
import '../html/paragraph/helper.dart';
|
||||
import 'layout_service_helper.dart';
|
||||
|
||||
final String placeholderChar = String.fromCharCode(0xFFFC);
|
||||
|
||||
const ui.Color white = ui.Color(0xFFFFFFFF);
|
||||
const ui.Color black = ui.Color(0xFF000000);
|
||||
const ui.Color red = ui.Color(0xFFFF0000);
|
||||
@ -166,7 +168,7 @@ Future<void> testMain() async {
|
||||
expect(paragraph.minIntrinsicWidth, 300.0);
|
||||
expect(paragraph.height, 50.0);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('', 0, 0, hardBreak: true, width: 300.0, left: 100.0),
|
||||
l(placeholderChar, 0, 1, hardBreak: true, width: 300.0, left: 100.0),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -185,7 +187,7 @@ Future<void> testMain() async {
|
||||
expect(paragraph.minIntrinsicWidth, 300.0);
|
||||
expect(paragraph.height, 50.0);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('abcd', 0, 4, hardBreak: true, width: 340.0, left: 30.0),
|
||||
l('abcd$placeholderChar', 0, 5, hardBreak: true, width: 340.0, left: 30.0),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -207,8 +209,8 @@ Future<void> testMain() async {
|
||||
expect(paragraph.height, 10.0 + 40.0 + 10.0);
|
||||
expectLines(paragraph, <TestLine>[
|
||||
l('Lorem', 0, 6, hardBreak: true, width: 50.0, height: 10.0, left: 125.0),
|
||||
l('', 6, 6, hardBreak: false, width: 300.0, height: 40.0, left: 0.0),
|
||||
l('ipsum', 6, 11, hardBreak: true, width: 50.0, height: 10.0, left: 125.0),
|
||||
l(placeholderChar, 6, 7, hardBreak: false, width: 300.0, height: 40.0, left: 0.0),
|
||||
l('ipsum', 7, 12, hardBreak: true, width: 50.0, height: 10.0, left: 125.0),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
@ -6,53 +6,53 @@ import 'package:test/bootstrap/browser.dart';
|
||||
import 'package:test/test.dart';
|
||||
|
||||
import 'package:ui/src/engine.dart';
|
||||
import 'package:ui/ui.dart';
|
||||
|
||||
import '../html/paragraph/helper.dart';
|
||||
import 'line_breaker_test_helper.dart';
|
||||
import 'line_breaker_test_raw_data.dart';
|
||||
|
||||
final String placeholderChar = String.fromCharCode(0xFFFC);
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
void testMain() {
|
||||
group('nextLineBreak', () {
|
||||
test('Does not go beyond the ends of a string', () {
|
||||
expect(split('foo'), <Line>[
|
||||
Line('foo', LineBreakType.endOfText),
|
||||
group('$LineBreakFragmenter', () {
|
||||
test('empty string', () {
|
||||
expect(split(''), <Line>[
|
||||
Line('', endOfText),
|
||||
]);
|
||||
|
||||
final LineBreakResult result = nextLineBreak('foo', 'foo'.length);
|
||||
expect(result.index, 'foo'.length);
|
||||
expect(result.type, LineBreakType.endOfText);
|
||||
});
|
||||
|
||||
test('whitespace', () {
|
||||
expect(split('foo bar'), <Line>[
|
||||
Line('foo ', LineBreakType.opportunity),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo ', opportunity, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split(' foo bar '), <Line>[
|
||||
Line(' ', LineBreakType.opportunity),
|
||||
Line('foo ', LineBreakType.opportunity),
|
||||
Line('bar ', LineBreakType.endOfText),
|
||||
Line(' ', opportunity, sp: 2),
|
||||
Line('foo ', opportunity, sp: 4),
|
||||
Line('bar ', endOfText, sp: 2),
|
||||
]);
|
||||
});
|
||||
|
||||
test('single-letter lines', () {
|
||||
expect(split('foo a bar'), <Line>[
|
||||
Line('foo ', LineBreakType.opportunity),
|
||||
Line('a ', LineBreakType.opportunity),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo ', opportunity, sp: 1),
|
||||
Line('a ', opportunity, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split('a b c'), <Line>[
|
||||
Line('a ', LineBreakType.opportunity),
|
||||
Line('b ', LineBreakType.opportunity),
|
||||
Line('c', LineBreakType.endOfText),
|
||||
Line('a ', opportunity, sp: 1),
|
||||
Line('b ', opportunity, sp: 1),
|
||||
Line('c', endOfText),
|
||||
]);
|
||||
expect(split(' a b '), <Line>[
|
||||
Line(' ', LineBreakType.opportunity),
|
||||
Line('a ', LineBreakType.opportunity),
|
||||
Line('b ', LineBreakType.endOfText),
|
||||
Line(' ', opportunity, sp: 1),
|
||||
Line('a ', opportunity, sp: 1),
|
||||
Line('b ', endOfText, sp: 1),
|
||||
]);
|
||||
});
|
||||
|
||||
@ -60,242 +60,344 @@ void testMain() {
|
||||
final String bk = String.fromCharCode(0x000B);
|
||||
// Can't have a line break between CR×LF.
|
||||
expect(split('foo\r\nbar'), <Line>[
|
||||
Line('foo\r\n', LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo\r\n', mandatory, nl: 2, sp: 2),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
|
||||
// Any other new line is considered a line break on its own.
|
||||
|
||||
expect(split('foo\n\nbar'), <Line>[
|
||||
Line('foo\n', LineBreakType.mandatory),
|
||||
Line('\n', LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo\n', mandatory, nl: 1, sp: 1),
|
||||
Line('\n', mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split('foo\r\rbar'), <Line>[
|
||||
Line('foo\r', LineBreakType.mandatory),
|
||||
Line('\r', LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo\r', mandatory, nl: 1, sp: 1),
|
||||
Line('\r', mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split('foo$bk${bk}bar'), <Line>[
|
||||
Line('foo$bk', LineBreakType.mandatory),
|
||||
Line(bk, LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo$bk', mandatory, nl: 1, sp: 1),
|
||||
Line(bk, mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
|
||||
expect(split('foo\n\rbar'), <Line>[
|
||||
Line('foo\n', LineBreakType.mandatory),
|
||||
Line('\r', LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo\n', mandatory, nl: 1, sp: 1),
|
||||
Line('\r', mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split('foo$bk\rbar'), <Line>[
|
||||
Line('foo$bk', LineBreakType.mandatory),
|
||||
Line('\r', LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo$bk', mandatory, nl: 1, sp: 1),
|
||||
Line('\r', mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split('foo\r${bk}bar'), <Line>[
|
||||
Line('foo\r', LineBreakType.mandatory),
|
||||
Line(bk, LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo\r', mandatory, nl: 1, sp: 1),
|
||||
Line(bk, mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split('foo$bk\nbar'), <Line>[
|
||||
Line('foo$bk', LineBreakType.mandatory),
|
||||
Line('\n', LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo$bk', mandatory, nl: 1, sp: 1),
|
||||
Line('\n', mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
expect(split('foo\n${bk}bar'), <Line>[
|
||||
Line('foo\n', LineBreakType.mandatory),
|
||||
Line(bk, LineBreakType.mandatory),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo\n', mandatory, nl: 1, sp: 1),
|
||||
Line(bk, mandatory, nl: 1, sp: 1),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
|
||||
// New lines at the beginning and end.
|
||||
|
||||
expect(split('foo\n'), <Line>[
|
||||
Line('foo\n', LineBreakType.mandatory),
|
||||
Line('', LineBreakType.endOfText),
|
||||
Line('foo\n', mandatory, nl: 1, sp: 1),
|
||||
Line('', endOfText),
|
||||
]);
|
||||
expect(split('foo\r'), <Line>[
|
||||
Line('foo\r', LineBreakType.mandatory),
|
||||
Line('', LineBreakType.endOfText),
|
||||
Line('foo\r', mandatory, nl: 1, sp: 1),
|
||||
Line('', endOfText),
|
||||
]);
|
||||
expect(split('foo$bk'), <Line>[
|
||||
Line('foo$bk', LineBreakType.mandatory),
|
||||
Line('', LineBreakType.endOfText),
|
||||
Line('foo$bk', mandatory, nl: 1, sp: 1),
|
||||
Line('', endOfText),
|
||||
]);
|
||||
|
||||
expect(split('\nfoo'), <Line>[
|
||||
Line('\n', LineBreakType.mandatory),
|
||||
Line('foo', LineBreakType.endOfText),
|
||||
Line('\n', mandatory, nl: 1, sp: 1),
|
||||
Line('foo', endOfText),
|
||||
]);
|
||||
expect(split('\rfoo'), <Line>[
|
||||
Line('\r', LineBreakType.mandatory),
|
||||
Line('foo', LineBreakType.endOfText),
|
||||
Line('\r', mandatory, nl: 1, sp: 1),
|
||||
Line('foo', endOfText),
|
||||
]);
|
||||
expect(split('${bk}foo'), <Line>[
|
||||
Line(bk, LineBreakType.mandatory),
|
||||
Line('foo', LineBreakType.endOfText),
|
||||
Line(bk, mandatory, nl: 1, sp: 1),
|
||||
Line('foo', endOfText),
|
||||
]);
|
||||
|
||||
// Whitespace with new lines.
|
||||
|
||||
expect(split('foo \n'), <Line>[
|
||||
Line('foo \n', LineBreakType.mandatory),
|
||||
Line('', LineBreakType.endOfText),
|
||||
Line('foo \n', mandatory, nl: 1, sp: 3),
|
||||
Line('', endOfText),
|
||||
]);
|
||||
|
||||
expect(split('foo \n '), <Line>[
|
||||
Line('foo \n', LineBreakType.mandatory),
|
||||
Line(' ', LineBreakType.endOfText),
|
||||
Line('foo \n', mandatory, nl: 1, sp: 3),
|
||||
Line(' ', endOfText, sp: 3),
|
||||
]);
|
||||
|
||||
expect(split('foo \n bar'), <Line>[
|
||||
Line('foo \n', LineBreakType.mandatory),
|
||||
Line(' ', LineBreakType.opportunity),
|
||||
Line('bar', LineBreakType.endOfText),
|
||||
Line('foo \n', mandatory, nl: 1, sp: 3),
|
||||
Line(' ', opportunity, sp: 3),
|
||||
Line('bar', endOfText),
|
||||
]);
|
||||
|
||||
expect(split('\n foo'), <Line>[
|
||||
Line('\n', LineBreakType.mandatory),
|
||||
Line(' ', LineBreakType.opportunity),
|
||||
Line('foo', LineBreakType.endOfText),
|
||||
Line('\n', mandatory, nl: 1, sp: 1),
|
||||
Line(' ', opportunity, sp: 2),
|
||||
Line('foo', endOfText),
|
||||
]);
|
||||
expect(split(' \n foo'), <Line>[
|
||||
Line(' \n', LineBreakType.mandatory),
|
||||
Line(' ', LineBreakType.opportunity),
|
||||
Line('foo', LineBreakType.endOfText),
|
||||
Line(' \n', mandatory, nl: 1, sp: 4),
|
||||
Line(' ', opportunity, sp: 2),
|
||||
Line('foo', endOfText),
|
||||
]);
|
||||
});
|
||||
|
||||
test('trailing spaces and new lines', () {
|
||||
expect(
|
||||
findBreaks('foo bar '),
|
||||
const <LineBreakResult>[
|
||||
LineBreakResult(4, 4, 3, LineBreakType.opportunity),
|
||||
LineBreakResult(9, 9, 7, LineBreakType.endOfText),
|
||||
expect(split('foo bar '), <Line>[
|
||||
Line('foo ', opportunity, sp: 1),
|
||||
Line('bar ', endOfText, sp: 2),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
findBreaks('foo \nbar\nbaz \n'),
|
||||
const <LineBreakResult>[
|
||||
LineBreakResult(6, 5, 3, LineBreakType.mandatory),
|
||||
LineBreakResult(10, 9, 9, LineBreakType.mandatory),
|
||||
LineBreakResult(17, 16, 13, LineBreakType.mandatory),
|
||||
LineBreakResult(17, 17, 17, LineBreakType.endOfText),
|
||||
expect(split('foo \nbar\nbaz \n'), <Line>[
|
||||
Line('foo \n', mandatory, nl: 1, sp: 3),
|
||||
Line('bar\n', mandatory, nl: 1, sp: 1),
|
||||
Line('baz \n', mandatory, nl: 1, sp: 4),
|
||||
Line('', endOfText),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('leading spaces', () {
|
||||
expect(
|
||||
findBreaks(' foo'),
|
||||
const <LineBreakResult>[
|
||||
LineBreakResult(1, 1, 0, LineBreakType.opportunity),
|
||||
LineBreakResult(4, 4, 4, LineBreakType.endOfText),
|
||||
expect(split(' foo'), <Line>[
|
||||
Line(' ', opportunity, sp: 1),
|
||||
Line('foo', endOfText),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
findBreaks(' foo'),
|
||||
const <LineBreakResult>[
|
||||
LineBreakResult(3, 3, 0, LineBreakType.opportunity),
|
||||
LineBreakResult(6, 6, 6, LineBreakType.endOfText),
|
||||
expect(split(' foo'), <Line>[
|
||||
Line(' ', opportunity, sp: 3),
|
||||
Line('foo', endOfText),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
findBreaks(' foo bar'),
|
||||
const <LineBreakResult>[
|
||||
LineBreakResult(2, 2, 0, LineBreakType.opportunity),
|
||||
LineBreakResult(8, 8, 5, LineBreakType.opportunity),
|
||||
LineBreakResult(11, 11, 11, LineBreakType.endOfText),
|
||||
expect(split(' foo bar'), <Line>[
|
||||
Line(' ', opportunity, sp: 2),
|
||||
Line('foo ', opportunity, sp: 3),
|
||||
Line('bar', endOfText),
|
||||
],
|
||||
);
|
||||
|
||||
expect(
|
||||
findBreaks(' \n foo'),
|
||||
const <LineBreakResult>[
|
||||
LineBreakResult(3, 2, 0, LineBreakType.mandatory),
|
||||
LineBreakResult(6, 6, 3, LineBreakType.opportunity),
|
||||
LineBreakResult(9, 9, 9, LineBreakType.endOfText),
|
||||
expect(split(' \n foo'), <Line>[
|
||||
Line(' \n', mandatory, nl: 1, sp: 3),
|
||||
Line(' ', opportunity, sp: 3),
|
||||
Line('foo', endOfText),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('whitespace before the last character', () {
|
||||
const String text = 'Lorem sit .';
|
||||
const LineBreakResult expectedResult =
|
||||
LineBreakResult(10, 10, 9, LineBreakType.opportunity);
|
||||
expect(split('Lorem sit .'), <Line>[
|
||||
Line('Lorem ', opportunity, sp: 1),
|
||||
Line('sit ', opportunity, sp: 1),
|
||||
Line('.', endOfText),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
LineBreakResult result;
|
||||
test('placeholders', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('Lorem');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('ipsum\n');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('dolor');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('\nsit');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
},
|
||||
);
|
||||
|
||||
result = nextLineBreak(text, 6);
|
||||
expect(result, expectedResult);
|
||||
final String placeholderChar = String.fromCharCode(0xFFFC);
|
||||
|
||||
result = nextLineBreak(text, 9);
|
||||
expect(result, expectedResult);
|
||||
expect(splitParagraph(paragraph), <Line>[
|
||||
Line(placeholderChar, opportunity),
|
||||
Line('Lorem', opportunity),
|
||||
Line(placeholderChar, opportunity),
|
||||
Line('ipsum\n', mandatory, nl: 1, sp: 1),
|
||||
Line(placeholderChar, opportunity),
|
||||
Line('dolor', opportunity),
|
||||
Line('$placeholderChar\n', mandatory, nl: 1, sp: 1),
|
||||
Line('sit', opportunity),
|
||||
Line(placeholderChar, endOfText),
|
||||
]);
|
||||
});
|
||||
|
||||
result = nextLineBreak(text, 9, maxEnd: 10);
|
||||
expect(result, expectedResult);
|
||||
test('single placeholder', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
},
|
||||
);
|
||||
|
||||
final String placeholderChar = String.fromCharCode(0xFFFC);
|
||||
|
||||
expect(splitParagraph(paragraph), <Line>[
|
||||
Line(placeholderChar, endOfText),
|
||||
]);
|
||||
});
|
||||
|
||||
test('placeholders surrounded by spaces and new lines', () {
|
||||
final CanvasParagraph paragraph = rich(
|
||||
EngineParagraphStyle(),
|
||||
(CanvasParagraphBuilder builder) {
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText(' Lorem ');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText(' \nipsum \n');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
builder.addText('\n');
|
||||
builder.addPlaceholder(100, 100, PlaceholderAlignment.top);
|
||||
},
|
||||
);
|
||||
|
||||
expect(splitParagraph(paragraph), <Line>[
|
||||
Line('$placeholderChar ', opportunity, sp: 2),
|
||||
Line('Lorem ', opportunity, sp: 2),
|
||||
Line('$placeholderChar \n', mandatory, nl: 1, sp: 3),
|
||||
Line('ipsum \n', mandatory, nl: 1, sp: 2),
|
||||
Line('$placeholderChar\n', mandatory, nl: 1, sp: 1),
|
||||
Line(placeholderChar, endOfText),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('surrogates', () {
|
||||
expect(split('A\u{1F600}'), <Line>[
|
||||
Line('A', opportunity),
|
||||
Line('\u{1F600}', endOfText),
|
||||
],
|
||||
);
|
||||
|
||||
expect(split('\u{1F600}A'), <Line>[
|
||||
Line('\u{1F600}', opportunity),
|
||||
Line('A', endOfText),
|
||||
],
|
||||
);
|
||||
|
||||
expect(split('\u{1F600}\u{1F600}'), <Line>[
|
||||
Line('\u{1F600}', opportunity),
|
||||
Line('\u{1F600}', endOfText),
|
||||
],
|
||||
);
|
||||
|
||||
expect(split('A \u{1F600} \u{1F600}'), <Line>[
|
||||
Line('A ', opportunity, sp: 1),
|
||||
Line('\u{1F600} ', opportunity, sp: 1),
|
||||
Line('\u{1F600}', endOfText),
|
||||
],
|
||||
);
|
||||
});
|
||||
|
||||
test('comprehensive test', () {
|
||||
final List<TestCase> testCollection = parseRawTestData(rawLineBreakTestData);
|
||||
final List<TestCase> testCollection =
|
||||
parseRawTestData(rawLineBreakTestData);
|
||||
for (int t = 0; t < testCollection.length; t++) {
|
||||
final TestCase testCase = testCollection[t];
|
||||
final String text = testCase.toText();
|
||||
|
||||
int lastLineBreak = 0;
|
||||
final String text = testCase.toText();
|
||||
final List<LineBreakFragment> fragments = LineBreakFragmenter(text).fragment();
|
||||
|
||||
// `f` is the index in the `fragments` list.
|
||||
int f = 0;
|
||||
LineBreakFragment currentFragment = fragments[f];
|
||||
|
||||
int surrogateCount = 0;
|
||||
// `s` is the index in the `testCase.signs` list.
|
||||
for (int s = 0; s < testCase.signs.length; s++) {
|
||||
for (int s = 0; s < testCase.signs.length - 1; s++) {
|
||||
// `i` is the index in the `text`.
|
||||
final int i = s + surrogateCount;
|
||||
if (s < testCase.chars.length && testCase.chars[s].isSurrogatePair) {
|
||||
surrogateCount++;
|
||||
}
|
||||
|
||||
final Sign sign = testCase.signs[s];
|
||||
final LineBreakResult result = nextLineBreak(text, lastLineBreak);
|
||||
|
||||
if (sign.isBreakOpportunity) {
|
||||
// The line break should've been found at index `i`.
|
||||
expect(
|
||||
result.index,
|
||||
currentFragment.end,
|
||||
i,
|
||||
reason: 'Failed at test case number $t:\n'
|
||||
'$testCase\n'
|
||||
'"$text"\n'
|
||||
'\nExpected line break at {$lastLineBreak - $i} but found line break at {$lastLineBreak - ${result.index}}.',
|
||||
'\nExpected fragment to end at {$i} but ended at {${currentFragment.end}}.',
|
||||
);
|
||||
|
||||
// Since this is a line break, passing a `maxEnd` that's greater
|
||||
// should return the same line break.
|
||||
final LineBreakResult maxEndResult =
|
||||
nextLineBreak(text, lastLineBreak, maxEnd: i + 1);
|
||||
expect(maxEndResult.index, i);
|
||||
expect(maxEndResult.type, isNot(LineBreakType.prohibited));
|
||||
|
||||
lastLineBreak = i;
|
||||
currentFragment = fragments[++f];
|
||||
} else {
|
||||
// This isn't a line break opportunity so the line break should be
|
||||
// somewhere after index `i`.
|
||||
expect(
|
||||
result.index,
|
||||
currentFragment.end,
|
||||
greaterThan(i),
|
||||
reason: 'Failed at test case number $t:\n'
|
||||
'$testCase\n'
|
||||
'"$text"\n'
|
||||
'\nUnexpected line break found at {$lastLineBreak - ${result.index}}.',
|
||||
'\nFragment ended in early at {${currentFragment.end}}.',
|
||||
);
|
||||
}
|
||||
|
||||
// Since this isn't a line break, passing it as a `maxEnd` should
|
||||
// return `maxEnd` as a prohibited line break type.
|
||||
final LineBreakResult maxEndResult =
|
||||
nextLineBreak(text, lastLineBreak, maxEnd: i);
|
||||
expect(maxEndResult.index, i);
|
||||
expect(maxEndResult.type, LineBreakType.prohibited);
|
||||
if (s < testCase.chars.length && testCase.chars[s].isSurrogatePair) {
|
||||
surrogateCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Now let's look at the last sign, which requires different handling.
|
||||
|
||||
// The last line break is an endOfText (or a hard break followed by
|
||||
// endOfText if the last character is a hard line break).
|
||||
if (currentFragment.type == mandatory) {
|
||||
// When last character is a hard line break, there should be an
|
||||
// extra fragment to represent the empty line at the end.
|
||||
expect(
|
||||
fragments,
|
||||
hasLength(f + 2),
|
||||
reason: 'Failed at test case number $t:\n'
|
||||
'$testCase\n'
|
||||
'"$text"\n'
|
||||
"\nExpected an extra fragment for endOfText but there wasn't one.",
|
||||
);
|
||||
|
||||
currentFragment = fragments[++f];
|
||||
}
|
||||
|
||||
expect(
|
||||
currentFragment.type,
|
||||
endOfText,
|
||||
reason: 'Failed at test case number $t:\n'
|
||||
'$testCase\n'
|
||||
'"$text"\n\n'
|
||||
'Expected an endOfText fragment but found: $currentFragment',
|
||||
);
|
||||
expect(
|
||||
currentFragment.end,
|
||||
text.length,
|
||||
reason: 'Failed at test case number $t:\n'
|
||||
'$testCase\n'
|
||||
'"$text"\n\n'
|
||||
'Expected an endOfText fragment ending at {${text.length}} but found: $currentFragment',
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -303,17 +405,32 @@ void testMain() {
|
||||
|
||||
/// Holds information about how a line was split from a string.
|
||||
class Line {
|
||||
Line(this.text, this.breakType);
|
||||
Line(this.text, this.breakType, {this.nl = 0, this.sp = 0});
|
||||
|
||||
factory Line.fromLineBreakFragment(String text, LineBreakFragment fragment) {
|
||||
return Line(
|
||||
text.substring(fragment.start, fragment.end),
|
||||
fragment.type,
|
||||
nl: fragment.trailingNewlines,
|
||||
sp: fragment.trailingSpaces,
|
||||
);
|
||||
}
|
||||
|
||||
final String text;
|
||||
final LineBreakType breakType;
|
||||
final int nl;
|
||||
final int sp;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(text, breakType);
|
||||
int get hashCode => Object.hash(text, breakType, nl, sp);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is Line && other.text == text && other.breakType == breakType;
|
||||
return other is Line &&
|
||||
other.text == text &&
|
||||
other.breakType == breakType &&
|
||||
other.nl == nl &&
|
||||
other.sp == sp;
|
||||
}
|
||||
|
||||
String get escapedText {
|
||||
@ -329,29 +446,20 @@ class Line {
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '"$escapedText" ($breakType)';
|
||||
return '"$escapedText" ($breakType, nl: $nl, sp: $sp)';
|
||||
}
|
||||
}
|
||||
|
||||
List<Line> split(String text) {
|
||||
final List<Line> lines = <Line>[];
|
||||
|
||||
int lastIndex = 0;
|
||||
for (final LineBreakResult brk in findBreaks(text)) {
|
||||
lines.add(Line(text.substring(lastIndex, brk.index), brk.type));
|
||||
lastIndex = brk.index;
|
||||
}
|
||||
return lines;
|
||||
return <Line>[
|
||||
for (final LineBreakFragment fragment in LineBreakFragmenter(text).fragment())
|
||||
Line.fromLineBreakFragment(text, fragment)
|
||||
];
|
||||
}
|
||||
|
||||
List<LineBreakResult> findBreaks(String text) {
|
||||
final List<LineBreakResult> breaks = <LineBreakResult>[];
|
||||
|
||||
LineBreakResult brk = nextLineBreak(text, 0);
|
||||
breaks.add(brk);
|
||||
while (brk.type != LineBreakType.endOfText) {
|
||||
brk = nextLineBreak(text, brk.index);
|
||||
breaks.add(brk);
|
||||
}
|
||||
return breaks;
|
||||
List<Line> splitParagraph(CanvasParagraph paragraph) {
|
||||
return <Line>[
|
||||
for (final LineBreakFragment fragment in LineBreakFragmenter(paragraph.plainText).fragment())
|
||||
Line.fromLineBreakFragment(paragraph.toPlainText(), fragment)
|
||||
];
|
||||
}
|
||||
|
||||
@ -2,125 +2,197 @@
|
||||
// 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 = 'ثنتان';
|
||||
import '../html/paragraph/helper.dart';
|
||||
|
||||
void main() {
|
||||
internalBootstrapBrowserTest(() => testMain);
|
||||
}
|
||||
|
||||
Future<void> testMain() async {
|
||||
group('$getDirectionalBlockEnd', () {
|
||||
group('$BidiFragmenter', () {
|
||||
test('empty string', () {
|
||||
expect(split(''), <_Bidi>[
|
||||
_Bidi('', null, ffPrevious),
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(split('Lorem 11 $rtlWord1 22 ipsum'), <_Bidi>[
|
||||
_Bidi('Lorem', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('11', ltr, ffPrevious),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi(rtlWord1, rtl, ffRtl),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('22', ltr, ffPrevious),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('ipsum', ltr, ffLtr),
|
||||
]);
|
||||
});
|
||||
|
||||
DirectionalPosition blockEnd;
|
||||
test('text and digits', () {
|
||||
expect(split('Lorem11 ${rtlWord1}22 33ipsum44dolor ${rtlWord2}55$rtlWord1'), <_Bidi>[
|
||||
_Bidi('Lorem11', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi(rtlWord1, rtl, ffRtl),
|
||||
_Bidi('22', ltr, ffPrevious),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('33ipsum44dolor', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi(rtlWord2, rtl, ffRtl),
|
||||
_Bidi('55', ltr, ffPrevious),
|
||||
_Bidi(rtlWord1, rtl, ffRtl),
|
||||
]);
|
||||
});
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, start, end);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, loremEnd);
|
||||
test('Mashriqi digits', () {
|
||||
expect(split('foo ١١ ٢٢ bar'), <_Bidi>[
|
||||
_Bidi('foo', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('١١', ltr, ffRtl),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('٢٢', ltr, ffRtl),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('bar', ltr, ffLtr),
|
||||
]);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, start, loremMiddle);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, loremMiddle);
|
||||
expect(split('$rtlWord1 ١١ ٢٢ $rtlWord2'), <_Bidi>[
|
||||
_Bidi(rtlWord1, rtl, ffRtl),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('١١', ltr, ffRtl),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('٢٢', ltr, ffRtl),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi(rtlWord2, rtl, ffRtl),
|
||||
]);
|
||||
});
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, loremMiddle, loremEnd);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, TextDirection.ltr);
|
||||
expect(blockEnd.lineBreak, loremEnd);
|
||||
test('spaces', () {
|
||||
expect(split(' '), <_Bidi>[
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
]);
|
||||
});
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, loremEnd, twelveStart);
|
||||
expect(blockEnd.isSpaceOnly, isTrue);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, twelveStart);
|
||||
test('symbols', () {
|
||||
expect(split('Calculate 2.2 + 4.5 and write the result'), <_Bidi>[
|
||||
_Bidi('Calculate', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('2', ltr, ffPrevious),
|
||||
_Bidi('.', null, ffSandwich),
|
||||
_Bidi('2', ltr, ffPrevious),
|
||||
_Bidi(' + ', null, ffSandwich),
|
||||
_Bidi('4', ltr, ffPrevious),
|
||||
_Bidi('.', null, ffSandwich),
|
||||
_Bidi('5', ltr, ffPrevious),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('and', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('write', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('the', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('result', ltr, ffLtr),
|
||||
]);
|
||||
|
||||
blockEnd = getDirectionalBlockEnd(text, twelveStart, rtl1Start);
|
||||
expect(blockEnd.isSpaceOnly, isFalse);
|
||||
expect(blockEnd.textDirection, isNull);
|
||||
expect(blockEnd.lineBreak, twelveEnd);
|
||||
expect(split('Calculate $rtlWord1 2.2 + 4.5 and write the result'), <_Bidi>[
|
||||
_Bidi('Calculate', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi(rtlWord1, rtl, ffRtl),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('2', ltr, ffPrevious),
|
||||
_Bidi('.', null, ffSandwich),
|
||||
_Bidi('2', ltr, ffPrevious),
|
||||
_Bidi(' + ', null, ffSandwich),
|
||||
_Bidi('4', ltr, ffPrevious),
|
||||
_Bidi('.', null, ffSandwich),
|
||||
_Bidi('5', ltr, ffPrevious),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('and', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('write', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('the', ltr, ffLtr),
|
||||
_Bidi(' ', null, ffSandwich),
|
||||
_Bidi('result', ltr, ffLtr),
|
||||
]);
|
||||
|
||||
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);
|
||||
expect(split('12 + 24 = 36'), <_Bidi>[
|
||||
_Bidi('12', ltr, ffPrevious),
|
||||
_Bidi(' + ', null, ffSandwich),
|
||||
_Bidi('24', ltr, ffPrevious),
|
||||
_Bidi(' = ', null, ffSandwich),
|
||||
_Bidi('36', ltr, ffPrevious),
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
expect(split('Lorem\n12\nipsum \n'), <_Bidi>[
|
||||
_Bidi('Lorem', ltr, ffLtr),
|
||||
_Bidi('\n', null, ffSandwich),
|
||||
_Bidi('12', ltr, ffPrevious),
|
||||
_Bidi('\n', null, ffSandwich),
|
||||
_Bidi('ipsum', ltr, ffLtr),
|
||||
_Bidi(' \n', null, ffSandwich),
|
||||
]);
|
||||
|
||||
DirectionalPosition blockEnd;
|
||||
expect(split('$rtlWord1\n $rtlWord2 \n'), <_Bidi>[
|
||||
_Bidi(rtlWord1, rtl, ffRtl),
|
||||
_Bidi('\n ', null, ffSandwich),
|
||||
_Bidi(rtlWord2, rtl, ffRtl),
|
||||
_Bidi(' \n', null, ffSandwich),
|
||||
]);
|
||||
});
|
||||
|
||||
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);
|
||||
test('surrogates', () {
|
||||
expect(split('A\u{1F600}'), <_Bidi>[
|
||||
_Bidi('A', ltr, ffLtr),
|
||||
_Bidi('\u{1F600}', null, ffSandwich),
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// Holds information about how a bidi region was split from a string.
|
||||
class _Bidi {
|
||||
_Bidi(this.text, this.textDirection, this.fragmentFlow);
|
||||
|
||||
factory _Bidi.fromBidiFragment(String text, BidiFragment bidiFragment) {
|
||||
return _Bidi(
|
||||
text.substring(bidiFragment.start, bidiFragment.end),
|
||||
bidiFragment.textDirection,
|
||||
bidiFragment.fragmentFlow,
|
||||
);
|
||||
}
|
||||
|
||||
final String text;
|
||||
final TextDirection? textDirection;
|
||||
final FragmentFlow fragmentFlow;
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(text, textDirection);
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is _Bidi &&
|
||||
other.text == text &&
|
||||
other.textDirection == textDirection &&
|
||||
other.fragmentFlow == fragmentFlow;
|
||||
}
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return '"$text" ($textDirection | $fragmentFlow)';
|
||||
}
|
||||
}
|
||||
|
||||
List<_Bidi> split(String text) {
|
||||
return <_Bidi>[
|
||||
for (final BidiFragment bidiFragment in BidiFragmenter(text).fragment())
|
||||
_Bidi.fromBidiFragment(text, bidiFragment)
|
||||
];
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user