Unverified Commit c2eaf835 authored by Gary Qian's avatar Gary Qian Committed by GitHub

Reland "Text inline widgets, TextSpan rework (#30069)" with improved backwards...

Reland "Text inline widgets, TextSpan rework (#30069)" with improved backwards compatibility (#34051)
parent 4cc6880c
......@@ -438,8 +438,8 @@ class ItemImageBox extends StatelessWidget {
borderRadius: BorderRadius.circular(2.0),
),
padding: const EdgeInsets.all(4.0),
child: const RichText(
text: TextSpan(
child: RichText(
text: const TextSpan(
style: TextStyle(color: Colors.white),
children: <TextSpan>[
TextSpan(
......
......@@ -145,7 +145,7 @@ class _FuzzerState extends State<Fuzzer> with SingleTickerProviderStateMixin {
return TextSpan(
text: _fiddleWithText(node.text),
style: _fiddleWithStyle(node.style),
children: _fiddleWithChildren(node.children?.map((TextSpan child) => _fiddleWith(child))?.toList() ?? <TextSpan>[]),
children: _fiddleWithChildren(node.children?.map((InlineSpan child) => _fiddleWith(child))?.toList() ?? <InlineSpan>[]),
);
}
......
......@@ -17,7 +17,7 @@
/// painting boxes.
library painting;
export 'dart:ui' show Shadow;
export 'dart:ui' show Shadow, PlaceholderAlignment;
export 'src/painting/alignment.dart';
export 'src/painting/basic_types.dart';
......@@ -46,9 +46,11 @@ export 'src/painting/image_decoder.dart';
export 'src/painting/image_provider.dart';
export 'src/painting/image_resolution.dart';
export 'src/painting/image_stream.dart';
export 'src/painting/inline_span.dart';
export 'src/painting/matrix_utils.dart';
export 'src/painting/notched_shapes.dart';
export 'src/painting/paint_utilities.dart';
export 'src/painting/placeholder_span.dart';
export 'src/painting/rounded_rectangle_border.dart';
export 'src/painting/shader_warm_up.dart';
export 'src/painting/shape_decoration.dart';
......
......@@ -993,6 +993,7 @@ class _DialPainter extends CustomPainter {
final double width = labelPainter.width * _semanticNodeSizeScale;
final double height = labelPainter.height * _semanticNodeSizeScale;
final Offset nodeOffset = getOffsetForTheta(labelTheta, ring) + Offset(-width / 2.0, -height / 2.0);
final TextSpan textSpan = labelPainter.text;
final CustomPainterSemantics node = CustomPainterSemantics(
rect: Rect.fromLTRB(
nodeOffset.dx - 24.0 + width / 2,
......@@ -1003,7 +1004,7 @@ class _DialPainter extends CustomPainter {
properties: SemanticsProperties(
sortKey: OrdinalSortKey(i.toDouble() + ordinalOffset),
selected: label.value == selectedValue,
value: labelPainter.text.text,
value: textSpan?.text,
textDirection: textDirection,
onTap: label.onTap,
),
......
This diff is collapsed.
// Copyright 2015 The Chromium 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:ui' as ui show PlaceholderAlignment;
import 'package:flutter/foundation.dart';
import 'basic_types.dart';
import 'inline_span.dart';
import 'text_painter.dart';
import 'text_span.dart';
import 'text_style.dart';
/// An immutable placeholder that is embedded inline within text.
///
/// [PlaceholderSpan] represents a placeholder that acts as a stand-in for other
/// content. A [PlaceholderSpan] by itself does not contain useful
/// information to change a [TextSpan]. Instead, this class must be extended
/// to define contents.
///
/// [WidgetSpan] from the widgets library extends [PlaceholderSpan] and may be
/// used instead to specify a widget as the contents of the placeholder.
///
/// See also:
///
/// * [WidgetSpan], a leaf node that represents an embedded inline widget.
/// * [TextSpan], a node that represents text in a [TextSpan] tree.
/// * [Text], a widget for showing uniformly-styled text.
/// * [RichText], a widget for finer control of text rendering.
/// * [TextPainter], a class for painting [TextSpan] objects on a [Canvas].
abstract class PlaceholderSpan extends InlineSpan {
/// Creates a [PlaceholderSpan] with the given values.
///
/// A [TextStyle] may be provided with the [style] property, but only the
/// decoration, foreground, background, and spacing options will be used.
const PlaceholderSpan({
this.alignment = ui.PlaceholderAlignment.bottom,
this.baseline,
TextStyle style,
}) : super(style: style,);
/// How the placeholder aligns vertically with the text.
///
/// See [ui.PlaceholderAlignment] for details on each mode.
final ui.PlaceholderAlignment alignment;
/// The [TextBaseline] to align against when using [ui.PlaceholderAlignment.baseline],
/// [ui.PlaceholderAlignment.aboveBaseline], and [ui.PlaceholderAlignment.belowBaseline].
///
/// This is ignored when using other alignment modes.
final TextBaseline baseline;
/// [PlaceholderSpan]s are flattened to a `0xFFFC` object replacement character in the
/// plain text representation when `includePlaceholders` is true.
@override
void computeToPlainText(StringBuffer buffer, {bool includeSemanticsLabels = true, bool includePlaceholders = true}) {
if (includePlaceholders) {
buffer.write('\uFFFC');
}
}
// TODO(garyq): Remove this after next stable release.
/// The [visitTextSpan] method is invalid on [PlaceholderSpan]s
@override
@Deprecated('Use to visitChildren instead')
bool visitTextSpan(bool visitor(TextSpan span)) {
assert(false, 'visitTextSpan is deprecated. Use visitChildren to support InlineSpans');
return false;
}
/// Populates the `semanticsOffsets` and `semanticsElements` with the appropriate data
/// to be able to construct a [SemanticsNode].
///
/// [PlaceholderSpan]s have a text length of 1, which corresponds to the object
/// replacement character (0xFFFC) that is inserted to represent it.
///
/// Null is added to `semanticsElements` for [PlaceholderSpan]s.
@override
void describeSemantics(Accumulator offset, List<int> semanticsOffsets, List<dynamic> semanticsElements) {
semanticsOffsets.add(offset.value);
semanticsOffsets.add(offset.value + 1);
semanticsElements.add(null); // null indicates this is a placeholder.
offset.increment(1);
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<ui.PlaceholderAlignment>('alignment', alignment, defaultValue: null));
properties.add(EnumProperty<TextBaseline>('baseline', baseline, defaultValue: null));
}
}
......@@ -1728,7 +1728,10 @@ abstract class RenderBox extends RenderObject {
return true;
}());
_size = value;
assert(() { debugAssertDoesMeetConstraints(); return true; }());
assert(() {
debugAssertDoesMeetConstraints();
return true;
}());
}
/// Claims ownership of the given [Size].
......
......@@ -284,8 +284,8 @@ mixin DebugOverflowIndicatorMixin on RenderObject {
final List<_OverflowRegionData> overflowRegions = _calculateOverflowRegions(overflow, containerRect);
for (_OverflowRegionData region in overflowRegions) {
context.canvas.drawRect(region.rect.shift(offset), _indicatorPaint);
if (_indicatorLabel[region.side.index].text?.text != region.label) {
final TextSpan textSpan = _indicatorLabel[region.side.index].text;
if (textSpan?.text != region.label) {
_indicatorLabel[region.side.index].text = TextSpan(
text: region.label,
style: _indicatorTextStyle,
......
......@@ -12,6 +12,7 @@ import 'package:flutter/services.dart';
import 'debug.dart';
import 'framework.dart';
import 'localizations.dart';
import 'widget_span.dart';
export 'package:flutter/animation.dart';
export 'package:flutter/foundation.dart' show
......@@ -4913,7 +4914,9 @@ class Flow extends MultiChildRenderObjectWidget {
/// * [TextSpan], which is used to describe the text in a paragraph.
/// * [Text], which automatically applies the ambient styles described by a
/// [DefaultTextStyle] to a single string.
class RichText extends LeafRenderObjectWidget {
/// * [Text.rich], a const text widget that provides similar functionality
/// as [RichText]. [Text.rich] will inherit [TextStyle] from [DefaultTextStyle].
class RichText extends MultiChildRenderObjectWidget {
/// Creates a paragraph of rich text.
///
/// The [text], [textAlign], [softWrap], [overflow], and [textScaleFactor]
......@@ -4924,7 +4927,7 @@ class RichText extends LeafRenderObjectWidget {
///
/// The [textDirection], if null, defaults to the ambient [Directionality],
/// which in that case must not be null.
const RichText({
RichText({
Key key,
@required this.text,
this.textAlign = TextAlign.start,
......@@ -4943,10 +4946,23 @@ class RichText extends LeafRenderObjectWidget {
assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0),
assert(textWidthBasis != null),
super(key: key);
super(key: key, children: _extractChildren(text));
// Traverses the InlineSpan tree and depth-first collects the list of
// child widgets that are created in WidgetSpans.
static List<Widget> _extractChildren(InlineSpan span) {
final List<Widget> result = <Widget>[];
span.visitChildren((InlineSpan span) {
if (span is WidgetSpan) {
result.add(span.child);
}
return true;
});
return result;
}
/// The text to display in this widget.
final TextSpan text;
final InlineSpan text;
/// How the text should be aligned horizontally.
final TextAlign textAlign;
......
......@@ -256,9 +256,16 @@ class Text extends StatelessWidget {
textSpan = null,
super(key: key);
/// Creates a text widget with a [TextSpan].
/// Creates a text widget with a [InlineSpan].
///
/// The following subclasses of [InlineSpan] may be used to build rich text:
///
/// * [TextSpan]s define text and children [InlineSpan]s.
/// * [WidgetSpan]s define embedded inline widgets.
///
/// The [textSpan] parameter must not be null.
///
/// See [RichText] which provides a lower-level way to draw text.
const Text.rich(
this.textSpan, {
Key key,
......@@ -285,10 +292,10 @@ class Text extends StatelessWidget {
/// This will be null if a [textSpan] is provided instead.
final String data;
/// The text to display as a [TextSpan].
/// The text to display as a [InlineSpan].
///
/// This will be null if [data] is provided instead.
final TextSpan textSpan;
final InlineSpan textSpan;
/// If non-null, the style to use for this text.
///
......
......@@ -2721,7 +2721,8 @@ class _InspectorOverlayLayer extends Layer {
) {
canvas.save();
final double maxWidth = size.width - 2 * (_kScreenEdgeMargin + _kTooltipPadding);
if (_textPainter == null || _textPainter.text.text != message || _textPainterMaxWidth != maxWidth) {
final TextSpan textSpan = _textPainter?.text;
if (_textPainter == null || textSpan.text != message || _textPainterMaxWidth != maxWidth) {
_textPainterMaxWidth = maxWidth;
_textPainter = TextPainter()
..maxLines = _kMaxTooltipLines
......
// Copyright 2015 The Chromium 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:ui' as ui show ParagraphBuilder, PlaceholderAlignment;
import 'package:flutter/painting.dart';
import 'framework.dart';
/// An immutable widget that is embedded inline within text.
///
/// The [child] property is the widget that will be embedded. Children are
/// constrained by the width of the paragraph.
///
/// The [child] property may contain its own [Widget] children (if applicable),
/// including [Text] and [RichText] widgets which may include additional
/// [WidgetSpan]s. Child [Text] and [RichText] widgets will be laid out
/// independently and occupy a rectangular space in the parent text layout.
///
/// [WidgetSpan]s will be ignored when passed into a [TextPainter] directly.
/// To properly layout and paint the [child] widget, [WidgetSpan] should be
/// passed into a [Text.rich] widget.
///
/// {@tool sample}
///
/// A card with `Hello World!` embedded inline within a TextSpan tree.
///
/// ```dart
/// Text.rich(
/// TextSpan(
/// children: <InlineSpan>[
/// TextSpan(text: 'Flutter is'),
/// WidgetSpan(
/// child: SizedBox(
/// width: 120,
/// height: 50,
/// child: Card(
/// child: Center(
/// child: Text('Hello World!')
/// )
/// ),
/// )
/// ),
/// TextSpan(text: 'the best!'),
/// ],
/// )
/// )
/// ```
/// {@end-tool}
///
/// [WidgetSpan] contributes the semantics of the [WidgetSpan.child] to the
/// semantics tree.
///
/// See also:
///
/// * [TextSpan], a node that represents text in an [InlineSpan] tree.
/// * [Text], a widget for showing uniformly-styled text.
/// * [RichText], a widget for finer control of text rendering.
/// * [TextPainter], a class for painting [InlineSpan] objects on a [Canvas].
@immutable
class WidgetSpan extends PlaceholderSpan {
/// Creates a [WidgetSpan] with the given values.
///
/// The [child] property must be non-null. [WidgetSpan] is a leaf node in
/// the [InlineSpan] tree. Child widgets are constrained by the width of the
/// paragraph they occupy. Child widget heights are unconstrained, and may
/// cause the text to overflow and be ellipsized/truncated.
///
/// A [TextStyle] may be provided with the [style] property, but only the
/// decoration, foreground, background, and spacing options will be used.
const WidgetSpan({
@required this.child,
ui.PlaceholderAlignment alignment = ui.PlaceholderAlignment.bottom,
TextBaseline baseline,
TextStyle style,
}) : assert(child != null),
assert((identical(alignment, ui.PlaceholderAlignment.aboveBaseline) ||
identical(alignment, ui.PlaceholderAlignment.belowBaseline) ||
identical(alignment, ui.PlaceholderAlignment.baseline)) ? baseline != null : true),
super(
alignment: alignment,
baseline: baseline,
style: style,
);
/// The widget to embed inline within text.
final Widget child;
/// Adds a placeholder box to the paragraph builder if a size has been
/// calculated for the widget.
///
/// Sizes are provided through `dimensions`, which should contain a 1:1
/// in-order mapping of widget to laid-out dimensions. If no such dimension
/// is provided, the widget will be skipped.
///
/// The `textScaleFactor` will be applied to the laid-out size of the widget.
@override
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, @required List<PlaceholderDimensions> dimensions }) {
assert(debugAssertIsValid());
assert(dimensions != null);
final bool hasStyle = style != null;
if (hasStyle) {
builder.pushStyle(style.getTextStyle(textScaleFactor: textScaleFactor));
}
assert(builder.placeholderCount < dimensions.length);
final PlaceholderDimensions currentDimensions = dimensions[builder.placeholderCount];
builder.addPlaceholder(
currentDimensions.size.width,
currentDimensions.size.height,
alignment,
scale: textScaleFactor,
baseline: currentDimensions.baseline,
baselineOffset: currentDimensions.baselineOffset,
);
if (hasStyle) {
builder.pop();
}
}
/// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk.
@override
bool visitChildren(InlineSpanVisitor visitor) {
return visitor(this);
}
@override
InlineSpan getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
return null;
}
@override
int codeUnitAtVisitor(int index, Accumulator offset) {
return null;
}
@override
RenderComparison compareTo(InlineSpan other) {
if (identical(this, other))
return RenderComparison.identical;
if (other.runtimeType != runtimeType)
return RenderComparison.layout;
if ((style == null) != (other.style == null))
return RenderComparison.layout;
final WidgetSpan typedOther = other;
if (child != typedOther.child || alignment != typedOther.alignment) {
return RenderComparison.layout;
}
RenderComparison result = RenderComparison.identical;
if (style != null) {
final RenderComparison candidate = style.compareTo(other.style);
if (candidate.index > result.index)
result = candidate;
if (result == RenderComparison.layout)
return result;
}
return result;
}
@override
bool operator ==(dynamic other) {
if (identical(this, other))
return true;
if (other.runtimeType != runtimeType)
return false;
if (super != other)
return false;
final WidgetSpan typedOther = other;
return typedOther.child == child
&& typedOther.alignment == alignment
&& typedOther.baseline == baseline;
}
@override
int get hashCode => hashValues(super.hashCode, child, alignment, baseline);
/// Returns the text span that contains the given position in the text.
@override
InlineSpan getSpanForPosition(TextPosition position) {
assert(debugAssertIsValid());
return null;
}
/// In debug mode, throws an exception if the object is not in a
/// valid configuration. Otherwise, returns true.
///
/// This is intended to be used as follows:
///
/// ```dart
/// assert(myWidgetSpan.debugAssertIsValid());
/// ```
@override
bool debugAssertIsValid() {
// WidgetSpans are always valid as asserts prevent invalid WidgetSpans
// from being constructed.
return true;
}
}
......@@ -108,4 +108,5 @@ export 'src/widgets/value_listenable_builder.dart';
export 'src/widgets/viewport.dart';
export 'src/widgets/visibility.dart';
export 'src/widgets/widget_inspector.dart';
export 'src/widgets/widget_span.dart';
export 'src/widgets/will_pop_scope.dart';
......@@ -43,7 +43,8 @@ void main() {
// 0 12345678 9 101234567 18 90123456 27
style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
);
expect(painter.text.text.length, 28);
TextSpan textSpan = painter.text;
expect(textSpan.text.length, 28);
painter.layout();
// The skips here are because the old rendering code considers the bidi formatting characters
......@@ -127,7 +128,8 @@ void main() {
);
final List<List<TextBox>> list = <List<TextBox>>[];
for (int index = 0; index < painter.text.text.length; index += 1)
textSpan = painter.text;
for (int index = 0; index < textSpan.text.length; index += 1)
list.add(painter.getBoxesForSelection(TextSelection(baseOffset: index, extentOffset: index + 1)));
expect(list, const <List<TextBox>>[
<TextBox>[], // U+202E, non-printing Unicode bidi formatting character
......@@ -172,7 +174,8 @@ void main() {
// 0 12345678 9 101234567 18 90123456 27
style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
);
expect(painter.text.text.length, 28);
final TextSpan textSpan = painter.text;
expect(textSpan.text.length, 28);
painter.layout();
final TextRange hebrew1 = painter.getWordBoundary(const TextPosition(offset: 4, affinity: TextAffinity.downstream));
......@@ -261,7 +264,8 @@ void main() {
text: 'A\u05D0', // A, Alef
style: TextStyle(fontFamily: 'Ahem', fontSize: 10.0),
);
expect(painter.text.text.length, 2);
final TextSpan textSpan = painter.text;
expect(textSpan.text.length, 2);
painter.layout(maxWidth: 10.0);
for (int index = 0; index <= 2; index += 1) {
......
......@@ -5,6 +5,7 @@
import 'dart:ui' as ui;
import 'package:flutter/painting.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart';
void main() {
......@@ -634,4 +635,98 @@ void main() {
expect(caretOffset.dx, closeTo(0.0, 0.0001));
expect(caretOffset.dy, closeTo(0.0, 0.0001));
});
test('TextPainter widget span', () {
final TextPainter painter = TextPainter()
..textDirection = TextDirection.ltr;
const String text = 'test';
painter.text = const TextSpan(
text: text,
children: <InlineSpan>[
WidgetSpan(child: SizedBox(width: 50, height: 30)),
TextSpan(text: text),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
TextSpan(text: text),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
WidgetSpan(child: SizedBox(width: 50, height: 30)),
]
);
// We provide dimensions for the widgets
painter.setPlaceholderDimensions(const <PlaceholderDimensions>[
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(51, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
PlaceholderDimensions(size: Size(50, 30), baselineOffset: 25, alignment: ui.PlaceholderAlignment.bottom),
]);
painter.layout(maxWidth: 500);
// Now, each of the WidgetSpans will have their own placeholder 'hole'.
Offset caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 1), ui.Rect.zero);
expect(caretOffset.dx, 14);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 4), ui.Rect.zero);
expect(caretOffset.dx, 56);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 5), ui.Rect.zero);
expect(caretOffset.dx, 106);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 6), ui.Rect.zero);
expect(caretOffset.dx, 120);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 10), ui.Rect.zero);
expect(caretOffset.dx, 212);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 11), ui.Rect.zero);
expect(caretOffset.dx, 262);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 12), ui.Rect.zero);
expect(caretOffset.dx, 276);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 13), ui.Rect.zero);
expect(caretOffset.dx, 290);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 14), ui.Rect.zero);
expect(caretOffset.dx, 304);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 15), ui.Rect.zero);
expect(caretOffset.dx, 318);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 16), ui.Rect.zero);
expect(caretOffset.dx, 368);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 17), ui.Rect.zero);
expect(caretOffset.dx, 418);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 18), ui.Rect.zero);
expect(caretOffset.dx, 0);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 19), ui.Rect.zero);
expect(caretOffset.dx, 50);
caretOffset = painter.getOffsetForCaret(const ui.TextPosition(offset: 23), ui.Rect.zero);
expect(caretOffset.dx, 250);
expect(painter.inlinePlaceholderBoxes.length, 14);
expect(painter.inlinePlaceholderBoxes[0], const TextBox.fromLTRBD(56, 0, 106, 30, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[2], const TextBox.fromLTRBD(212, 0, 262, 30, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[3], const TextBox.fromLTRBD(318, 0, 368, 30, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[4], const TextBox.fromLTRBD(368, 0, 418, 30, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[5], const TextBox.fromLTRBD(418, 0, 468, 30, TextDirection.ltr));
// line should break here
expect(painter.inlinePlaceholderBoxes[6], const TextBox.fromLTRBD(0, 30, 50, 60, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[7], const TextBox.fromLTRBD(50, 30, 100, 60, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[10], const TextBox.fromLTRBD(200, 30, 250, 60, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[11], const TextBox.fromLTRBD(250, 30, 300, 60, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[12], const TextBox.fromLTRBD(300, 30, 351, 60, TextDirection.ltr));
expect(painter.inlinePlaceholderBoxes[13], const TextBox.fromLTRBD(351, 30, 401, 60, TextDirection.ltr));
});
}
......@@ -3,17 +3,17 @@
// found in the LICENSE file.
import 'package:flutter/painting.dart';
import 'package:flutter_test/flutter_test.dart' show nonconst;
import 'package:flutter/widgets.dart';
import '../flutter_test_alternative.dart';
void main() {
test('TextSpan equals', () {
final TextSpan a1 = TextSpan(text: nonconst('a'));
final TextSpan a2 = TextSpan(text: nonconst('a'));
final TextSpan b1 = TextSpan(children: <TextSpan>[ a1 ]);
final TextSpan b2 = TextSpan(children: <TextSpan>[ a2 ]);
final TextSpan c1 = TextSpan(text: nonconst(null));
final TextSpan c2 = TextSpan(text: nonconst(null));
const TextSpan a1 = TextSpan(text: 'a');
const TextSpan a2 = TextSpan(text: 'a');
const TextSpan b1 = TextSpan(children: <TextSpan>[ a1 ]);
const TextSpan b2 = TextSpan(children: <TextSpan>[ a2 ]);
const TextSpan c1 = TextSpan(text: null);
const TextSpan c2 = TextSpan(text: null);
expect(a1 == a2, isTrue);
expect(b1 == b2, isTrue);
......@@ -73,6 +73,18 @@ void main() {
expect(textSpan.toPlainText(), 'abc');
});
test('WidgetSpan toPlainText', () {
const TextSpan textSpan = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(child: SizedBox(width: 10, height: 10)),
TextSpan(text: 'c'),
],
);
expect(textSpan.toPlainText(), 'ab\uFFFCc');
});
test('TextSpan toPlainText with semanticsLabel', () {
const TextSpan textSpan = TextSpan(
text: 'a',
......@@ -84,4 +96,117 @@ void main() {
expect(textSpan.toPlainText(), 'afooc');
expect(textSpan.toPlainText(includeSemanticsLabels: false), 'abc');
});
test('TextSpan widget change test', () {
const TextSpan textSpan1 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(child: SizedBox(width: 10, height: 10)),
TextSpan(text: 'c'),
],
);
const TextSpan textSpan2 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(child: SizedBox(width: 10, height: 10)),
TextSpan(text: 'c'),
],
);
const TextSpan textSpan3 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(child: SizedBox(width: 11, height: 10)),
TextSpan(text: 'c'),
],
);
const TextSpan textSpan4 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(child: Text('test')),
TextSpan(text: 'c'),
],
);
const TextSpan textSpan5 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(child: Text('different!')),
TextSpan(text: 'c'),
],
);
const TextSpan textSpan6 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(
child: SizedBox(width: 10, height: 10),
alignment: PlaceholderAlignment.top,
),
TextSpan(text: 'c'),
],
);
expect(textSpan1.compareTo(textSpan3), RenderComparison.layout);
expect(textSpan1.compareTo(textSpan4), RenderComparison.layout);
expect(textSpan1.compareTo(textSpan1), RenderComparison.identical);
expect(textSpan2.compareTo(textSpan2), RenderComparison.identical);
expect(textSpan3.compareTo(textSpan3), RenderComparison.identical);
expect(textSpan2.compareTo(textSpan3), RenderComparison.layout);
expect(textSpan4.compareTo(textSpan5), RenderComparison.layout);
expect(textSpan3.compareTo(textSpan5), RenderComparison.layout);
expect(textSpan2.compareTo(textSpan5), RenderComparison.layout);
expect(textSpan1.compareTo(textSpan5), RenderComparison.layout);
expect(textSpan1.compareTo(textSpan6), RenderComparison.layout);
});
test('TextSpan nested widget change test', () {
const TextSpan textSpan1 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
WidgetSpan(child: SizedBox(width: 10, height: 10)),
TextSpan(text: 'The sky is falling :)')
],
)
),
),
TextSpan(text: 'c'),
],
);
const TextSpan textSpan2 = TextSpan(
text: 'a',
children: <InlineSpan>[
TextSpan(text: 'b'),
WidgetSpan(
child: Text.rich(
TextSpan(
children: <InlineSpan>[
WidgetSpan(child: SizedBox(width: 10, height: 11)),
TextSpan(text: 'The sky is falling :)')
],
)
),
),
TextSpan(text: 'c'),
],
);
expect(textSpan1.compareTo(textSpan2), RenderComparison.layout);
expect(textSpan1.compareTo(textSpan1), RenderComparison.identical);
expect(textSpan2.compareTo(textSpan2), RenderComparison.identical);
});
}
......@@ -5,6 +5,7 @@
import 'dart:ui' as ui show TextBox;
import 'package:flutter/rendering.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
......@@ -324,4 +325,93 @@ void main() {
expect(paragraph.locale, const Locale('ja', 'JP'));
});
test('inline widgets test', () {
const TextSpan text = TextSpan(
text: 'a',
style: TextStyle(fontSize: 10.0),
children: <InlineSpan>[
WidgetSpan(child: SizedBox(width: 21, height: 21)),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
TextSpan(text: 'a'),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
],
);
// Fake the render boxes that correspond to the WidgetSpans. We use
// RenderParagraph to reduce dependencies this test has.
final List<RenderBox> renderBoxes = <RenderBox>[];
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
final RenderParagraph paragraph = RenderParagraph(
text,
textDirection: TextDirection.ltr,
children: renderBoxes,
);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
const TextSelection(baseOffset: 0, extentOffset: 8)
);
expect(boxes.length, equals(5));
expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
expect(boxes[4], const TextBox.fromLTRBD(48.0, 0.0, 62.0, 14.0, TextDirection.ltr));
// Ahem-based tests don't yet quite work on Windows or some MacOS environments
}, skip: isWindows || isMacOS);
test('inline widgets multiline test', () {
const TextSpan text = TextSpan(
text: 'a',
style: TextStyle(fontSize: 10.0),
children: <InlineSpan>[
WidgetSpan(child: SizedBox(width: 21, height: 21)),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
TextSpan(text: 'a'),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
WidgetSpan(child: SizedBox(width: 21, height: 21)),
],
);
// Fake the render boxes that correspond to the WidgetSpans. We use
// RenderParagraph to reduce dependencies this test has.
final List<RenderBox> renderBoxes = <RenderBox>[];
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
renderBoxes.add(RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr));
final RenderParagraph paragraph = RenderParagraph(
text,
textDirection: TextDirection.ltr,
children: renderBoxes,
);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
const TextSelection(baseOffset: 0, extentOffset: 12)
);
expect(boxes.length, equals(9));
expect(boxes[0], const TextBox.fromLTRBD(0.0, 4.0, 10.0, 14.0, TextDirection.ltr));
expect(boxes[1], const TextBox.fromLTRBD(10.0, 0.0, 24.0, 14.0, TextDirection.ltr));
expect(boxes[2], const TextBox.fromLTRBD(24.0, 0.0, 38.0, 14.0, TextDirection.ltr));
expect(boxes[3], const TextBox.fromLTRBD(38.0, 4.0, 48.0, 14.0, TextDirection.ltr));
// Wraps
expect(boxes[4], const TextBox.fromLTRBD(0.0, 14.0, 14.0, 28.0 , TextDirection.ltr));
expect(boxes[5], const TextBox.fromLTRBD(14.0, 14.0, 28.0, 28.0, TextDirection.ltr));
expect(boxes[6], const TextBox.fromLTRBD(28.0, 14.0, 42.0, 28.0, TextDirection.ltr));
// Wraps
expect(boxes[7], const TextBox.fromLTRBD(0.0, 28.0, 14.0, 42.0, TextDirection.ltr));
expect(boxes[8], const TextBox.fromLTRBD(14.0, 28.0, 28.0, 42.0 , TextDirection.ltr));
// Ahem-based tests don't yet quite work on Windows or some MacOS environments
}, skip: isWindows || isMacOS);
}
......@@ -10,6 +10,7 @@ import 'package:flutter/material.dart';
void main() {
testWidgets('BackdropFilter\'s cull rect does not shrink', (WidgetTester tester) async {
tester.binding.addTime(const Duration(seconds: 15));
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
......
......@@ -8,7 +8,6 @@ import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/rendering.dart';
void main() {
group('PhysicalShape', () {
testWidgets('properties', (WidgetTester tester) async {
......
......@@ -1870,8 +1870,9 @@ void main() {
final RenderEditable renderEditable = findRenderEditable(tester);
// The actual text span is split into 3 parts with the middle part underlined.
expect(renderEditable.text.children.length, 3);
expect(renderEditable.text.children[1].text, 'composing');
expect(renderEditable.text.children[1].style.decoration, TextDecoration.underline);
final TextSpan textSpan = renderEditable.text.children[1];
expect(textSpan.text, 'composing');
expect(textSpan.style.decoration, TextDecoration.underline);
focusNode.unfocus();
await tester.pump();
......
......@@ -2,11 +2,11 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/gestures.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import '../rendering/mock_canvas.dart';
import 'semantics_tester.dart';
......@@ -294,6 +294,140 @@ void main() {
semantics.dispose();
}, skip: true); // TODO(jonahwilliams): correct once https://github.com/flutter/flutter/issues/20891 is resolved.
testWidgets('inline widgets generate semantic nodes', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
await tester.pumpWidget(
Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(text: 'a '),
TextSpan(text: 'pebble', recognizer: TapGestureRecognizer()..onTap = () { }),
const TextSpan(text: ' in the '),
WidgetSpan(
child: SizedBox(
width: 20,
height: 40,
child: Card(
child: RichText(
text: const TextSpan(text: 'INTERRUPTION'),
textDirection: TextDirection.rtl,
),
),
),
),
const TextSpan(text: 'sky'),
],
style: textStyle,
),
textDirection: TextDirection.ltr,
),
);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
children: <TestSemantics>[
TestSemantics(
label: 'a ',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'pebble',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
),
TestSemantics(
label: ' in the ',
textDirection: TextDirection.ltr,
),
TestSemantics(
label: 'INTERRUPTION',
textDirection: TextDirection.rtl,
),
TestSemantics(
label: 'sky',
textDirection: TextDirection.ltr,
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true, ignoreRect: true));
semantics.dispose();
});
testWidgets('inline widgets semantic nodes scale', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester);
const TextStyle textStyle = TextStyle(fontFamily: 'Ahem');
await tester.pumpWidget(
Text.rich(
TextSpan(
children: <InlineSpan>[
const TextSpan(text: 'a '),
TextSpan(text: 'pebble', recognizer: TapGestureRecognizer()..onTap = () { }),
const TextSpan(text: ' in the '),
WidgetSpan(
child: SizedBox(
width: 20,
height: 40,
child: Card(
child: RichText(
text: const TextSpan(text: 'INTERRUPTION'),
textDirection: TextDirection.rtl,
),
),
),
),
const TextSpan(text: 'sky'),
],
style: textStyle,
),
textDirection: TextDirection.ltr,
textScaleFactor: 2,
),
);
final TestSemantics expectedSemantics = TestSemantics.root(
children: <TestSemantics>[
TestSemantics.rootChild(
rect: const Rect.fromLTRB(0.0, 0.0, 800.0, 600.0),
children: <TestSemantics>[
TestSemantics(
label: 'a ',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(-4.0, 48.0, 60.0, 84.0),
),
TestSemantics(
label: 'pebble',
textDirection: TextDirection.ltr,
actions: <SemanticsAction>[
SemanticsAction.tap,
],
rect: const Rect.fromLTRB(52.0, 48.0, 228.0, 84.0),
),
TestSemantics(
label: ' in the ',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(220.0, 48.0, 452.0, 84.0),
),
TestSemantics(
label: 'INTERRUPTION',
textDirection: TextDirection.rtl,
rect: const Rect.fromLTRB(448.0, 0.0, 488.0, 80.0),
),
TestSemantics(
label: 'sky',
textDirection: TextDirection.ltr,
rect: const Rect.fromLTRB(484.0, 48.0, 576.0, 84.0),
),
],
),
],
);
expect(semantics, hasSemantics(expectedSemantics, ignoreTransform: true, ignoreId: true,));
semantics.dispose();
});
testWidgets('Overflow is clipping correctly - short text with overflow: clip', (WidgetTester tester) async {
await _pumpTextWidget(
......
......@@ -329,7 +329,10 @@ class TestWidgetInspectorService extends Object with WidgetInspectorService {
}
// State type is private, hence using dynamic.
dynamic getInspectorState() => inspectorKey.currentState;
String paragraphText(RenderParagraph paragraph) => paragraph.text.text;
String paragraphText(RenderParagraph paragraph) {
final TextSpan textSpan = paragraph.text;
return textSpan.text;
}
await tester.pumpWidget(
Directionality(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment