mirror of
https://github.com/flutter/flutter
synced 2024-10-13 11:42:54 +00:00
Reland "Text inline widgets, TextSpan rework" (#33946)
This commit is contained in:
parent
881cdfdeb0
commit
14414f350a
|
@ -1 +1 @@
|
|||
041efaf483a1cd011f4b4f6dcd04e0d5cad9436e
|
||||
eb9c1d66709a1f8a0291076865fe387ceed96dca
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
249
packages/flutter/lib/src/painting/inline_span.dart
Normal file
249
packages/flutter/lib/src/painting/inline_span.dart
Normal file
|
@ -0,0 +1,249 @@
|
|||
// 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;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
import 'basic_types.dart';
|
||||
import 'text_painter.dart';
|
||||
import 'text_style.dart';
|
||||
|
||||
/// Mutable wrapper of an integer that can be passed by reference to track a
|
||||
/// value across a recursive stack.
|
||||
class Accumulator {
|
||||
/// [Accumulator] may be initialized with a specified value, otherwise, it will
|
||||
/// initialize to zero.
|
||||
Accumulator([this._value = 0]);
|
||||
|
||||
/// The integer stored in this [Accumulator].
|
||||
int get value => _value;
|
||||
int _value;
|
||||
|
||||
/// Increases the [value] by the `addend`.
|
||||
void increment(int addend) {
|
||||
assert(addend >= 0);
|
||||
_value += addend;
|
||||
}
|
||||
}
|
||||
/// Called on each span as [InlineSpan.visitChildren] walks the [InlineSpan] tree.
|
||||
///
|
||||
/// Returns true when the walk should continue, and false to stop visiting further
|
||||
/// [InlineSpan]s.
|
||||
typedef InlineSpanVisitor = bool Function(InlineSpan span);
|
||||
|
||||
/// An immutable span of inline content which forms part of a paragraph.
|
||||
///
|
||||
/// * The subclass [TextSpan] specifies text and may contain child [InlineSpan]s.
|
||||
/// * The subclass [PlaceholderSpan] represents a placeholder that may be
|
||||
/// filled with non-text content. [PlaceholderSpan] itself defines a
|
||||
/// [ui.PlaceholderAlignemnt] and a [TextBaseline]. To be useful,
|
||||
/// [PlaceholderSpan] must be extended to define content. An instance of
|
||||
/// this is the [WidgetSpan] class in the widgets library.
|
||||
/// * The subclass [WidgetSpan] specifies embedded inline widgets.
|
||||
///
|
||||
/// {@tool sample}
|
||||
///
|
||||
/// This example shows a tree of [InlineSpan]s that make a query asking for a
|
||||
/// name with a [TextField] embedded inline.
|
||||
///
|
||||
/// ```dart
|
||||
/// Text.rich(
|
||||
/// TextSpan(
|
||||
/// text: 'My name is ',
|
||||
/// style: TextStyle(color: Colors.black),
|
||||
/// children: <InlineSpan>[
|
||||
/// WidgetSpan(
|
||||
/// alignment: PlaceholderAlignment.baseline,
|
||||
/// baseline: TextBaseline.alphabetic,
|
||||
/// child: ConstrainedBox(
|
||||
/// constraints: BoxConstraints(maxWidth: 100),
|
||||
/// child: TextField(),
|
||||
/// )
|
||||
/// ),
|
||||
/// TextSpan(
|
||||
/// text: '.',
|
||||
/// ),
|
||||
/// ],
|
||||
/// ),
|
||||
/// )
|
||||
/// ```
|
||||
/// {@end-tool}
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [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
|
||||
abstract class InlineSpan extends DiagnosticableTree {
|
||||
/// Creates an [InlineSpan] with the given values.
|
||||
const InlineSpan({
|
||||
this.style,
|
||||
});
|
||||
|
||||
/// The [TextStyle] to apply to this span.
|
||||
///
|
||||
/// The [style] is also applied to any child spans when this is an instance
|
||||
/// of [TextSpan].
|
||||
final TextStyle style;
|
||||
|
||||
/// Apply the properties of this object to the given [ParagraphBuilder], from
|
||||
/// which a [Paragraph] can be obtained.
|
||||
///
|
||||
/// The `textScaleFactor` parameter specifies a scale that the text and
|
||||
/// placeholders will be scaled by. The scaling is performed before layout,
|
||||
/// so the text will be laid out with the scaled glyphs and placeholders.
|
||||
///
|
||||
/// The `dimensions` parameter specifies the sizes of the placeholders.
|
||||
/// Each [PlaceholderSpan] must be paired with a [PlaceholderDimensions]
|
||||
/// in the same order as defined in the [InlineSpan] tree.
|
||||
///
|
||||
/// [Paragraph] objects can be drawn on [Canvas] objects.
|
||||
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions });
|
||||
|
||||
/// Walks this [InlineSpan] and any descendants in pre-order and calls `visitor`
|
||||
/// for each span that has content.
|
||||
///
|
||||
/// When `visitor` returns true, the walk will continue. When `visitor` returns
|
||||
/// false, then the walk will end.
|
||||
bool visitChildren(InlineSpanVisitor visitor);
|
||||
|
||||
/// Returns the text span that contains the given position in the text.
|
||||
InlineSpan getSpanForPosition(TextPosition position) {
|
||||
assert(debugAssertIsValid());
|
||||
final Accumulator offset = Accumulator();
|
||||
InlineSpan result;
|
||||
visitChildren((InlineSpan span) {
|
||||
result = span.getSpanForPositionVisitor(position, offset);
|
||||
return result == null;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Performs the check at each [InlineSpan] for if the `position` falls within the range
|
||||
/// of the span and returns the span if it does.
|
||||
///
|
||||
/// The `offset` parameter tracks the current index offset in the text buffer formed
|
||||
/// if the contents of the [InlineSpan] tree were concatenated together starting
|
||||
/// from the root [InlineSpan].
|
||||
///
|
||||
/// This method should not be directly called. Use [getSpanForPosition] instead.
|
||||
@protected
|
||||
InlineSpan getSpanForPositionVisitor(TextPosition position, Accumulator offset);
|
||||
|
||||
/// Flattens the [InlineSpan] tree into a single string.
|
||||
///
|
||||
/// Styles are not honored in this process. If `includeSemanticsLabels` is
|
||||
/// true, then the text returned will include the [TextSpan.semanticsLabel]s
|
||||
/// instead of the text contents for [TextSpan]s.
|
||||
///
|
||||
/// When `includePlaceholders` is true, [PlaceholderSpan]s in the tree will be
|
||||
/// represented as a 0xFFFC 'object replacement character'.
|
||||
String toPlainText({bool includeSemanticsLabels = true, bool includePlaceholders = true}) {
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
computeToPlainText(buffer, includeSemanticsLabels: includeSemanticsLabels, includePlaceholders: includePlaceholders);
|
||||
return buffer.toString();
|
||||
}
|
||||
|
||||
/// Walks the [InlineSpan] tree and writes the plain text representation to `buffer`.
|
||||
///
|
||||
/// This method should not be directly called. Use [toPlainText] instead.
|
||||
///
|
||||
/// Styles are not honored in this process. If `includeSemanticsLabels` is
|
||||
/// true, then the text returned will include the [TextSpan.semanticsLabel]s
|
||||
/// instead of the text contents for [TextSpan]s.
|
||||
///
|
||||
/// When `includePlaceholders` is true, [PlaceholderSpan]s in the tree will be
|
||||
/// represented as a 0xFFFC 'object replacement character'.
|
||||
///
|
||||
/// The plain-text representation of this [InlineSpan] is written into the `buffer`.
|
||||
/// This method will then recursively call [computeToPlainText] on its childen
|
||||
/// [InlineSpan]s if available.
|
||||
@protected
|
||||
void computeToPlainText(StringBuffer buffer, {bool includeSemanticsLabels = true, bool includePlaceholders = true});
|
||||
|
||||
/// Returns the UTF-16 code unit at the given `index` in the flattened string.
|
||||
///
|
||||
/// This only accounts for the [TextSpan.text] values and ignores [PlaceholderSpans].
|
||||
///
|
||||
/// Returns null if the `index` is out of bounds.
|
||||
int codeUnitAt(int index) {
|
||||
if (index < 0)
|
||||
return null;
|
||||
final Accumulator offset = Accumulator();
|
||||
int result;
|
||||
visitChildren((InlineSpan span) {
|
||||
result = span.codeUnitAtVisitor(index, offset);
|
||||
return result == null;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Performs the check at each [InlineSpan] for if the `index` falls within the range
|
||||
/// of the span and returns the corresponding code unit. Returns null otherwise.
|
||||
///
|
||||
/// The `offset` parameter tracks the current index offset in the text buffer formed
|
||||
/// if the contents of the [InlineSpan] tree were concatenated together starting
|
||||
/// from the root [InlineSpan].
|
||||
///
|
||||
/// This method should not be directly called. Use [codeUnitAt] instead.
|
||||
@protected
|
||||
int codeUnitAtVisitor(int index, Accumulator offset);
|
||||
|
||||
/// Populates the `semanticsOffsets` and `semanticsElements` with the appropriate data
|
||||
/// to be able to construct a [SemanticsNode].
|
||||
///
|
||||
/// If applicable, the beginning and end text offset are added to [semanticsOffsets].
|
||||
/// [PlaceholderSpan]s have a text length of 1, which corresponds to the object
|
||||
/// replacement character (0xFFFC) that is inserted to represent it.
|
||||
///
|
||||
/// Any [GestureRecognizer]s are added to `semanticsElements`. Null is added to
|
||||
/// `semanticsElements` for [PlaceholderSpan]s.
|
||||
void describeSemantics(Accumulator offset, List<int> semanticsOffsets, List<dynamic> semanticsElements);
|
||||
|
||||
/// In checked 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(myInlineSpan.debugAssertIsValid());
|
||||
/// ```
|
||||
bool debugAssertIsValid() => true;
|
||||
|
||||
/// Describe the difference between this span and another, in terms of
|
||||
/// how much damage it will make to the rendering. The comparison is deep.
|
||||
///
|
||||
/// Comparing [InlineSpan] objects of different types, for example, comparing
|
||||
/// a [TextSpan] to a [WidgetSpan], always results in [RenderComparison.layout].
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextStyle.compareTo], which does the same thing for [TextStyle]s.
|
||||
RenderComparison compareTo(InlineSpan other);
|
||||
|
||||
@override
|
||||
bool operator ==(dynamic other) {
|
||||
if (identical(this, other))
|
||||
return true;
|
||||
if (other.runtimeType != runtimeType)
|
||||
return false;
|
||||
final InlineSpan typedOther = other;
|
||||
return typedOther.style == style;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => style.hashCode;
|
||||
|
||||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace;
|
||||
|
||||
if (style != null) {
|
||||
style.debugFillProperties(properties);
|
||||
}
|
||||
}
|
||||
}
|
85
packages/flutter/lib/src/painting/placeholder_span.dart
Normal file
85
packages/flutter/lib/src/painting/placeholder_span.dart
Normal file
|
@ -0,0 +1,85 @@
|
|||
// 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');
|
||||
}
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
}
|
|
@ -3,18 +3,82 @@
|
|||
// found in the LICENSE file.
|
||||
|
||||
import 'dart:math' show min, max;
|
||||
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle;
|
||||
import 'dart:ui' as ui show Paragraph, ParagraphBuilder, ParagraphConstraints, ParagraphStyle, PlaceholderAlignment;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'basic_types.dart';
|
||||
import 'inline_span.dart';
|
||||
import 'placeholder_span.dart';
|
||||
import 'strut_style.dart';
|
||||
import 'text_span.dart';
|
||||
|
||||
export 'package:flutter/services.dart' show TextRange, TextSelection;
|
||||
|
||||
/// Holds the [Size] and baseline required to represent the dimensions of
|
||||
/// a placeholder in text.
|
||||
///
|
||||
/// Placeholders specify an empty space in the text layout, which is used
|
||||
/// to later render arbitrary inline widgets into defined by a [WidgetSpan].
|
||||
///
|
||||
/// The [size] and [alignment] properties are required and cannot be null.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WidgetSpan], a subclass of [InlineSpan] and [PlaceholderSpan] that
|
||||
/// represents an inline widget embedded within text. The space this
|
||||
/// widget takes is indicated by a placeholder.
|
||||
/// * [RichText], a text widget that supports text inline widgets.
|
||||
@immutable
|
||||
class PlaceholderDimensions {
|
||||
/// Constructs a [PlaceholderDimensions] with the specified parameters.
|
||||
///
|
||||
/// The `size` and `alignment` are required as a placeholder's dimensions
|
||||
/// require at least `size` and `alignment` to be fully defined.
|
||||
const PlaceholderDimensions({
|
||||
@required this.size,
|
||||
@required this.alignment,
|
||||
this.baseline,
|
||||
this.baselineOffset,
|
||||
}) : assert(size != null),
|
||||
assert(alignment != null);
|
||||
|
||||
/// Width and height dimensions of the placeholder.
|
||||
final Size size;
|
||||
|
||||
/// How to align the placeholder with the text.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [baseline], the baseline to align to when using
|
||||
/// [ui.PlaceholderAlignment.baseline],
|
||||
/// [ui.PlaceholderAlignment.aboveBaseline],
|
||||
/// or [ui.PlaceholderAlignment.underBaseline].
|
||||
/// * [baselineOffset], the distance of the alphabetic baseline from the upper
|
||||
/// edge of the placeholder.
|
||||
final ui.PlaceholderAlignment alignment;
|
||||
|
||||
/// Distance of the [baseline] from the upper edge of the placeholder.
|
||||
///
|
||||
/// Only used when [alignment] is [ui.PlaceholderAlignment.baseline].
|
||||
final double baselineOffset;
|
||||
|
||||
/// The [TextBaseline] to align to. Used with:
|
||||
///
|
||||
/// * [ui.PlaceholderAlignment.baseline]
|
||||
/// * [ui.PlaceholderAlignment.aboveBaseline]
|
||||
/// * [ui.PlaceholderAlignment.underBaseline]
|
||||
/// * [ui.PlaceholderAlignment.middle]
|
||||
final TextBaseline baseline;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'PlaceholderDimensions($size, $baseline)';
|
||||
}
|
||||
}
|
||||
|
||||
/// The different ways of considering the width of one or more lines of text.
|
||||
///
|
||||
/// See [Text.widthType].
|
||||
|
@ -30,6 +94,9 @@ enum TextWidthBasis {
|
|||
longestLine,
|
||||
}
|
||||
|
||||
/// This is used to cache and pass the computed metrics regarding the
|
||||
/// caret's size and position. This is preferred due to the expensive
|
||||
/// nature of the calculation.
|
||||
class _CaretMetrics {
|
||||
const _CaretMetrics({this.offset, this.fullHeight});
|
||||
/// The offset of the top left corner of the caret from the top left
|
||||
|
@ -67,7 +134,7 @@ class TextPainter {
|
|||
///
|
||||
/// The [maxLines] property, if non-null, must be greater than zero.
|
||||
TextPainter({
|
||||
TextSpan text,
|
||||
InlineSpan text,
|
||||
TextAlign textAlign = TextAlign.start,
|
||||
TextDirection textDirection,
|
||||
double textScaleFactor = 1.0,
|
||||
|
@ -99,9 +166,9 @@ class TextPainter {
|
|||
/// After this is set, you must call [layout] before the next call to [paint].
|
||||
///
|
||||
/// This and [textDirection] must be non-null before you call [layout].
|
||||
TextSpan get text => _text;
|
||||
TextSpan _text;
|
||||
set text(TextSpan value) {
|
||||
InlineSpan get text => _text;
|
||||
InlineSpan _text;
|
||||
set text(InlineSpan value) {
|
||||
assert(value == null || value.debugAssertIsValid());
|
||||
if (_text == value)
|
||||
return;
|
||||
|
@ -266,6 +333,53 @@ class TextPainter {
|
|||
|
||||
ui.Paragraph _layoutTemplate;
|
||||
|
||||
/// An ordered list of [TextBox]es that bound the positions of the placeholders
|
||||
/// in the paragraph.
|
||||
///
|
||||
/// Each box corresponds to a [PlaceholderSpan] in the order they were defined
|
||||
/// in the [InlineSpan] tree.
|
||||
List<TextBox> get inlinePlaceholderBoxes => _inlinePlaceholderBoxes;
|
||||
List<TextBox> _inlinePlaceholderBoxes;
|
||||
|
||||
/// An ordered list of scales for each placeholder in the paragraph.
|
||||
///
|
||||
/// The scale is used as a multiplier on the height, width and baselineOffset of
|
||||
/// the placeholder. Scale is primarily used to handle accessibility scaling.
|
||||
///
|
||||
/// Each scale corresponds to a [PlaceholderSpan] in the order they were defined
|
||||
/// in the [InlineSpan] tree.
|
||||
List<double> get inlinePlaceholderScales => _inlinePlaceholderScales;
|
||||
List<double> _inlinePlaceholderScales;
|
||||
|
||||
/// Sets the dimensions of each placeholder in [text].
|
||||
///
|
||||
/// The number of [PlaceholderDimensions] provided should be the same as the
|
||||
/// number of [PlaceholderSpan]s in text. Passing in an empty or null `value`
|
||||
/// will do nothing.
|
||||
///
|
||||
/// If [layout] is attempted without setting the placeholder dimensions, the
|
||||
/// placeholders will be ignored in the text layout and no valid
|
||||
/// [inlinePlaceholderBoxes] will be returned.
|
||||
void setPlaceholderDimensions(List<PlaceholderDimensions> value) {
|
||||
if (value == null || value.isEmpty || listEquals(value, _placeholderDimensions)) {
|
||||
return;
|
||||
}
|
||||
assert(() {
|
||||
int placeholderCount = 0;
|
||||
text.visitChildren((InlineSpan span) {
|
||||
if (span is PlaceholderSpan) {
|
||||
placeholderCount += 1;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return placeholderCount;
|
||||
}() == value.length);
|
||||
_placeholderDimensions = value;
|
||||
_needsLayout = true;
|
||||
_paragraph = null;
|
||||
}
|
||||
List<PlaceholderDimensions> _placeholderDimensions;
|
||||
|
||||
ui.ParagraphStyle _createParagraphStyle([ TextDirection defaultTextDirection ]) {
|
||||
// The defaultTextDirection argument is used for preferredLineHeight in case
|
||||
// textDirection hasn't yet been set.
|
||||
|
@ -419,7 +533,8 @@ class TextPainter {
|
|||
_needsLayout = false;
|
||||
if (_paragraph == null) {
|
||||
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
|
||||
_text.build(builder, textScaleFactor: textScaleFactor);
|
||||
_text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
|
||||
_inlinePlaceholderScales = builder.placeholderScales;
|
||||
_paragraph = builder.build();
|
||||
}
|
||||
_lastMinWidth = minWidth;
|
||||
|
@ -427,9 +542,11 @@ class TextPainter {
|
|||
_paragraph.layout(ui.ParagraphConstraints(width: maxWidth));
|
||||
if (minWidth != maxWidth) {
|
||||
final double newWidth = maxIntrinsicWidth.clamp(minWidth, maxWidth);
|
||||
if (newWidth != width)
|
||||
if (newWidth != width) {
|
||||
_paragraph.layout(ui.ParagraphConstraints(width: newWidth));
|
||||
}
|
||||
}
|
||||
_inlinePlaceholderBoxes = _paragraph.getBoxesForPlaceholders();
|
||||
}
|
||||
|
||||
/// Paints the text onto the given canvas at the given offset.
|
||||
|
@ -491,7 +608,7 @@ class TextPainter {
|
|||
// TODO(garyq): Use actual extended grapheme cluster length instead of
|
||||
// an increasing cluster length amount to achieve deterministic performance.
|
||||
Rect _getRectFromUpstream(int offset, Rect caretPrototype) {
|
||||
final String flattenedText = _text.toPlainText();
|
||||
final String flattenedText = _text.toPlainText(includePlaceholders: false);
|
||||
final int prevCodeUnit = _text.codeUnitAt(max(0, offset - 1));
|
||||
if (prevCodeUnit == null)
|
||||
return null;
|
||||
|
@ -507,10 +624,12 @@ class TextPainter {
|
|||
if (boxes.isEmpty) {
|
||||
// When we are at the beginning of the line, a non-surrogate position will
|
||||
// return empty boxes. We break and try from downstream instead.
|
||||
if (!needsSearch)
|
||||
if (!needsSearch) {
|
||||
break; // Only perform one iteration if no search is required.
|
||||
if (prevRuneOffset < -flattenedText.length)
|
||||
}
|
||||
if (prevRuneOffset < -flattenedText.length) {
|
||||
break; // Stop iterating when beyond the max length of the text.
|
||||
}
|
||||
// Multiply by two to log(n) time cover the entire text span. This allows
|
||||
// faster discovery of very long clusters and reduces the possibility
|
||||
// of certain large clusters taking much longer than others, which can
|
||||
|
@ -538,7 +657,7 @@ class TextPainter {
|
|||
// TODO(garyq): Use actual extended grapheme cluster length instead of
|
||||
// an increasing cluster length amount to achieve deterministic performance.
|
||||
Rect _getRectFromDownstream(int offset, Rect caretPrototype) {
|
||||
final String flattenedText = _text.toPlainText();
|
||||
final String flattenedText = _text.toPlainText(includePlaceholders: false);
|
||||
// We cap the offset at the final index of the _text.
|
||||
final int nextCodeUnit = _text.codeUnitAt(min(offset, flattenedText == null ? 0 : flattenedText.length - 1));
|
||||
if (nextCodeUnit == null)
|
||||
|
@ -554,10 +673,12 @@ class TextPainter {
|
|||
if (boxes.isEmpty) {
|
||||
// When we are at the end of the line, a non-surrogate position will
|
||||
// return empty boxes. We break and try from upstream instead.
|
||||
if (!needsSearch)
|
||||
if (!needsSearch) {
|
||||
break; // Only perform one iteration if no search is required.
|
||||
if (nextRuneOffset >= flattenedText.length << 1)
|
||||
}
|
||||
if (nextRuneOffset >= flattenedText.length << 1) {
|
||||
break; // Stop iterating when beyond the max length of the text.
|
||||
}
|
||||
// Multiply by two to log(n) time cover the entire text span. This allows
|
||||
// faster discovery of very long clusters and reduces the possibility
|
||||
// of certain large clusters taking much longer than others, which can
|
||||
|
|
|
@ -9,6 +9,8 @@ import 'package:flutter/gestures.dart';
|
|||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'basic_types.dart';
|
||||
import 'inline_span.dart';
|
||||
import 'text_painter.dart';
|
||||
import 'text_style.dart';
|
||||
|
||||
/// An immutable span of text.
|
||||
|
@ -21,7 +23,9 @@ import 'text_style.dart';
|
|||
/// only partially) override the [style] of this object. If a
|
||||
/// [TextSpan] has both [text] and [children], then the [text] is
|
||||
/// treated as if it was an unstyled [TextSpan] at the start of the
|
||||
/// [children] list.
|
||||
/// [children] list. Leaving the [TextSpan.text] field null results
|
||||
/// in the [TextSpan] acting as an empty node in the [InlineSpan]
|
||||
/// tree with a list of children.
|
||||
///
|
||||
/// To paint a [TextSpan] on a [Canvas], use a [TextPainter]. To display a text
|
||||
/// span in a widget, use a [RichText]. For text with a single style, consider
|
||||
|
@ -42,27 +46,33 @@ import 'text_style.dart';
|
|||
/// _There is some more detailed sample code in the documentation for the
|
||||
/// [recognizer] property._
|
||||
///
|
||||
/// The [TextSpan.text] will be used as the semantics label unless overriden
|
||||
/// by the [TextSpan.semanticsLabel] property. Any [PlaceholderSpan]s in the
|
||||
/// [TextSpan.children] list will separate the text before and after it into
|
||||
/// two semantics nodes.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [WidgetSpan], a leaf node that represents an embedded inline widget
|
||||
/// in an [InlineSpan] tree. Specify a widget within the [children]
|
||||
/// list by wrapping the widget with a [WidgetSpan]. The widget will be
|
||||
/// laid out inline within the paragraph.
|
||||
/// * [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].
|
||||
@immutable
|
||||
class TextSpan extends DiagnosticableTree {
|
||||
class TextSpan extends InlineSpan {
|
||||
/// Creates a [TextSpan] with the given values.
|
||||
///
|
||||
/// For the object to be useful, at least one of [text] or
|
||||
/// [children] should be set.
|
||||
const TextSpan({
|
||||
this.style,
|
||||
this.text,
|
||||
this.children,
|
||||
TextStyle style,
|
||||
this.recognizer,
|
||||
this.semanticsLabel,
|
||||
});
|
||||
|
||||
/// The style to apply to the [text] and the [children].
|
||||
final TextStyle style;
|
||||
}) : super(style: style,);
|
||||
|
||||
/// The text contained in the span.
|
||||
///
|
||||
|
@ -79,26 +89,26 @@ class TextSpan extends DiagnosticableTree {
|
|||
/// supported and may have unexpected results.
|
||||
///
|
||||
/// The list must not contain any nulls.
|
||||
final List<TextSpan> children;
|
||||
final List<InlineSpan> children;
|
||||
|
||||
/// A gesture recognizer that will receive events that hit this text span.
|
||||
/// A gesture recognizer that will receive events that hit this span.
|
||||
///
|
||||
/// [TextSpan] itself does not implement hit testing or event dispatch. The
|
||||
/// object that manages the [TextSpan] painting is also responsible for
|
||||
/// [InlineSpan] itself does not implement hit testing or event dispatch. The
|
||||
/// object that manages the [InlineSpan] painting is also responsible for
|
||||
/// dispatching events. In the rendering library, that is the
|
||||
/// [RenderParagraph] object, which corresponds to the [RichText] widget in
|
||||
/// the widgets layer; these objects do not bubble events in [TextSpan]s, so a
|
||||
/// the widgets layer; these objects do not bubble events in [InlineSpan]s, so a
|
||||
/// [recognizer] is only effective for events that directly hit the [text] of
|
||||
/// that [TextSpan], not any of its [children].
|
||||
/// that [InlineSpan], not any of its [children].
|
||||
///
|
||||
/// [TextSpan] also does not manage the lifetime of the gesture recognizer.
|
||||
/// [InlineSpan] also does not manage the lifetime of the gesture recognizer.
|
||||
/// The code that owns the [GestureRecognizer] object must call
|
||||
/// [GestureRecognizer.dispose] when the [TextSpan] object is no longer used.
|
||||
/// [GestureRecognizer.dispose] when the [InlineSpan] object is no longer used.
|
||||
///
|
||||
/// {@tool sample}
|
||||
///
|
||||
/// This example shows how to manage the lifetime of a gesture recognizer
|
||||
/// provided to a [TextSpan] object. It defines a `BuzzingText` widget which
|
||||
/// provided to an [InlineSpan] object. It defines a `BuzzingText` widget which
|
||||
/// uses the [HapticFeedback] class to vibrate the device when the user
|
||||
/// long-presses the "find the" span, which is underlined in wavy green. The
|
||||
/// hit-testing is handled by the [RichText] widget.
|
||||
|
@ -131,11 +141,11 @@ class TextSpan extends DiagnosticableTree {
|
|||
///
|
||||
/// @override
|
||||
/// Widget build(BuildContext context) {
|
||||
/// return RichText(
|
||||
/// text: TextSpan(
|
||||
/// return Text.rich(
|
||||
/// TextSpan(
|
||||
/// text: 'Can you ',
|
||||
/// style: TextStyle(color: Colors.black),
|
||||
/// children: <TextSpan>[
|
||||
/// children: <InlineSpan>[
|
||||
/// TextSpan(
|
||||
/// text: 'find the',
|
||||
/// style: TextStyle(
|
||||
|
@ -157,7 +167,7 @@ class TextSpan extends DiagnosticableTree {
|
|||
/// {@end-tool}
|
||||
final GestureRecognizer recognizer;
|
||||
|
||||
/// An alternative semantics label for this text.
|
||||
/// An alternative semantics label for this [TextSpan].
|
||||
///
|
||||
/// If present, the semantics of this span will contain this value instead
|
||||
/// of the actual text.
|
||||
|
@ -177,7 +187,8 @@ class TextSpan extends DiagnosticableTree {
|
|||
/// Rather than using this directly, it's simpler to use the
|
||||
/// [TextPainter] class to paint [TextSpan] objects onto [Canvas]
|
||||
/// objects.
|
||||
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0 }) {
|
||||
@override
|
||||
void build(ui.ParagraphBuilder builder, { double textScaleFactor = 1.0, List<PlaceholderDimensions> dimensions }) {
|
||||
assert(debugAssertIsValid());
|
||||
final bool hasStyle = style != null;
|
||||
if (hasStyle)
|
||||
|
@ -185,9 +196,9 @@ class TextSpan extends DiagnosticableTree {
|
|||
if (text != null)
|
||||
builder.addText(text);
|
||||
if (children != null) {
|
||||
for (TextSpan child in children) {
|
||||
for (InlineSpan child in children) {
|
||||
assert(child != null);
|
||||
child.build(builder, textScaleFactor: textScaleFactor);
|
||||
child.build(builder, textScaleFactor: textScaleFactor, dimensions: dimensions);
|
||||
}
|
||||
}
|
||||
if (hasStyle)
|
||||
|
@ -196,14 +207,15 @@ class TextSpan extends DiagnosticableTree {
|
|||
|
||||
/// Walks this text span and its descendants in pre-order and calls [visitor]
|
||||
/// for each span that has text.
|
||||
bool visitTextSpan(bool visitor(TextSpan span)) {
|
||||
@override
|
||||
bool visitChildren(InlineSpanVisitor visitor) {
|
||||
if (text != null) {
|
||||
if (!visitor(this))
|
||||
return false;
|
||||
}
|
||||
if (children != null) {
|
||||
for (TextSpan child in children) {
|
||||
if (!child.visitTextSpan(visitor))
|
||||
for (InlineSpan child in children) {
|
||||
if (!child.visitChildren(visitor))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -211,63 +223,62 @@ class TextSpan extends DiagnosticableTree {
|
|||
}
|
||||
|
||||
/// Returns the text span that contains the given position in the text.
|
||||
TextSpan getSpanForPosition(TextPosition position) {
|
||||
assert(debugAssertIsValid());
|
||||
@override
|
||||
InlineSpan getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
}
|
||||
final TextAffinity affinity = position.affinity;
|
||||
final int targetOffset = position.offset;
|
||||
int offset = 0;
|
||||
TextSpan result;
|
||||
visitTextSpan((TextSpan span) {
|
||||
assert(result == null);
|
||||
final int endOffset = offset + span.text.length;
|
||||
if (targetOffset == offset && affinity == TextAffinity.downstream ||
|
||||
targetOffset > offset && targetOffset < endOffset ||
|
||||
targetOffset == endOffset && affinity == TextAffinity.upstream) {
|
||||
result = span;
|
||||
return false;
|
||||
}
|
||||
offset = endOffset;
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
final int endOffset = offset.value + text.length;
|
||||
if (offset.value == targetOffset && affinity == TextAffinity.downstream ||
|
||||
offset.value < targetOffset && targetOffset < endOffset ||
|
||||
endOffset == targetOffset && affinity == TextAffinity.upstream) {
|
||||
return this;
|
||||
}
|
||||
offset.increment(text.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Flattens the [TextSpan] tree into a single string.
|
||||
///
|
||||
/// Styles are not honored in this process. If `includeSemanticsLabels` is
|
||||
/// true, then the text returned will include the [semanticsLabel]s instead of
|
||||
/// the text contents when they are present.
|
||||
String toPlainText({bool includeSemanticsLabels = true}) {
|
||||
@override
|
||||
void computeToPlainText(StringBuffer buffer, {bool includeSemanticsLabels = true, bool includePlaceholders = true}) {
|
||||
assert(debugAssertIsValid());
|
||||
final StringBuffer buffer = StringBuffer();
|
||||
visitTextSpan((TextSpan span) {
|
||||
if (span.semanticsLabel != null && includeSemanticsLabels) {
|
||||
buffer.write(span.semanticsLabel);
|
||||
} else {
|
||||
buffer.write(span.text);
|
||||
if (semanticsLabel != null && includeSemanticsLabels) {
|
||||
buffer.write(semanticsLabel);
|
||||
} else if (text != null) {
|
||||
buffer.write(text);
|
||||
}
|
||||
if (children != null) {
|
||||
for (InlineSpan child in children) {
|
||||
child.computeToPlainText(buffer,
|
||||
includeSemanticsLabels: includeSemanticsLabels,
|
||||
includePlaceholders: includePlaceholders,
|
||||
);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
return buffer.toString();
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the UTF-16 code unit at the given index in the flattened string.
|
||||
///
|
||||
/// Returns null if the index is out of bounds.
|
||||
int codeUnitAt(int index) {
|
||||
if (index < 0)
|
||||
@override
|
||||
int codeUnitAtVisitor(int index, Accumulator offset) {
|
||||
if (text == null) {
|
||||
return null;
|
||||
int offset = 0;
|
||||
int result;
|
||||
visitTextSpan((TextSpan span) {
|
||||
if (index - offset < span.text.length) {
|
||||
result = span.text.codeUnitAt(index - offset);
|
||||
return false;
|
||||
}
|
||||
offset += span.text.length;
|
||||
return true;
|
||||
});
|
||||
return result;
|
||||
}
|
||||
if (index - offset.value < text.length) {
|
||||
return text.codeUnitAt(index - offset.value);
|
||||
}
|
||||
offset.increment(text.length);
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
void describeSemantics(Accumulator offset, List<int> semanticsOffsets, List<dynamic> semanticsElements) {
|
||||
if (recognizer != null && (recognizer is TapGestureRecognizer || recognizer is LongPressGestureRecognizer)) {
|
||||
final int length = semanticsLabel?.length ?? text.length;
|
||||
semanticsOffsets.add(offset.value);
|
||||
semanticsOffsets.add(offset.value + length);
|
||||
semanticsElements.add(recognizer);
|
||||
}
|
||||
offset.increment(text != null ? text.length : 0);
|
||||
}
|
||||
|
||||
/// In checked mode, throws an exception if the object is not in a
|
||||
|
@ -278,45 +289,39 @@ class TextSpan extends DiagnosticableTree {
|
|||
/// ```dart
|
||||
/// assert(myTextSpan.debugAssertIsValid());
|
||||
/// ```
|
||||
@override
|
||||
bool debugAssertIsValid() {
|
||||
assert(() {
|
||||
if (!visitTextSpan((TextSpan span) {
|
||||
if (span.children != null) {
|
||||
for (TextSpan child in span.children) {
|
||||
if (child == null)
|
||||
return false;
|
||||
}
|
||||
if (children != null) {
|
||||
for (InlineSpan child in children) {
|
||||
assert(child != null,
|
||||
'TextSpan contains a null child.\n...'
|
||||
'A TextSpan object with a non-null child list should not have any nulls in its child list.\n'
|
||||
'The full text in question was:\n'
|
||||
'${toStringDeep(prefixLineOne: ' ')}'
|
||||
);
|
||||
assert(child.debugAssertIsValid());
|
||||
}
|
||||
return true;
|
||||
})) {
|
||||
throw FlutterError(
|
||||
'TextSpan contains a null child.\n'
|
||||
'A TextSpan object with a non-null child list should not have any nulls in its child list.\n'
|
||||
'The full text in question was:\n'
|
||||
'${toStringDeep(prefixLineOne: ' ')}'
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
return true;
|
||||
return super.debugAssertIsValid();
|
||||
}
|
||||
|
||||
/// Describe the difference between this text span and another, in terms of
|
||||
/// how much damage it will make to the rendering. The comparison is deep.
|
||||
///
|
||||
/// See also:
|
||||
///
|
||||
/// * [TextStyle.compareTo], which does the same thing for [TextStyle]s.
|
||||
RenderComparison compareTo(TextSpan other) {
|
||||
@override
|
||||
RenderComparison compareTo(InlineSpan other) {
|
||||
if (identical(this, other))
|
||||
return RenderComparison.identical;
|
||||
if (other.text != text ||
|
||||
children?.length != other.children?.length ||
|
||||
(style == null) != (other.style == null))
|
||||
if (other.runtimeType != runtimeType)
|
||||
return RenderComparison.layout;
|
||||
RenderComparison result = recognizer == other.recognizer ? RenderComparison.identical : RenderComparison.metadata;
|
||||
final TextSpan textSpan = other;
|
||||
if (textSpan.text != text ||
|
||||
children?.length != textSpan.children?.length ||
|
||||
(style == null) != (textSpan.style == null))
|
||||
return RenderComparison.layout;
|
||||
RenderComparison result = recognizer == textSpan.recognizer ? RenderComparison.identical : RenderComparison.metadata;
|
||||
if (style != null) {
|
||||
final RenderComparison candidate = style.compareTo(other.style);
|
||||
final RenderComparison candidate = style.compareTo(textSpan.style);
|
||||
if (candidate.index > result.index)
|
||||
result = candidate;
|
||||
if (result == RenderComparison.layout)
|
||||
|
@ -324,7 +329,7 @@ class TextSpan extends DiagnosticableTree {
|
|||
}
|
||||
if (children != null) {
|
||||
for (int index = 0; index < children.length; index += 1) {
|
||||
final RenderComparison candidate = children[index].compareTo(other.children[index]);
|
||||
final RenderComparison candidate = children[index].compareTo(textSpan.children[index]);
|
||||
if (candidate.index > result.index)
|
||||
result = candidate;
|
||||
if (result == RenderComparison.layout)
|
||||
|
@ -340,16 +345,17 @@ class TextSpan extends DiagnosticableTree {
|
|||
return true;
|
||||
if (other.runtimeType != runtimeType)
|
||||
return false;
|
||||
if (super != other)
|
||||
return false;
|
||||
final TextSpan typedOther = other;
|
||||
return typedOther.text == text
|
||||
&& typedOther.style == style
|
||||
&& typedOther.recognizer == recognizer
|
||||
&& typedOther.semanticsLabel == semanticsLabel
|
||||
&& listEquals<TextSpan>(typedOther.children, children);
|
||||
&& listEquals<InlineSpan>(typedOther.children, children);
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => hashValues(style, text, recognizer, semanticsLabel, hashList(children));
|
||||
int get hashCode => hashValues(super.hashCode, text, recognizer, semanticsLabel, hashList(children));
|
||||
|
||||
@override
|
||||
String toStringShort() => '$runtimeType';
|
||||
|
@ -357,11 +363,10 @@ class TextSpan extends DiagnosticableTree {
|
|||
@override
|
||||
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
|
||||
super.debugFillProperties(properties);
|
||||
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace;
|
||||
// Properties on style are added as if they were properties directly on
|
||||
// this TextSpan.
|
||||
if (style != null)
|
||||
style.debugFillProperties(properties);
|
||||
|
||||
properties.add(StringProperty('text', text, showName: false, defaultValue: null));
|
||||
if (style == null && text == null && children == null)
|
||||
properties.add(DiagnosticsNode.message('(empty)'));
|
||||
|
||||
properties.add(DiagnosticsProperty<GestureRecognizer>(
|
||||
'recognizer', recognizer,
|
||||
|
@ -369,22 +374,16 @@ class TextSpan extends DiagnosticableTree {
|
|||
defaultValue: null,
|
||||
));
|
||||
|
||||
|
||||
if (semanticsLabel != null) {
|
||||
properties.add(StringProperty('semanticsLabel', semanticsLabel));
|
||||
}
|
||||
|
||||
|
||||
properties.add(StringProperty('text', text, showName: false, defaultValue: null));
|
||||
if (style == null && text == null && children == null)
|
||||
properties.add(DiagnosticsNode.message('(empty)'));
|
||||
}
|
||||
|
||||
@override
|
||||
List<DiagnosticsNode> debugDescribeChildren() {
|
||||
if (children == null)
|
||||
return const <DiagnosticsNode>[];
|
||||
return children.map<DiagnosticsNode>((TextSpan child) {
|
||||
return children.map<DiagnosticsNode>((InlineSpan child) {
|
||||
if (child != null) {
|
||||
return child.toDiagnosticsNode();
|
||||
} else {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// 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 Gradient, Shader, TextBox;
|
||||
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment;
|
||||
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/gestures.dart';
|
||||
|
@ -10,6 +10,7 @@ import 'package:flutter/painting.dart';
|
|||
import 'package:flutter/semantics.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import 'package:vector_math/vector_math_64.dart';
|
||||
|
||||
import 'box.dart';
|
||||
import 'debug.dart';
|
||||
|
@ -35,8 +36,27 @@ enum TextOverflow {
|
|||
|
||||
const String _kEllipsis = '\u2026';
|
||||
|
||||
/// A render object that displays a paragraph of text
|
||||
class RenderParagraph extends RenderBox {
|
||||
/// Parent data for use with [RenderParagraph].
|
||||
class TextParentData extends ContainerBoxParentData<RenderBox> {
|
||||
/// The scaling of the text.
|
||||
double scale;
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
final List<String> values = <String>[];
|
||||
if (offset != null)
|
||||
values.add('offset=$offset');
|
||||
if (scale != null)
|
||||
values.add('scale=$scale');
|
||||
values.add(super.toString());
|
||||
return values.join('; ');
|
||||
}
|
||||
}
|
||||
|
||||
/// A render object that displays a paragraph of text.
|
||||
class RenderParagraph extends RenderBox
|
||||
with ContainerRenderObjectMixin<RenderBox, TextParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, TextParentData> {
|
||||
/// Creates a paragraph render object.
|
||||
///
|
||||
/// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
|
||||
|
@ -44,8 +64,7 @@ class RenderParagraph extends RenderBox {
|
|||
///
|
||||
/// The [maxLines] property may be null (and indeed defaults to null), but if
|
||||
/// it is not null, it must be greater than zero.
|
||||
RenderParagraph(
|
||||
TextSpan text, {
|
||||
RenderParagraph(InlineSpan text, {
|
||||
TextAlign textAlign = TextAlign.start,
|
||||
@required TextDirection textDirection,
|
||||
bool softWrap = true,
|
||||
|
@ -55,6 +74,7 @@ class RenderParagraph extends RenderBox {
|
|||
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
|
||||
Locale locale,
|
||||
StrutStyle strutStyle,
|
||||
List<RenderBox> children,
|
||||
}) : assert(text != null),
|
||||
assert(text.debugAssertIsValid()),
|
||||
assert(textAlign != null),
|
||||
|
@ -76,13 +96,22 @@ class RenderParagraph extends RenderBox {
|
|||
locale: locale,
|
||||
strutStyle: strutStyle,
|
||||
textWidthBasis: textWidthBasis,
|
||||
);
|
||||
) {
|
||||
addAll(children);
|
||||
_extractPlaceholderSpans(text);
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! TextParentData)
|
||||
child.parentData = TextParentData();
|
||||
}
|
||||
|
||||
final TextPainter _textPainter;
|
||||
|
||||
/// The text to display
|
||||
TextSpan get text => _textPainter.text;
|
||||
set text(TextSpan value) {
|
||||
InlineSpan get text => _textPainter.text;
|
||||
set text(InlineSpan value) {
|
||||
assert(value != null);
|
||||
switch (_textPainter.text.compareTo(value)) {
|
||||
case RenderComparison.identical:
|
||||
|
@ -90,17 +119,31 @@ class RenderParagraph extends RenderBox {
|
|||
return;
|
||||
case RenderComparison.paint:
|
||||
_textPainter.text = value;
|
||||
_extractPlaceholderSpans(value);
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
break;
|
||||
case RenderComparison.layout:
|
||||
_textPainter.text = value;
|
||||
_overflowShader = null;
|
||||
_extractPlaceholderSpans(value);
|
||||
markNeedsLayout();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
List<PlaceholderSpan> _placeholderSpans;
|
||||
void _extractPlaceholderSpans(InlineSpan span) {
|
||||
_placeholderSpans = <PlaceholderSpan>[];
|
||||
span.visitChildren((InlineSpan span) {
|
||||
if (span is PlaceholderSpan) {
|
||||
final PlaceholderSpan placeholderSpan = span;
|
||||
_placeholderSpans.add(placeholderSpan);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/// How the text should be aligned horizontally.
|
||||
TextAlign get textAlign => _textPainter.textAlign;
|
||||
set textAlign(TextAlign value) {
|
||||
|
@ -229,28 +272,31 @@ class RenderParagraph extends RenderBox {
|
|||
markNeedsLayout();
|
||||
}
|
||||
|
||||
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
|
||||
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
|
||||
_textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
|
||||
}
|
||||
|
||||
void _layoutTextWithConstraints(BoxConstraints constraints) {
|
||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMinIntrinsicWidth(double height) {
|
||||
_layoutText();
|
||||
if (!_canComputeIntrinsics()) {
|
||||
return 0.0;
|
||||
}
|
||||
_computeChildrenWidthWithMinIntrinsics(height);
|
||||
_layoutText(); // layout with infinite width.
|
||||
return _textPainter.minIntrinsicWidth;
|
||||
}
|
||||
|
||||
@override
|
||||
double computeMaxIntrinsicWidth(double height) {
|
||||
_layoutText();
|
||||
if (!_canComputeIntrinsics()) {
|
||||
return 0.0;
|
||||
}
|
||||
_computeChildrenWidthWithMaxIntrinsics(height);
|
||||
_layoutText(); // layout with infinite width.
|
||||
return _textPainter.maxIntrinsicWidth;
|
||||
}
|
||||
|
||||
double _computeIntrinsicHeight(double width) {
|
||||
if (!_canComputeIntrinsics()) {
|
||||
return 0.0;
|
||||
}
|
||||
_computeChildrenHeightWithMinIntrinsics(width);
|
||||
_layoutText(minWidth: width, maxWidth: width);
|
||||
return _textPainter.height;
|
||||
}
|
||||
|
@ -274,9 +320,114 @@ class RenderParagraph extends RenderBox {
|
|||
return _textPainter.computeDistanceToActualBaseline(baseline);
|
||||
}
|
||||
|
||||
// Intrinsics cannot be calculated without a full layout for
|
||||
// alignments that require the baseline (baseline, aboveBaseline,
|
||||
// belowBaseline).
|
||||
bool _canComputeIntrinsics() {
|
||||
for (PlaceholderSpan span in _placeholderSpans) {
|
||||
switch (span.alignment) {
|
||||
case ui.PlaceholderAlignment.baseline:
|
||||
case ui.PlaceholderAlignment.aboveBaseline:
|
||||
case ui.PlaceholderAlignment.belowBaseline: {
|
||||
assert(RenderObject.debugCheckingIntrinsics,
|
||||
'Intrinsics are not available for PlaceholderAlignment.baseline, '
|
||||
'PlaceholderAlignment.aboveBaseline, or PlaceholderAlignment.belowBaseline,');
|
||||
return false;
|
||||
}
|
||||
case ui.PlaceholderAlignment.top:
|
||||
case ui.PlaceholderAlignment.middle:
|
||||
case ui.PlaceholderAlignment.bottom: {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _computeChildrenWidthWithMaxIntrinsics(double height) {
|
||||
RenderBox child = firstChild;
|
||||
final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
|
||||
int childIndex = 0;
|
||||
while (child != null) {
|
||||
// Height and baseline is irrelevant as all text will be laid
|
||||
// out in a single line.
|
||||
placeholderDimensions[childIndex] = PlaceholderDimensions(
|
||||
size: Size(child.getMaxIntrinsicWidth(height), height),
|
||||
alignment: _placeholderSpans[childIndex].alignment,
|
||||
baseline: _placeholderSpans[childIndex].baseline,
|
||||
);
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
_textPainter.setPlaceholderDimensions(placeholderDimensions);
|
||||
}
|
||||
|
||||
void _computeChildrenWidthWithMinIntrinsics(double height) {
|
||||
RenderBox child = firstChild;
|
||||
final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
|
||||
int childIndex = 0;
|
||||
while (child != null) {
|
||||
final double intrinsicWidth = child.getMinIntrinsicWidth(height);
|
||||
final double intrinsicHeight = child.getMinIntrinsicHeight(intrinsicWidth);
|
||||
placeholderDimensions[childIndex] = PlaceholderDimensions(
|
||||
size: Size(intrinsicWidth, intrinsicHeight),
|
||||
alignment: _placeholderSpans[childIndex].alignment,
|
||||
baseline: _placeholderSpans[childIndex].baseline,
|
||||
);
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
_textPainter.setPlaceholderDimensions(placeholderDimensions);
|
||||
}
|
||||
|
||||
void _computeChildrenHeightWithMinIntrinsics(double width) {
|
||||
RenderBox child = firstChild;
|
||||
final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
|
||||
int childIndex = 0;
|
||||
while (child != null) {
|
||||
final double intrinsicHeight = child.getMinIntrinsicHeight(width);
|
||||
final double intrinsicWidth = child.getMinIntrinsicWidth(intrinsicHeight);
|
||||
placeholderDimensions[childIndex] = PlaceholderDimensions(
|
||||
size: Size(intrinsicWidth, intrinsicHeight),
|
||||
alignment: _placeholderSpans[childIndex].alignment,
|
||||
baseline: _placeholderSpans[childIndex].baseline,
|
||||
);
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
_textPainter.setPlaceholderDimensions(placeholderDimensions);
|
||||
}
|
||||
|
||||
@override
|
||||
bool hitTestSelf(Offset position) => true;
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { Offset position }) {
|
||||
RenderBox child = firstChild;
|
||||
while (child != null) {
|
||||
final TextParentData textParentData = child.parentData;
|
||||
final Matrix4 transform = Matrix4.translationValues(textParentData.offset.dx, textParentData.offset.dy, 0.0)
|
||||
..scale(textParentData.scale, textParentData.scale, textParentData.scale);
|
||||
final bool isHit = result.addWithPaintTransform(
|
||||
transform: transform,
|
||||
position: position,
|
||||
hitTest: (BoxHitTestResult result, Offset transformed) {
|
||||
assert(() {
|
||||
final Offset manualPosition = (position - textParentData.offset) / textParentData.scale;
|
||||
return (transformed.dx - manualPosition.dx).abs() < precisionErrorTolerance
|
||||
&& (transformed.dy - manualPosition.dy).abs() < precisionErrorTolerance;
|
||||
}());
|
||||
return child.hitTest(result, position: transformed);
|
||||
},
|
||||
);
|
||||
if (isHit) {
|
||||
return true;
|
||||
}
|
||||
child = childAfter(child);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
void handleEvent(PointerEvent event, BoxHitTestEntry entry) {
|
||||
assert(debugHandleEvent(event, entry));
|
||||
|
@ -299,9 +450,81 @@ class RenderParagraph extends RenderBox {
|
|||
@visibleForTesting
|
||||
bool get debugHasOverflowShader => _overflowShader != null;
|
||||
|
||||
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
|
||||
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
|
||||
_textPainter.layout(minWidth: minWidth, maxWidth: widthMatters ? maxWidth : double.infinity);
|
||||
}
|
||||
|
||||
void _layoutTextWithConstraints(BoxConstraints constraints) {
|
||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||
}
|
||||
|
||||
// Layout the child inline widgets. We then pass the dimensions of the
|
||||
// children to _textPainter so that appropriate placeholders can be inserted
|
||||
// into the LibTxt layout. This does not do anything if no inline widgets were
|
||||
// specified.
|
||||
void _layoutChildren(BoxConstraints constraints) {
|
||||
if (childCount == 0) {
|
||||
return;
|
||||
}
|
||||
RenderBox child = firstChild;
|
||||
final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>(childCount);
|
||||
int childIndex = 0;
|
||||
while (child != null) {
|
||||
// Only constrain the width to the maximum width of the paragraph.
|
||||
// Leave height unconstrained, which will overflow if expanded past.
|
||||
child.layout(
|
||||
BoxConstraints(
|
||||
maxWidth: constraints.maxWidth,
|
||||
),
|
||||
parentUsesSize: true
|
||||
);
|
||||
double baselineOffset;
|
||||
switch (_placeholderSpans[childIndex].alignment) {
|
||||
case ui.PlaceholderAlignment.baseline: {
|
||||
baselineOffset = child.getDistanceToBaseline(_placeholderSpans[childIndex].baseline);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
baselineOffset = null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
placeholderDimensions[childIndex] = PlaceholderDimensions(
|
||||
size: child.size,
|
||||
alignment: _placeholderSpans[childIndex].alignment,
|
||||
baseline: _placeholderSpans[childIndex].baseline,
|
||||
baselineOffset: baselineOffset,
|
||||
);
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
_textPainter.setPlaceholderDimensions(placeholderDimensions);
|
||||
}
|
||||
|
||||
// Iterate through the laid-out children and set the parentData offsets based
|
||||
// off of the placeholders inserted for each child.
|
||||
void _setParentData() {
|
||||
RenderBox child = firstChild;
|
||||
int childIndex = 0;
|
||||
while (child != null) {
|
||||
final TextParentData textParentData = child.parentData;
|
||||
textParentData.offset = Offset(
|
||||
_textPainter.inlinePlaceholderBoxes[childIndex].left,
|
||||
_textPainter.inlinePlaceholderBoxes[childIndex].top
|
||||
);
|
||||
textParentData.scale = _textPainter.inlinePlaceholderScales[childIndex];
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void performLayout() {
|
||||
_layoutChildren(constraints);
|
||||
_layoutTextWithConstraints(constraints);
|
||||
_setParentData();
|
||||
|
||||
// We grab _textPainter.size and _textPainter.didExceedMaxLines here because
|
||||
// assigning to `size` will trigger us to validate our intrinsic sizes,
|
||||
// which will change _textPainter's layout because the intrinsic size
|
||||
|
@ -386,13 +609,12 @@ class RenderParagraph extends RenderBox {
|
|||
// If you remove this call, make sure that changing the textAlign still
|
||||
// works properly.
|
||||
_layoutTextWithConstraints(constraints);
|
||||
final Canvas canvas = context.canvas;
|
||||
|
||||
assert(() {
|
||||
if (debugRepaintTextRainbowEnabled) {
|
||||
final Paint paint = Paint()
|
||||
..color = debugCurrentRepaintColor.toColor();
|
||||
canvas.drawRect(offset & size, paint);
|
||||
context.canvas.drawRect(offset & size, paint);
|
||||
}
|
||||
return true;
|
||||
}());
|
||||
|
@ -402,22 +624,44 @@ class RenderParagraph extends RenderBox {
|
|||
if (_overflowShader != null) {
|
||||
// This layer limits what the shader below blends with to be just the text
|
||||
// (as opposed to the text and its background).
|
||||
canvas.saveLayer(bounds, Paint());
|
||||
context.canvas.saveLayer(bounds, Paint());
|
||||
} else {
|
||||
canvas.save();
|
||||
context.canvas.save();
|
||||
}
|
||||
canvas.clipRect(bounds);
|
||||
context.canvas.clipRect(bounds);
|
||||
}
|
||||
_textPainter.paint(context.canvas, offset);
|
||||
|
||||
RenderBox child = firstChild;
|
||||
int childIndex = 0;
|
||||
while (child != null) {
|
||||
assert(childIndex < _textPainter.inlinePlaceholderBoxes.length);
|
||||
final TextParentData textParentData = child.parentData;
|
||||
|
||||
final double scale = textParentData.scale;
|
||||
context.pushTransform(
|
||||
needsCompositing,
|
||||
offset + textParentData.offset,
|
||||
Matrix4.diagonal3Values(scale, scale, scale),
|
||||
(PaintingContext context, Offset offset) {
|
||||
context.paintChild(
|
||||
child,
|
||||
offset,
|
||||
);
|
||||
},
|
||||
);
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
_textPainter.paint(canvas, offset);
|
||||
if (_needsClipping) {
|
||||
if (_overflowShader != null) {
|
||||
canvas.translate(offset.dx, offset.dy);
|
||||
context.canvas.translate(offset.dx, offset.dy);
|
||||
final Paint paint = Paint()
|
||||
..blendMode = BlendMode.modulate
|
||||
..shader = _overflowShader;
|
||||
canvas.drawRect(Offset.zero & size, paint);
|
||||
context.canvas.drawRect(Offset.zero & size, paint);
|
||||
}
|
||||
canvas.restore();
|
||||
context.canvas.restore();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -481,26 +725,23 @@ class RenderParagraph extends RenderBox {
|
|||
return _textPainter.size;
|
||||
}
|
||||
|
||||
final List<int> _recognizerOffsets = <int>[];
|
||||
final List<GestureRecognizer> _recognizers = <GestureRecognizer>[];
|
||||
// The offsets for each span that requires custom semantics.
|
||||
final List<int> _inlineSemanticsOffsets = <int>[];
|
||||
// Holds either [GestureRecognizer] or null (for placeholders) to generate
|
||||
// proper semnatics configurations.
|
||||
final List<dynamic> _inlineSemanticsElements = <dynamic>[];
|
||||
|
||||
@override
|
||||
void describeSemanticsConfiguration(SemanticsConfiguration config) {
|
||||
super.describeSemanticsConfiguration(config);
|
||||
_recognizerOffsets.clear();
|
||||
_recognizers.clear();
|
||||
int offset = 0;
|
||||
text.visitTextSpan((TextSpan span) {
|
||||
if (span.recognizer != null && (span.recognizer is TapGestureRecognizer || span.recognizer is LongPressGestureRecognizer)) {
|
||||
final int length = span.semanticsLabel?.length ?? span.text.length;
|
||||
_recognizerOffsets.add(offset);
|
||||
_recognizerOffsets.add(offset + length);
|
||||
_recognizers.add(span.recognizer);
|
||||
}
|
||||
offset += span.text.length;
|
||||
_inlineSemanticsOffsets.clear();
|
||||
_inlineSemanticsElements.clear();
|
||||
final Accumulator offset = Accumulator();
|
||||
text.visitChildren((InlineSpan span) {
|
||||
span.describeSemantics(offset, _inlineSemanticsOffsets, _inlineSemanticsElements);
|
||||
return true;
|
||||
});
|
||||
if (_recognizerOffsets.isNotEmpty) {
|
||||
if (_inlineSemanticsOffsets.isNotEmpty) {
|
||||
config.explicitChildNodes = true;
|
||||
config.isSemanticBoundary = true;
|
||||
} else {
|
||||
|
@ -511,10 +752,9 @@ class RenderParagraph extends RenderBox {
|
|||
|
||||
@override
|
||||
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
|
||||
assert(_recognizerOffsets.isNotEmpty);
|
||||
assert(_recognizerOffsets.length.isEven);
|
||||
assert(_recognizers.isNotEmpty);
|
||||
assert(children.isEmpty);
|
||||
assert(_inlineSemanticsOffsets.isNotEmpty);
|
||||
assert(_inlineSemanticsOffsets.length.isEven);
|
||||
assert(_inlineSemanticsElements.isNotEmpty);
|
||||
final List<SemanticsNode> newChildren = <SemanticsNode>[];
|
||||
final String rawLabel = text.toPlainText();
|
||||
int current = 0;
|
||||
|
@ -522,7 +762,7 @@ class RenderParagraph extends RenderBox {
|
|||
TextDirection currentDirection = textDirection;
|
||||
Rect currentRect;
|
||||
|
||||
SemanticsConfiguration buildSemanticsConfig(int start, int end) {
|
||||
SemanticsConfiguration buildSemanticsConfig(int start, int end, { bool includeText = true }) {
|
||||
final TextDirection initialDirection = currentDirection;
|
||||
final TextSelection selection = TextSelection(baseOffset: start, extentOffset: end);
|
||||
final List<ui.TextBox> rects = getBoxesForSelection(selection);
|
||||
|
@ -542,15 +782,21 @@ class RenderParagraph extends RenderBox {
|
|||
rect.bottom.ceilToDouble() + 4.0,
|
||||
);
|
||||
order += 1;
|
||||
return SemanticsConfiguration()
|
||||
final SemanticsConfiguration configuration = SemanticsConfiguration()
|
||||
..sortKey = OrdinalSortKey(order)
|
||||
..textDirection = initialDirection
|
||||
..label = rawLabel.substring(start, end);
|
||||
..textDirection = initialDirection;
|
||||
if (includeText) {
|
||||
configuration.label = rawLabel.substring(start, end);
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
|
||||
for (int i = 0, j = 0; i < _recognizerOffsets.length; i += 2, j++) {
|
||||
final int start = _recognizerOffsets[i];
|
||||
final int end = _recognizerOffsets[i + 1];
|
||||
int childIndex = 0;
|
||||
RenderBox child = firstChild;
|
||||
for (int i = 0, j = 0; i < _inlineSemanticsOffsets.length; i += 2, j++) {
|
||||
final int start = _inlineSemanticsOffsets[i];
|
||||
final int end = _inlineSemanticsOffsets[i + 1];
|
||||
// Add semantics for any text between the previous recognizer/widget and this one.
|
||||
if (current != start) {
|
||||
final SemanticsNode node = SemanticsNode();
|
||||
final SemanticsConfiguration configuration = buildSemanticsConfig(current, start);
|
||||
|
@ -558,19 +804,38 @@ class RenderParagraph extends RenderBox {
|
|||
node.rect = currentRect;
|
||||
newChildren.add(node);
|
||||
}
|
||||
final SemanticsNode node = SemanticsNode();
|
||||
final SemanticsConfiguration configuration = buildSemanticsConfig(start, end);
|
||||
final GestureRecognizer recognizer = _recognizers[j];
|
||||
if (recognizer is TapGestureRecognizer) {
|
||||
configuration.onTap = recognizer.onTap;
|
||||
} else if (recognizer is LongPressGestureRecognizer) {
|
||||
configuration.onLongPress = recognizer.onLongPress;
|
||||
} else {
|
||||
assert(false);
|
||||
final dynamic inlineElement = _inlineSemanticsElements[j];
|
||||
final SemanticsConfiguration configuration = buildSemanticsConfig(start, end, includeText: false);
|
||||
if (inlineElement != null) {
|
||||
// Add semantics for this recognizer.
|
||||
final SemanticsNode node = SemanticsNode();
|
||||
if (inlineElement is TapGestureRecognizer) {
|
||||
final TapGestureRecognizer recognizer = inlineElement;
|
||||
configuration.onTap = recognizer.onTap;
|
||||
} else if (inlineElement is LongPressGestureRecognizer) {
|
||||
final LongPressGestureRecognizer recognizer = inlineElement;
|
||||
configuration.onLongPress = recognizer.onLongPress;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
node.updateWith(config: configuration);
|
||||
node.rect = currentRect;
|
||||
newChildren.add(node);
|
||||
} else if (childIndex < children.length) {
|
||||
// Add semantics for this placeholder. Semantics are precomputed in the children
|
||||
// argument.
|
||||
final SemanticsNode childNode = children.elementAt(childIndex);
|
||||
final TextParentData parentData = child.parentData;
|
||||
childNode.rect = Rect.fromLTWH(
|
||||
childNode.rect.left,
|
||||
childNode.rect.top,
|
||||
childNode.rect.width * parentData.scale,
|
||||
childNode.rect.height * parentData.scale,
|
||||
);
|
||||
newChildren.add(children.elementAt(childIndex));
|
||||
childIndex += 1;
|
||||
child = childAfter(child);
|
||||
}
|
||||
node.updateWith(config: configuration);
|
||||
node.rect = currentRect;
|
||||
newChildren.add(node);
|
||||
current = end;
|
||||
}
|
||||
if (current < rawLabel.length) {
|
||||
|
|
|
@ -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.
|
||||
///
|
||||
|
|
|
@ -2752,7 +2752,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
|
||||
|
|
198
packages/flutter/lib/src/widgets/widget_span.dart
Normal file
198
packages/flutter/lib/src/widgets/widget_span.dart
Normal file
|
@ -0,0 +1,198 @@
|
|||
// 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();
|
||||
|
|
|
@ -151,16 +151,15 @@ void main() {
|
|||
decoration: const BoxDecoration(
|
||||
color: Colors.green,
|
||||
),
|
||||
child: RichText(
|
||||
textDirection: TextDirection.ltr,
|
||||
text: TextSpan(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'text1 ',
|
||||
style: TextStyle(
|
||||
color: translucentGreen,
|
||||
background: Paint()
|
||||
..color = red.withOpacity(0.5),
|
||||
),
|
||||
children: <TextSpan>[
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: 'text2',
|
||||
style: TextStyle(
|
||||
|
@ -171,6 +170,7 @@ void main() {
|
|||
),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
@ -242,7 +242,7 @@ void main() {
|
|||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.StrutDefault.png'),
|
||||
);
|
||||
}, skip: true); // Should only be on linux (skip: !Platform.isLinux).
|
||||
}, skip: true); // Should only be on linux (skip: !isLinux).
|
||||
// Disabled for now until font inconsistency is resolved.
|
||||
|
||||
testWidgets('Strut text 1', (WidgetTester tester) async {
|
||||
|
@ -270,7 +270,7 @@ void main() {
|
|||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.1.1.png'),
|
||||
);
|
||||
}, skip: true); // Should only be on linux (skip: !Platform.isLinux).
|
||||
}, skip: true); // Should only be on linux (skip: !isLinux).
|
||||
// Disabled for now until font inconsistency is resolved.
|
||||
|
||||
testWidgets('Strut text 2', (WidgetTester tester) async {
|
||||
|
@ -299,7 +299,7 @@ void main() {
|
|||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.2.1.png'),
|
||||
);
|
||||
}, skip: true); // Should only be on linux (skip: !Platform.isLinux).
|
||||
}, skip: true); // Should only be on linux (skip: !isLinux).
|
||||
// Disabled for now until font inconsistency is resolved.
|
||||
|
||||
testWidgets('Strut text rich', (WidgetTester tester) async {
|
||||
|
@ -319,7 +319,7 @@ void main() {
|
|||
color: Colors.red,
|
||||
fontSize: 30,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: 'Second line!\n',
|
||||
style: TextStyle(
|
||||
|
@ -351,7 +351,7 @@ void main() {
|
|||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.3.1.png'),
|
||||
);
|
||||
}, skip: true); // Should only be on linux (skip: !Platform.isLinux).
|
||||
}, skip: true); // Should only be on linux (skip: !isLinux).
|
||||
// Disabled for now until font inconsistency is resolved.
|
||||
|
||||
testWidgets('Strut text font fallback', (WidgetTester tester) async {
|
||||
|
@ -387,7 +387,7 @@ void main() {
|
|||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.Strut.4.1.png'),
|
||||
);
|
||||
}, skip: true); // Should only be on linux (skip: !Platform.isLinux).
|
||||
}, skip: true); // Should only be on linux (skip: !isLinux).
|
||||
// Disabled for now until font inconsistency is resolved.
|
||||
|
||||
testWidgets('Strut text rich forceStrutHeight', (WidgetTester tester) async {
|
||||
|
@ -407,7 +407,7 @@ void main() {
|
|||
color: Colors.red,
|
||||
fontSize: 30,
|
||||
),
|
||||
children: <TextSpan>[
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: 'Second line!\n',
|
||||
style: TextStyle(
|
||||
|
@ -439,7 +439,7 @@ void main() {
|
|||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.StrutForce.1.1.png'),
|
||||
);
|
||||
}, skip: true); // Should only be on linux (skip: !Platform.isLinux).
|
||||
}, skip: true); // Should only be on linux (skip: !isLinux).
|
||||
// Disabled for now until font inconsistency is resolved.
|
||||
|
||||
testWidgets('Decoration thickness', (WidgetTester tester) async {
|
||||
|
@ -518,4 +518,807 @@ void main() {
|
|||
matchesGoldenFile('text_golden.DecorationThickness.1.0.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
testWidgets('Text Inline widget', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'C ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
TextSpan(text: 'He ', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffffff00),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff0000),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'hello world! sieze the day!'),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic
|
||||
),
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Text('embedded'),
|
||||
),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidget.1.1.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
testWidgets('Text Inline widget textfield', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: MaterialApp(
|
||||
home: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'My name is: ',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
child: SizedBox(width: 70, height: 25, child: TextField()),
|
||||
),
|
||||
TextSpan(text: ', and my favorite city is: ', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
child: SizedBox(width: 70, height: 25, child: TextField()),
|
||||
),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidget.2.2.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
// This tests if multiple Text.rich widgets are able to inline nest within each other.
|
||||
testWidgets('Text Inline widget nesting', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: MaterialApp(
|
||||
home: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'outer',
|
||||
style: TextStyle(
|
||||
fontSize: 20,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'inner',
|
||||
style: TextStyle(color: Color(0xff402f4ff)),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
child: Text.rich(
|
||||
TextSpan(
|
||||
text: 'inner2',
|
||||
style: TextStyle(color: Color(0xff003ffff)),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffffff30),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff5f00f0),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff5fff00),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff5f0000),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'outer', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
child: SizedBox(width: 70, height: 25, child: TextField()),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff00ff),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xff0000ff),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetNest.1.2.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
testWidgets('Text Inline widget baseline', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'C ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
TextSpan(text: 'He ', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffffff00),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff0000),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'hello world! sieze the day!'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.baseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Text('embedded'),
|
||||
),
|
||||
TextSpan(text: 'ref'),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetBaseline.1.1.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
testWidgets('Text Inline widget aboveBaseline', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'C ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.aboveBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
TextSpan(text: 'He ', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.aboveBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffffff00),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff0000),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'hello world! sieze the day!'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.aboveBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.aboveBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.aboveBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.aboveBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.aboveBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Text('embedded'),
|
||||
),
|
||||
TextSpan(text: 'ref'),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetAboveBaseline.1.1.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
testWidgets('Text Inline widget belowBaseline', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'C ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.belowBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
TextSpan(text: 'He ', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.belowBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffffff00),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff0000),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'hello world! sieze the day!'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.belowBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.belowBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.belowBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.belowBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.belowBaseline,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Text('embedded'),
|
||||
),
|
||||
TextSpan(text: 'ref'),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetBelowBaseline.1.1.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
testWidgets('Text Inline widget top', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'C ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.top,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
TextSpan(text: 'He ', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.top,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffffff00),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff0000),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'hello world! sieze the day!'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.top,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.top,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.top,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.top,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.top,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Text('embedded'),
|
||||
),
|
||||
TextSpan(text: 'ref'),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetTop.1.1.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
|
||||
testWidgets('Text Inline widget middle', (WidgetTester tester) async {
|
||||
await tester.pumpWidget(
|
||||
Center(
|
||||
child: RepaintBoundary(
|
||||
child: Material(
|
||||
child: Directionality(
|
||||
textDirection: TextDirection.ltr,
|
||||
child: Container(
|
||||
width: 400.0,
|
||||
height: 200.0,
|
||||
decoration: const BoxDecoration(
|
||||
color: Color(0xff00ff00),
|
||||
),
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(minWidth: 0, maxWidth: 200, minHeight: 0, maxHeight: 100),
|
||||
child: const Text.rich(
|
||||
TextSpan(
|
||||
text: 'C ',
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
),
|
||||
children: <InlineSpan>[
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
TextSpan(text: 'He ', style: TextStyle(fontSize: 20)),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 50.0,
|
||||
height: 55.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffffff00),
|
||||
),
|
||||
child: Center(
|
||||
child:SizedBox(
|
||||
width: 10.0,
|
||||
height: 15.0,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
color: Color(0xffff0000),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
)
|
||||
),
|
||||
),
|
||||
TextSpan(text: 'hello world! sieze the day!'),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Checkbox(value: false, onChanged: null),
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: Checkbox(value: true, onChanged: null),
|
||||
)
|
||||
),
|
||||
WidgetSpan(
|
||||
alignment: PlaceholderAlignment.middle,
|
||||
baseline: TextBaseline.alphabetic,
|
||||
child: Text('embedded'),
|
||||
),
|
||||
TextSpan(text: 'ref'),
|
||||
],
|
||||
),
|
||||
textDirection: TextDirection.ltr,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
await expectLater(
|
||||
find.byType(Container),
|
||||
matchesGoldenFile('text_golden.TextInlineWidgetMiddle.1.1.png'),
|
||||
);
|
||||
}, skip: !isLinux); // Coretext uses different thicknesses for decoration
|
||||
}
|
||||
|
|
|
@ -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(
|
||||
|
|
Loading…
Reference in a new issue