blob: 202af72064d102a4ea5ab46251c3abc9bf82e915 [file] [log] [blame]
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'dart:math' as math;
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment, TextHeightBehavior;
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
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';
import 'object.dart';
/// How overflowing text should be handled.
///
/// A [TextOverflow] can be passed to [Text] and [RichText] via their
/// [Text.overflow] and [RichText.overflow] properties respectively.
enum TextOverflow {
/// Clip the overflowing text to fix its container.
clip,
/// Fade the overflowing text to transparent.
fade,
/// Use an ellipsis to indicate that the text has overflowed.
ellipsis,
/// Render overflowing text outside of its container.
visible,
}
const String _kEllipsis = '\u2026';
/// 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) 'offset=$offset',
if (scale != null) 'scale=$scale',
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>,
RelayoutWhenSystemFontsChangeMixin {
/// Creates a paragraph render object.
///
/// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
/// [textScaleFactor] arguments must not be null.
///
/// The [maxLines] property may be null (and indeed defaults to null), but if
/// it is not null, it must be greater than zero.
RenderParagraph(InlineSpan text, {
TextAlign textAlign = TextAlign.start,
@required TextDirection textDirection,
bool softWrap = true,
TextOverflow overflow = TextOverflow.clip,
double textScaleFactor = 1.0,
int maxLines,
Locale locale,
StrutStyle strutStyle,
TextWidthBasis textWidthBasis = TextWidthBasis.parent,
ui.TextHeightBehavior textHeightBehavior,
List<RenderBox> children,
}) : assert(text != null),
assert(text.debugAssertIsValid()),
assert(textAlign != null),
assert(textDirection != null),
assert(softWrap != null),
assert(overflow != null),
assert(textScaleFactor != null),
assert(maxLines == null || maxLines > 0),
assert(textWidthBasis != null),
_softWrap = softWrap,
_overflow = overflow,
_textPainter = TextPainter(
text: text,
textAlign: textAlign,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
locale: locale,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior
) {
addAll(children);
_extractPlaceholderSpans(text);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! TextParentData)
child.parentData = TextParentData();
}
final TextPainter _textPainter;
/// The text to display.
InlineSpan get text => _textPainter.text;
set text(InlineSpan value) {
assert(value != null);
switch (_textPainter.text.compareTo(value)) {
case RenderComparison.identical:
case RenderComparison.metadata:
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) {
assert(value != null);
if (_textPainter.textAlign == value)
return;
_textPainter.textAlign = value;
markNeedsPaint();
}
/// The directionality of the text.
///
/// This decides how the [TextAlign.start], [TextAlign.end], and
/// [TextAlign.justify] values of [textAlign] are interpreted.
///
/// This is also used to disambiguate how to render bidirectional text. For
/// example, if the [text] is an English phrase followed by a Hebrew phrase,
/// in a [TextDirection.ltr] context the English phrase will be on the left
/// and the Hebrew phrase to its right, while in a [TextDirection.rtl]
/// context, the English phrase will be on the right and the Hebrew phrase on
/// its left.
///
/// This must not be null.
TextDirection get textDirection => _textPainter.textDirection;
set textDirection(TextDirection value) {
assert(value != null);
if (_textPainter.textDirection == value)
return;
_textPainter.textDirection = value;
markNeedsLayout();
}
/// Whether the text should break at soft line breaks.
///
/// If false, the glyphs in the text will be positioned as if there was
/// unlimited horizontal space.
///
/// If [softWrap] is false, [overflow] and [textAlign] may have unexpected
/// effects.
bool get softWrap => _softWrap;
bool _softWrap;
set softWrap(bool value) {
assert(value != null);
if (_softWrap == value)
return;
_softWrap = value;
markNeedsLayout();
}
/// How visual overflow should be handled.
TextOverflow get overflow => _overflow;
TextOverflow _overflow;
set overflow(TextOverflow value) {
assert(value != null);
if (_overflow == value)
return;
_overflow = value;
_textPainter.ellipsis = value == TextOverflow.ellipsis ? _kEllipsis : null;
markNeedsLayout();
}
/// The number of font pixels for each logical pixel.
///
/// For example, if the text scale factor is 1.5, text will be 50% larger than
/// the specified font size.
double get textScaleFactor => _textPainter.textScaleFactor;
set textScaleFactor(double value) {
assert(value != null);
if (_textPainter.textScaleFactor == value)
return;
_textPainter.textScaleFactor = value;
_overflowShader = null;
markNeedsLayout();
}
/// An optional maximum number of lines for the text to span, wrapping if
/// necessary. If the text exceeds the given number of lines, it will be
/// truncated according to [overflow] and [softWrap].
int get maxLines => _textPainter.maxLines;
/// The value may be null. If it is not null, then it must be greater than
/// zero.
set maxLines(int value) {
assert(value == null || value > 0);
if (_textPainter.maxLines == value)
return;
_textPainter.maxLines = value;
_overflowShader = null;
markNeedsLayout();
}
/// Used by this paragraph's internal [TextPainter] to select a
/// locale-specific font.
///
/// In some cases the same Unicode character may be rendered differently
/// depending
/// on the locale. For example the '骨' character is rendered differently in
/// the Chinese and Japanese locales. In these cases the [locale] may be used
/// to select a locale-specific font.
Locale get locale => _textPainter.locale;
/// The value may be null.
set locale(Locale value) {
if (_textPainter.locale == value)
return;
_textPainter.locale = value;
_overflowShader = null;
markNeedsLayout();
}
/// {@macro flutter.painting.textPainter.strutStyle}
StrutStyle get strutStyle => _textPainter.strutStyle;
/// The value may be null.
set strutStyle(StrutStyle value) {
if (_textPainter.strutStyle == value)
return;
_textPainter.strutStyle = value;
_overflowShader = null;
markNeedsLayout();
}
/// {@macro flutter.widgets.basic.TextWidthBasis}
TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis;
set textWidthBasis(TextWidthBasis value) {
assert(value != null);
if (_textPainter.textWidthBasis == value)
return;
_textPainter.textWidthBasis = value;
_overflowShader = null;
markNeedsLayout();
}
/// {@macro flutter.dart:ui.textHeightBehavior}
ui.TextHeightBehavior get textHeightBehavior => _textPainter.textHeightBehavior;
set textHeightBehavior(ui.TextHeightBehavior value) {
if (_textPainter.textHeightBehavior == value)
return;
_textPainter.textHeightBehavior = value;
_overflowShader = null;
markNeedsLayout();
}
@override
double computeMinIntrinsicWidth(double height) {
if (!_canComputeIntrinsics()) {
return 0.0;
}
_computeChildrenWidthWithMinIntrinsics(height);
_layoutText(); // layout with infinite width.
return _textPainter.minIntrinsicWidth;
}
@override
double computeMaxIntrinsicWidth(double height) {
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;
}
@override
double computeMinIntrinsicHeight(double width) {
return _computeIntrinsicHeight(width);
}
@override
double computeMaxIntrinsicHeight(double width) {
return _computeIntrinsicHeight(width);
}
@override
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(!debugNeedsLayout);
assert(constraints != null);
assert(constraints.debugAssertIsValid());
_layoutTextWithConstraints(constraints);
// TODO(garyq): Since our metric for ideographic baseline is currently
// inaccurate and the non-alphabetic baselines are based off of the
// alphabetic baseline, we use the alphabetic for now to produce correct
// layouts. We should eventually change this back to pass the `baseline`
// property when the ideographic baseline is properly implemented
// (https://github.com/flutter/flutter/issues/22625).
return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
}
// Intrinsics cannot be calculated without a full layout for
// alignments that require the baseline (baseline, aboveBaseline,
// belowBaseline).
bool _canComputeIntrinsics() {
for (final 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 as TextParentData;
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));
if (event is! PointerDownEvent)
return;
_layoutTextWithConstraints(constraints);
final Offset offset = entry.localPosition;
final TextPosition position = _textPainter.getPositionForOffset(offset);
final InlineSpan span = _textPainter.text.getSpanForPosition(position);
if (span == null) {
return;
}
if (span is TextSpan) {
final TextSpan textSpan = span;
textSpan.recognizer?.addPointer(event as PointerDownEvent);
}
}
bool _needsClipping = false;
ui.Shader _overflowShader;
/// Whether this paragraph currently has a [dart:ui.Shader] for its overflow
/// effect.
///
/// Used to test this object. Not for use in production.
@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,
);
}
@override
void systemFontsDidChange() {
super.systemFontsDidChange();
_textPainter.markNeedsLayout();
}
// Placeholder dimensions representing the sizes of child inline widgets.
//
// These need to be cached because the text painter's placeholder dimensions
// will be overwritten during intrinsic width/height calculations and must be
// restored to the original values before final layout and painting.
List<PlaceholderDimensions> _placeholderDimensions;
void _layoutTextWithConstraints(BoxConstraints constraints) {
_textPainter.setPlaceholderDimensions(_placeholderDimensions);
_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;
_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;
}
}
// 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 && childIndex < _textPainter.inlinePlaceholderBoxes.length) {
final TextParentData textParentData = child.parentData as TextParentData;
textParentData.offset = Offset(
_textPainter.inlinePlaceholderBoxes[childIndex].left,
_textPainter.inlinePlaceholderBoxes[childIndex].top,
);
textParentData.scale = _textPainter.inlinePlaceholderScales[childIndex];
child = childAfter(child);
childIndex += 1;
}
}
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
_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
// calculations are destructive. Other _textPainter state will also be
// affected. See also RenderEditable which has a similar issue.
final Size textSize = _textPainter.size;
final bool textDidExceedMaxLines = _textPainter.didExceedMaxLines;
size = constraints.constrain(textSize);
final bool didOverflowHeight = size.height < textSize.height || textDidExceedMaxLines;
final bool didOverflowWidth = size.width < textSize.width;
// TODO(abarth): We're only measuring the sizes of the line boxes here. If
// the glyphs draw outside the line boxes, we might think that there isn't
// visual overflow when there actually is visual overflow. This can become
// a problem if we start having horizontal overflow and introduce a clip
// that affects the actual (but undetected) vertical overflow.
final bool hasVisualOverflow = didOverflowWidth || didOverflowHeight;
if (hasVisualOverflow) {
switch (_overflow) {
case TextOverflow.visible:
_needsClipping = false;
_overflowShader = null;
break;
case TextOverflow.clip:
case TextOverflow.ellipsis:
_needsClipping = true;
_overflowShader = null;
break;
case TextOverflow.fade:
assert(textDirection != null);
_needsClipping = true;
final TextPainter fadeSizePainter = TextPainter(
text: TextSpan(style: _textPainter.text.style, text: '\u2026'),
textDirection: textDirection,
textScaleFactor: textScaleFactor,
locale: locale,
)..layout();
if (didOverflowWidth) {
double fadeEnd, fadeStart;
switch (textDirection) {
case TextDirection.rtl:
fadeEnd = 0.0;
fadeStart = fadeSizePainter.width;
break;
case TextDirection.ltr:
fadeEnd = size.width;
fadeStart = fadeEnd - fadeSizePainter.width;
break;
}
_overflowShader = ui.Gradient.linear(
Offset(fadeStart, 0.0),
Offset(fadeEnd, 0.0),
<Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
);
} else {
final double fadeEnd = size.height;
final double fadeStart = fadeEnd - fadeSizePainter.height / 2.0;
_overflowShader = ui.Gradient.linear(
Offset(0.0, fadeStart),
Offset(0.0, fadeEnd),
<Color>[const Color(0xFFFFFFFF), const Color(0x00FFFFFF)],
);
}
break;
}
} else {
_needsClipping = false;
_overflowShader = null;
}
}
@override
void paint(PaintingContext context, Offset offset) {
// Ideally we could compute the min/max intrinsic width/height with a
// non-destructive operation. However, currently, computing these values
// will destroy state inside the painter. If that happens, we need to get
// back the correct state by calling _layout again.
//
// TODO(abarth): Make computing the min/max intrinsic width/height a
// non-destructive operation.
//
// If you remove this call, make sure that changing the textAlign still
// works properly.
_layoutTextWithConstraints(constraints);
assert(() {
if (debugRepaintTextRainbowEnabled) {
final Paint paint = Paint()
..color = debugCurrentRepaintColor.toColor();
context.canvas.drawRect(offset & size, paint);
}
return true;
}());
if (_needsClipping) {
final Rect bounds = offset & size;
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).
context.canvas.saveLayer(bounds, Paint());
} else {
context.canvas.save();
}
context.canvas.clipRect(bounds);
}
_textPainter.paint(context.canvas, offset);
RenderBox child = firstChild;
int childIndex = 0;
// childIndex might be out of index of placeholder boxes. This can happen
// if engine truncates children due to ellipsis. Sadly, we would not know
// it until we finish layout, and RenderObject is in immutable state at
// this point.
while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes.length) {
final TextParentData textParentData = child.parentData as TextParentData;
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;
}
if (_needsClipping) {
if (_overflowShader != null) {
context.canvas.translate(offset.dx, offset.dy);
final Paint paint = Paint()
..blendMode = BlendMode.modulate
..shader = _overflowShader;
context.canvas.drawRect(Offset.zero & size, paint);
}
context.canvas.restore();
}
}
/// Returns the offset at which to paint the caret.
///
/// Valid only after [layout].
Offset getOffsetForCaret(TextPosition position, Rect caretPrototype) {
assert(!debugNeedsLayout);
_layoutTextWithConstraints(constraints);
return _textPainter.getOffsetForCaret(position, caretPrototype);
}
/// Returns a list of rects that bound the given selection.
///
/// A given selection might have more than one rect if this text painter
/// contains bidirectional text because logically contiguous text might not be
/// visually contiguous.
///
/// Valid only after [layout].
List<ui.TextBox> getBoxesForSelection(TextSelection selection) {
assert(!debugNeedsLayout);
_layoutTextWithConstraints(constraints);
return _textPainter.getBoxesForSelection(selection);
}
/// Returns the position within the text for the given pixel offset.
///
/// Valid only after [layout].
TextPosition getPositionForOffset(Offset offset) {
assert(!debugNeedsLayout);
_layoutTextWithConstraints(constraints);
return _textPainter.getPositionForOffset(offset);
}
/// Returns the text range of the word at the given offset. Characters not
/// part of a word, such as spaces, symbols, and punctuation, have word breaks
/// on both sides. In such cases, this method will return a text range that
/// contains the given text position.
///
/// Word boundaries are defined more precisely in Unicode Standard Annex #29
/// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
///
/// Valid only after [layout].
TextRange getWordBoundary(TextPosition position) {
assert(!debugNeedsLayout);
_layoutTextWithConstraints(constraints);
return _textPainter.getWordBoundary(position);
}
/// Returns the size of the text as laid out.
///
/// This can differ from [size] if the text overflowed or if the [constraints]
/// provided by the parent [RenderObject] forced the layout to be bigger than
/// necessary for the given [text].
///
/// This returns the [TextPainter.size] of the underlying [TextPainter].
///
/// Valid only after [layout].
Size get textSize {
assert(!debugNeedsLayout);
return _textPainter.size;
}
/// Collected during [describeSemanticsConfiguration], used by
/// [assembleSemanticsNode] and [_combineSemanticsInfo].
List<InlineSpanSemanticsInformation> _semanticsInfo;
/// Combines _semanticsInfo entries where permissible, determined by
/// [InlineSpanSemanticsInformation.requiresOwnNode].
List<InlineSpanSemanticsInformation> _combineSemanticsInfo() {
assert(_semanticsInfo != null);
final List<InlineSpanSemanticsInformation> combined = <InlineSpanSemanticsInformation>[];
String workingText = '';
String workingLabel;
for (final InlineSpanSemanticsInformation info in _semanticsInfo) {
if (info.requiresOwnNode) {
if (workingText != null) {
combined.add(InlineSpanSemanticsInformation(
workingText,
semanticsLabel: workingLabel ?? workingText,
));
workingText = '';
workingLabel = null;
}
combined.add(info);
} else {
workingText += info.text;
workingLabel ??= '';
if (info.semanticsLabel != null) {
workingLabel += info.semanticsLabel;
} else {
workingLabel += info.text;
}
}
}
if (workingText != null) {
combined.add(InlineSpanSemanticsInformation(
workingText,
semanticsLabel: workingLabel,
));
} else {
assert(workingLabel != null);
}
return combined;
}
@override
void describeSemanticsConfiguration(SemanticsConfiguration config) {
super.describeSemanticsConfiguration(config);
_semanticsInfo = text.getSemanticsInformation();
if (_semanticsInfo.any((InlineSpanSemanticsInformation info) => info.recognizer != null)) {
config.explicitChildNodes = true;
config.isSemanticBoundary = true;
} else {
final StringBuffer buffer = StringBuffer();
for (final InlineSpanSemanticsInformation info in _semanticsInfo) {
buffer.write(info.semanticsLabel ?? info.text);
}
config.label = buffer.toString();
config.textDirection = textDirection;
}
}
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
// can be re-used when [assembleSemanticsNode] is called again. This ensures
// stable ids for the [SemanticsNode]s of [TextSpan]s across
// [assembleSemanticsNode] invocations.
Queue<SemanticsNode> _cachedChildNodes;
@override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
assert(_semanticsInfo != null && _semanticsInfo.isNotEmpty);
final List<SemanticsNode> newChildren = <SemanticsNode>[];
TextDirection currentDirection = textDirection;
Rect currentRect;
double ordinal = 0.0;
int start = 0;
int placeholderIndex = 0;
RenderBox child = firstChild;
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
for (final InlineSpanSemanticsInformation info in _combineSemanticsInfo()) {
final TextDirection initialDirection = currentDirection;
final TextSelection selection = TextSelection(
baseOffset: start,
extentOffset: start + info.text.length,
);
final List<ui.TextBox> rects = getBoxesForSelection(selection);
if (rects.isEmpty) {
continue;
}
Rect rect = rects.first.toRect();
currentDirection = rects.first.direction;
for (final ui.TextBox textBox in rects.skip(1)) {
rect = rect.expandToInclude(textBox.toRect());
currentDirection = textBox.direction;
}
// Any of the text boxes may have had infinite dimensions.
// We shouldn't pass infinite dimensions up to the bridges.
rect = Rect.fromLTWH(
math.max(0.0, rect.left),
math.max(0.0, rect.top),
math.min(rect.width, constraints.maxWidth),
math.min(rect.height, constraints.maxHeight),
);
// round the current rectangle to make this API testable and add some
// padding so that the accessibility rects do not overlap with the text.
currentRect = Rect.fromLTRB(
rect.left.floorToDouble() - 4.0,
rect.top.floorToDouble() - 4.0,
rect.right.ceilToDouble() + 4.0,
rect.bottom.ceilToDouble() + 4.0,
);
if (info.isPlaceholder) {
final SemanticsNode childNode = children.elementAt(placeholderIndex++);
final TextParentData parentData = child.parentData as TextParentData;
childNode.rect = Rect.fromLTWH(
childNode.rect.left,
childNode.rect.top,
childNode.rect.width * parentData.scale,
childNode.rect.height * parentData.scale,
);
newChildren.add(childNode);
child = childAfter(child);
} else {
final SemanticsConfiguration configuration = SemanticsConfiguration()
..sortKey = OrdinalSortKey(ordinal++)
..textDirection = initialDirection
..label = info.semanticsLabel ?? info.text;
final GestureRecognizer recognizer = info.recognizer;
if (recognizer != null) {
if (recognizer is TapGestureRecognizer) {
configuration.onTap = recognizer.onTap;
configuration.isLink = true;
} else if (recognizer is LongPressGestureRecognizer) {
configuration.onLongPress = recognizer.onLongPress;
} else {
assert(false);
}
}
final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
? _cachedChildNodes.removeFirst()
: SemanticsNode();
newChild
..updateWith(config: configuration)
..rect = currentRect;
newChildCache.addLast(newChild);
newChildren.add(newChild);
}
start += info.text.length;
}
_cachedChildNodes = newChildCache;
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
}
@override
void clearSemantics() {
super.clearSemantics();
_cachedChildNodes = null;
}
@override
List<DiagnosticsNode> debugDescribeChildren() {
return <DiagnosticsNode>[
text.toDiagnosticsNode(
name: 'text',
style: DiagnosticsTreeStyle.transition,
)
];
}
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(EnumProperty<TextAlign>('textAlign', textAlign));
properties.add(EnumProperty<TextDirection>('textDirection', textDirection));
properties.add(
FlagProperty(
'softWrap',
value: softWrap,
ifTrue: 'wrapping at box width',
ifFalse: 'no wrapping except at line break characters',
showName: true,
)
);
properties.add(EnumProperty<TextOverflow>('overflow', overflow));
properties.add(
DoubleProperty(
'textScaleFactor',
textScaleFactor,
defaultValue: 1.0,
)
);
properties.add(
DiagnosticsProperty<Locale>(
'locale',
locale,
defaultValue: null,
)
);
properties.add(IntProperty('maxLines', maxLines, ifNull: 'unlimited'));
}
}