// 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: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() { _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; } } @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; 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); } } newChildren.add( SemanticsNode() ..updateWith(config: configuration) ..rect = currentRect, ); } start += info.text.length; } node.updateWith(config: config, childrenInInversePaintOrder: newChildren); } @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')); } }