Unverified Commit b5df180a authored by LongCatIsLooong's avatar LongCatIsLooong Committed by GitHub

Move shared inline widget logic to `RenderInlineWidgetContainerDefaults` (#127308)

- Added `InlineWidgetContainerDefaults` for deduping inline widget code
- Added a helper function `WidgetSpan.extractFromInlineSpan` for extracting `WidgetSpan`s and automatically applying text scaling (at widget level)
- Removed `TextPainter.inlinePlaceholderScales`. I'm going to deprecate the `scale` argument in `TextPainter.addPlaceholder` next, as scaling is now done at the widget level.
- Added runtime check and comments to make sure nobody is extending `PlaceholderSpan` directly (unfortunately we can't remove `PlaceholderSpan`  without moving RenderEditable and RenderParagraph to the widgets library).
parent 49901d34
...@@ -395,9 +395,6 @@ abstract class InlineSpan extends DiagnosticableTree { ...@@ -395,9 +395,6 @@ abstract class InlineSpan extends DiagnosticableTree {
void debugFillProperties(DiagnosticPropertiesBuilder properties) { void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties); super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace; properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace;
style?.debugFillProperties(properties);
if (style != null) {
style!.debugFillProperties(properties);
}
} }
} }
...@@ -15,12 +15,16 @@ import 'text_style.dart'; ...@@ -15,12 +15,16 @@ import 'text_style.dart';
/// An immutable placeholder that is embedded inline within text. /// An immutable placeholder that is embedded inline within text.
/// ///
/// [PlaceholderSpan] represents a placeholder that acts as a stand-in for other /// [PlaceholderSpan] represents a placeholder that acts as a stand-in for other
/// content. A [PlaceholderSpan] by itself does not contain useful /// content. A [PlaceholderSpan] by itself does not contain useful information
/// information to change a [TextSpan]. Instead, this class must be extended /// to change a [TextSpan]. [WidgetSpan] from the widgets library extends
/// to define contents. /// [PlaceholderSpan] and may be used instead to specify a widget as the contents
/// of the placeholder.
///
/// Flutter widgets such as [TextField], [Text] and [RichText] do not recognize
/// [PlaceholderSpan] subclasses other than [WidgetSpan]. **Consider
/// implementing the [WidgetSpan] interface instead of the [Placeholder]
/// interface.**
/// ///
/// [WidgetSpan] from the widgets library extends [PlaceholderSpan] and may be
/// used instead to specify a widget as the contents of the placeholder.
/// ///
/// See also: /// See also:
/// ///
...@@ -89,4 +93,10 @@ abstract class PlaceholderSpan extends InlineSpan { ...@@ -89,4 +93,10 @@ abstract class PlaceholderSpan extends InlineSpan {
properties.add(EnumProperty<ui.PlaceholderAlignment>('alignment', alignment, defaultValue: null)); properties.add(EnumProperty<ui.PlaceholderAlignment>('alignment', alignment, defaultValue: null));
properties.add(EnumProperty<TextBaseline>('baseline', baseline, defaultValue: null)); properties.add(EnumProperty<TextBaseline>('baseline', baseline, defaultValue: null));
} }
@override
bool debugAssertIsValid() {
assert(false, 'Consider implementing the WidgetSpan interface instead.');
return super.debugAssertIsValid();
}
} }
...@@ -124,7 +124,14 @@ class PlaceholderDimensions { ...@@ -124,7 +124,14 @@ class PlaceholderDimensions {
@override @override
String toString() { String toString() {
return 'PlaceholderDimensions($size, $baseline${baselineOffset == null ? ", $baselineOffset" : ""})'; return switch (alignment) {
ui.PlaceholderAlignment.top ||
ui.PlaceholderAlignment.bottom ||
ui.PlaceholderAlignment.middle ||
ui.PlaceholderAlignment.aboveBaseline ||
ui.PlaceholderAlignment.belowBaseline => 'PlaceholderDimensions($size, $alignment)',
ui.PlaceholderAlignment.baseline => 'PlaceholderDimensions($size, $alignment($baselineOffset from top))',
};
} }
} }
...@@ -863,16 +870,6 @@ class TextPainter { ...@@ -863,16 +870,6 @@ class TextPainter {
return rawBoxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false); return rawBoxes.map((TextBox box) => _shiftTextBox(box, offset)).toList(growable: false);
} }
/// 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]. /// Sets the dimensions of each placeholder in [text].
/// ///
/// The number of [PlaceholderDimensions] provided should be the same as the /// The number of [PlaceholderDimensions] provided should be the same as the
...@@ -1029,7 +1026,6 @@ class TextPainter { ...@@ -1029,7 +1026,6 @@ class TextPainter {
ui.Paragraph _createParagraph(InlineSpan text) { ui.Paragraph _createParagraph(InlineSpan text) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle()); final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions); text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
_inlinePlaceholderScales = builder.placeholderScales;
assert(() { assert(() {
_debugMarkNeedsLayoutCallStack = null; _debugMarkNeedsLayoutCallStack = null;
return true; return true;
......
...@@ -2170,8 +2170,7 @@ abstract class RenderBox extends RenderObject { ...@@ -2170,8 +2170,7 @@ abstract class RenderBox extends RenderObject {
double? getDistanceToActualBaseline(TextBaseline baseline) { double? getDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.'); assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.');
_cachedBaselines ??= <TextBaseline, double?>{}; _cachedBaselines ??= <TextBaseline, double?>{};
_cachedBaselines!.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline)); return _cachedBaselines!.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline));
return _cachedBaselines![baseline];
} }
/// Returns the distance from the y-coordinate of the position of the box to /// Returns the distance from the y-coordinate of the position of the box to
......
...@@ -15,6 +15,7 @@ import 'package:flutter/services.dart'; ...@@ -15,6 +15,7 @@ import 'package:flutter/services.dart';
import 'box.dart'; import 'box.dart';
import 'custom_paint.dart'; import 'custom_paint.dart';
import 'layer.dart'; import 'layer.dart';
import 'layout_helper.dart';
import 'object.dart'; import 'object.dart';
import 'paragraph.dart'; import 'paragraph.dart';
import 'viewport_offset.dart'; import 'viewport_offset.dart';
...@@ -265,7 +266,7 @@ class VerticalCaretMovementRun implements Iterator<TextPosition> { ...@@ -265,7 +266,7 @@ class VerticalCaretMovementRun implements Iterator<TextPosition> {
/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value /// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
/// to actually blink the cursor, and other features not mentioned above are the /// to actually blink the cursor, and other features not mentioned above are the
/// responsibility of higher layers and not handled by this object. /// responsibility of higher layers and not handled by this object.
class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ContainerRenderObjectMixin<RenderBox, TextParentData>, RenderBoxContainerDefaultsMixin<RenderBox, TextParentData> implements TextLayoutMetrics { class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ContainerRenderObjectMixin<RenderBox, TextParentData>, RenderInlineChildrenContainerDefaults implements TextLayoutMetrics {
/// Creates a render object that implements the visual aspects of a text field. /// Creates a render object that implements the visual aspects of a text field.
/// ///
/// The [textAlign] argument must not be null. It defaults to [TextAlign.start]. /// The [textAlign] argument must not be null. It defaults to [TextAlign.start].
...@@ -385,14 +386,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -385,14 +386,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_updateForegroundPainter(foregroundPainter); _updateForegroundPainter(foregroundPainter);
_updatePainter(painter); _updatePainter(painter);
addAll(children); addAll(children);
_extractPlaceholderSpans(text);
}
@override
void setupParentData(RenderBox child) {
if (child.parentData is! TextParentData) {
child.parentData = TextParentData();
}
} }
/// Child render objects /// Child render objects
...@@ -435,17 +428,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -435,17 +428,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_foregroundPainter = newPainter; _foregroundPainter = newPainter;
} }
late List<PlaceholderSpan> _placeholderSpans;
void _extractPlaceholderSpans(InlineSpan? span) {
_placeholderSpans = <PlaceholderSpan>[];
span?.visitChildren((InlineSpan span) {
if (span is PlaceholderSpan) {
_placeholderSpans.add(span);
}
return true;
});
}
/// The [RenderEditablePainter] to use for painting above this /// The [RenderEditablePainter] to use for painting above this
/// [RenderEditable]'s text content. /// [RenderEditable]'s text content.
/// ///
...@@ -826,7 +808,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -826,7 +808,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
_textPainter.text = value; _textPainter.text = value;
_cachedAttributedValue = null; _cachedAttributedValue = null;
_cachedCombinedSemanticsInfos = null; _cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value); _canComputeIntrinsicsCached = null;
markNeedsTextLayout(); markNeedsTextLayout();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
} }
...@@ -1412,13 +1394,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -1412,13 +1394,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) { children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
final SemanticsNode childNode = children.elementAt(childIndex); final SemanticsNode childNode = children.elementAt(childIndex);
final TextParentData parentData = child!.parentData! as TextParentData; final TextParentData parentData = child!.parentData! as TextParentData;
assert(parentData.scale != null); assert(parentData.offset != null);
childNode.rect = Rect.fromLTWH(
childNode.rect.left,
childNode.rect.top,
childNode.rect.width * parentData.scale!,
childNode.rect.height * parentData.scale!,
);
newChildren.add(childNode); newChildren.add(childNode);
childIndex += 1; childIndex += 1;
} }
...@@ -1931,9 +1907,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -1931,9 +1907,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
@override @override
@protected @protected
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
// Hit test text spans.
bool hitText = false;
final InlineSpan? textSpan = _textPainter.text; final InlineSpan? textSpan = _textPainter.text;
if (textSpan != null) { if (textSpan != null) {
final Offset effectivePosition = position - _paintOffset; final Offset effectivePosition = position - _paintOffset;
...@@ -1941,42 +1914,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -1941,42 +1914,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
final Object? span = textSpan.getSpanForPosition(textPosition); final Object? span = textSpan.getSpanForPosition(textPosition);
if (span is HitTestTarget) { if (span is HitTestTarget) {
result.add(HitTestEntry(span)); result.add(HitTestEntry(span));
hitText = true;
}
}
// Hit test render object children
RenderBox? child = firstChild;
int childIndex = 0;
while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
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; return true;
} }
child = childAfter(child);
childIndex += 1;
} }
return hitText; return hitTestInlineChildren(result, position);
} }
late TapGestureRecognizer _tap; late TapGestureRecognizer _tap;
...@@ -2235,77 +2176,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2235,77 +2176,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
// restored to the original values before final layout and painting. // restored to the original values before final layout and painting.
List<PlaceholderDimensions>? _placeholderDimensions; List<PlaceholderDimensions>? _placeholderDimensions;
// 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.
List<PlaceholderDimensions> _layoutChildren(BoxConstraints constraints, {bool dry = false}) {
if (childCount == 0) {
_textPainter.setPlaceholderDimensions(<PlaceholderDimensions>[]);
return <PlaceholderDimensions>[];
}
RenderBox? child = firstChild;
final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty);
int childIndex = 0;
// Only constrain the width to the maximum width of the paragraph.
// Leave height unconstrained, which will overflow if expanded past.
BoxConstraints boxConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
// The content will be enlarged by textScaleFactor during painting phase.
// We reduce constraints by textScaleFactor, so that the content will fit
// into the box once it is enlarged.
boxConstraints = boxConstraints / textScaleFactor;
while (child != null) {
double? baselineOffset;
final Size childSize;
if (!dry) {
child.layout(
boxConstraints,
parentUsesSize: true,
);
childSize = child.size;
switch (_placeholderSpans[childIndex].alignment) {
case ui.PlaceholderAlignment.baseline:
baselineOffset = child.getDistanceToBaseline(
_placeholderSpans[childIndex].baseline!,
);
case ui.PlaceholderAlignment.aboveBaseline:
case ui.PlaceholderAlignment.belowBaseline:
case ui.PlaceholderAlignment.bottom:
case ui.PlaceholderAlignment.middle:
case ui.PlaceholderAlignment.top:
baselineOffset = null;
}
} else {
assert(_placeholderSpans[childIndex].alignment != ui.PlaceholderAlignment.baseline);
childSize = child.getDryLayout(boxConstraints);
}
placeholderDimensions[childIndex] = PlaceholderDimensions(
size: childSize,
alignment: _placeholderSpans[childIndex].alignment,
baseline: _placeholderSpans[childIndex].baseline,
baselineOffset: baselineOffset,
);
child = childAfter(child);
childIndex += 1;
}
return placeholderDimensions;
}
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;
}
}
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) { void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin); final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin);
final double availableMinWidth = math.min(minWidth, availableMaxWidth); final double availableMinWidth = math.min(minWidth, availableMaxWidth);
...@@ -2377,34 +2247,31 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2377,34 +2247,31 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
); );
} }
bool _canComputeDryLayout() { bool _canComputeDryLayoutForInlineWidgets() {
// Dry layout cannot be calculated without a full layout for return text?.visitChildren((InlineSpan span) {
// alignments that require the baseline (baseline, aboveBaseline, return (span is! PlaceholderSpan) || switch (span.alignment) {
// belowBaseline). ui.PlaceholderAlignment.baseline ||
for (final PlaceholderSpan span in _placeholderSpans) { ui.PlaceholderAlignment.aboveBaseline ||
switch (span.alignment) { ui.PlaceholderAlignment.belowBaseline => false,
case ui.PlaceholderAlignment.baseline: ui.PlaceholderAlignment.top ||
case ui.PlaceholderAlignment.aboveBaseline: ui.PlaceholderAlignment.middle ||
case ui.PlaceholderAlignment.belowBaseline: ui.PlaceholderAlignment.bottom => true,
return false; };
case ui.PlaceholderAlignment.top: }) ?? true;
case ui.PlaceholderAlignment.middle:
case ui.PlaceholderAlignment.bottom:
continue;
}
}
return true;
} }
bool? _canComputeIntrinsicsCached;
bool get _canComputeIntrinsics => _canComputeIntrinsicsCached ??= _canComputeDryLayoutForInlineWidgets();
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
if (!_canComputeDryLayout()) { if (!_canComputeIntrinsics) {
assert(debugCannotComputeDryLayout( assert(debugCannotComputeDryLayout(
reason: 'Dry layout not available for alignments that require baseline.', reason: 'Dry layout not available for alignments that require baseline.',
)); ));
return Size.zero; return Size.zero;
} }
_textPainter.setPlaceholderDimensions(_layoutChildren(constraints, dry: true)); _textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
final double width = forceLine ? constraints.maxWidth : constraints final double width = forceLine ? constraints.maxWidth : constraints
.constrainWidth(_textPainter.size.width + _caretMargin); .constrainWidth(_textPainter.size.width + _caretMargin);
...@@ -2414,10 +2281,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2414,10 +2281,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
@override @override
void performLayout() { void performLayout() {
final BoxConstraints constraints = this.constraints; final BoxConstraints constraints = this.constraints;
_placeholderDimensions = _layoutChildren(constraints); _placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
_textPainter.setPlaceholderDimensions(_placeholderDimensions); _textPainter.setPlaceholderDimensions(_placeholderDimensions);
_computeTextMetricsIfNeeded(); _computeTextMetricsIfNeeded();
_setParentData(); positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
_computeCaretPrototype(); _computeCaretPrototype();
// We grab _textPainter.size here because assigning to `size` on the next // We grab _textPainter.size here because assigning to `size` on the next
// line will trigger us to validate our intrinsic sizes, which will change // line will trigger us to validate our intrinsic sizes, which will change
...@@ -2592,31 +2459,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2592,31 +2459,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
} }
_textPainter.paint(context.canvas, effectiveOffset); _textPainter.paint(context.canvas, effectiveOffset);
paintInlineChildren(context, 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,
effectiveOffset + textParentData.offset,
Matrix4.diagonal3Values(scale, scale, scale),
(PaintingContext context, Offset offset) {
context.paintChild(
child!,
offset,
);
},
);
child = childAfter(child);
childIndex += 1;
}
if (foregroundChild != null) { if (foregroundChild != null) {
context.paintChild(foregroundChild, offset); context.paintChild(foregroundChild, offset);
...@@ -2648,6 +2491,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ...@@ -2648,6 +2491,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
} }
} }
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
if (child == _foregroundRenderObject || child == _backgroundRenderObject) {
return;
}
defaultApplyPaintTransform(child, transform);
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
_computeTextMetricsIfNeeded(); _computeTextMetricsIfNeeded();
......
...@@ -13,29 +13,13 @@ import 'package:flutter/services.dart'; ...@@ -13,29 +13,13 @@ import 'package:flutter/services.dart';
import 'box.dart'; import 'box.dart';
import 'debug.dart'; import 'debug.dart';
import 'editable.dart';
import 'layer.dart'; import 'layer.dart';
import 'layout_helper.dart';
import 'object.dart'; import 'object.dart';
import 'selection.dart'; import 'selection.dart';
const String _kEllipsis = '\u2026'; const String _kEllipsis = '\u2026';
/// Parent data for use with [RenderParagraph] and [RenderEditable].
class TextParentData extends ContainerBoxParentData<RenderBox> {
/// The scaling of the text.
double? scale;
@override
String toString() {
final List<String> values = <String>[
'offset=$offset',
if (scale != null) 'scale=$scale',
super.toString(),
];
return values.join('; ');
}
}
/// Used by the [RenderParagraph] to map its rendering children to their /// Used by the [RenderParagraph] to map its rendering children to their
/// corresponding semantics nodes. /// corresponding semantics nodes.
/// ///
...@@ -62,11 +46,210 @@ class PlaceholderSpanIndexSemanticsTag extends SemanticsTag { ...@@ -62,11 +46,210 @@ class PlaceholderSpanIndexSemanticsTag extends SemanticsTag {
int get hashCode => Object.hash(PlaceholderSpanIndexSemanticsTag, index); int get hashCode => Object.hash(PlaceholderSpanIndexSemanticsTag, index);
} }
/// Parent data used by [RenderParagraph] and [RenderEditable] to annotate
/// inline contents (such as [WidgetSpan]s) with.
class TextParentData extends ParentData with ContainerParentDataMixin<RenderBox> {
/// The offset at which to paint the child in the parent's coordinate system.
///
/// A `null` value indicates this inline widget is not laid out. For instance,
/// when the inline widget has never been laid out, or the inline widget is
/// ellipsized away.
Offset? get offset => _offset;
Offset? _offset;
/// The [PlaceholderSpan] associated with this render child.
///
/// This field is usually set by a [ParentDataWidget], and is typically not
/// null when `performLayout` is called.
PlaceholderSpan? span;
@override
void detach() {
span = null;
_offset = null;
super.detach();
}
@override
String toString() =>'widget: $span, ${offset == null ? "not laid out" : "offset: $offset"}';
}
/// A mixin that provides useful default behaviors for text [RenderBox]es
/// ([RenderParagraph] and [RenderEditable] for example) with inline content
/// children managed by the [ContainerRenderObjectMixin] mixin.
///
/// This mixin assumes every child managed by the [ContainerRenderObjectMixin]
/// mixin corresponds to a [PlaceholderSpan], and they are organized in logical
/// order of the text (the order each [PlaceholderSpan] is encountered when the
/// user reads the text).
///
/// To use this mixin in a [RenderBox] class:
///
/// * Call [layoutInlineChildren] in the `performLayout` and `computeDryLayout`
/// implementation, and during intrinsic size calculations, to get the size
/// information of the inline widgets as a `List` of `PlaceholderDimensions`.
/// Determine the positioning of the inline widgets (which is usually done by
/// a [TextPainter] using its line break algorithm).
///
/// * Call [positionInlineChildren] with the positioning information of the
/// inline widgets.
///
/// * Implement [RenderBox.applyPaintTransform], optionally with
/// [defaultApplyPaintTransform].
///
/// * Call [paintInlineChildren] in [RenderBox.paint] to paint the inline widgets.
///
/// * Call [hitTestInlineChildren] in [RenderBox.hitTestChildren] to hit test the
/// inline widgets.
///
/// See also:
///
/// * [WidgetSpan.extractFromInlineSpan], a helper function for extracting
/// [WidgetSpan]s from an [InlineSpan] tree.
mixin RenderInlineChildrenContainerDefaults on RenderBox, ContainerRenderObjectMixin<RenderBox, TextParentData> {
@override
void setupParentData(RenderBox child) {
if (child.parentData is! TextParentData) {
child.parentData = TextParentData();
}
}
static PlaceholderDimensions _layoutChild(RenderBox child, double maxWidth, ChildLayouter layoutChild) {
final TextParentData parentData = child.parentData! as TextParentData;
final PlaceholderSpan? span = parentData.span;
assert(span != null);
return span == null
? PlaceholderDimensions.empty
: PlaceholderDimensions(
size: layoutChild(child, BoxConstraints(maxWidth: maxWidth)),
alignment: span.alignment,
baseline: span.baseline,
baselineOffset: switch (span.alignment) {
ui.PlaceholderAlignment.aboveBaseline ||
ui.PlaceholderAlignment.belowBaseline ||
ui.PlaceholderAlignment.bottom ||
ui.PlaceholderAlignment.middle ||
ui.PlaceholderAlignment.top => null,
ui.PlaceholderAlignment.baseline => child.getDistanceToBaseline(span.baseline!),
},
);
}
/// Computes the layout for every inline child using the given `layoutChild`
/// function and the `maxWidth` constraint.
///
/// Returns a list of [PlaceholderDimensions], representing the layout results
/// for each child managed by the [ContainerRenderObjectMixin] mixin.
///
/// Since this method does not impose a maximum height constraint on the
/// inline children, some children may become taller than this [RenderBox].
///
/// See also:
///
/// * [TextPainter.setPlaceholderDimensions], the method that usually takes
/// the layout results from this method as the input.
@protected
List<PlaceholderDimensions> layoutInlineChildren(double maxWidth, ChildLayouter layoutChild) {
return <PlaceholderDimensions>[
for (RenderBox? child = firstChild; child != null; child = childAfter(child))
_layoutChild(child, maxWidth, layoutChild),
];
}
/// Positions each inline child according to the coordinates provided in the
/// `boxes` list.
///
/// The `boxes` list must be in logical order, which is the order each child
/// is encountered when the user reads the text. Usually the length of the
/// list equals [childCount], but it can be less than that, when some children
/// are ommitted due to ellipsing. It never exceeds [childCount].
///
/// See also:
///
/// * [TextPainter.inlinePlaceholderBoxes], the method that can be used to
/// get the input `boxes`.
@protected
void positionInlineChildren(List<ui.TextBox> boxes) {
RenderBox? child = firstChild;
for (final ui.TextBox box in boxes) {
if (child == null) {
assert(false, 'The length of boxes (${boxes.length}) should be greater than childCount ($childCount)');
return;
}
final TextParentData textParentData = child.parentData! as TextParentData;
textParentData._offset = Offset(box.left, box.top);
child = childAfter(child);
}
while (child != null) {
final TextParentData textParentData = child.parentData! as TextParentData;
textParentData._offset = null;
child = childAfter(child);
}
}
/// Applies the transform that would be applied when painting the given child
/// to the given matrix.
///
/// Render children whose [TextParentData.offset] is null zeros out the
/// `transform` to indicate they're invisible thus should not be painted.
@protected
void defaultApplyPaintTransform(RenderBox child, Matrix4 transform) {
final TextParentData childParentData = child.parentData! as TextParentData;
final Offset? offset = childParentData.offset;
if (offset == null) {
transform.setZero();
} else {
transform.translate(offset.dx, offset.dy);
}
}
/// Paints each inline child.
///
/// Render children whose [TextParentData.offset] is null will be skipped by
/// this method.
@protected
void paintInlineChildren(PaintingContext context, Offset offset) {
RenderBox? child = firstChild;
while (child != null) {
final TextParentData childParentData = child.parentData! as TextParentData;
final Offset? childOffset = childParentData.offset;
if (childOffset == null) {
return;
}
context.paintChild(child, childOffset + offset);
child = childAfter(child);
}
}
/// Performs a hit test on each inline child.
///
/// Render children whose [TextParentData.offset] is null will be skipped by
/// this method.
@protected
bool hitTestInlineChildren(BoxHitTestResult result, Offset position) {
RenderBox? child = firstChild;
while (child != null) {
final TextParentData childParentData = child.parentData! as TextParentData;
final Offset? childOffset = childParentData.offset;
if (childOffset == null) {
return false;
}
final bool isHit = result.addWithPaintOffset(
offset: childOffset,
position: position,
hitTest: (BoxHitTestResult result, Offset transformed) => child!.hitTest(result, position: transformed),
);
if (isHit) {
return true;
}
child = childAfter(child);
}
return false;
}
}
/// A render object that displays a paragraph of text. /// A render object that displays a paragraph of text.
class RenderParagraph extends RenderBox class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBox, TextParentData>, RenderInlineChildrenContainerDefaults, RelayoutWhenSystemFontsChangeMixin {
with ContainerRenderObjectMixin<RenderBox, TextParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
RelayoutWhenSystemFontsChangeMixin {
/// Creates a paragraph render object. /// Creates a paragraph render object.
/// ///
/// The [text], [textAlign], [textDirection], [overflow], [softWrap], and /// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
...@@ -106,17 +289,9 @@ class RenderParagraph extends RenderBox ...@@ -106,17 +289,9 @@ class RenderParagraph extends RenderBox
textHeightBehavior: textHeightBehavior, textHeightBehavior: textHeightBehavior,
) { ) {
addAll(children); addAll(children);
_extractPlaceholderSpans(text);
this.registrar = registrar; this.registrar = registrar;
} }
@override
void setupParentData(RenderBox child) {
if (child.parentData is! TextParentData) {
child.parentData = TextParentData();
}
}
static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit); static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit);
final TextPainter _textPainter; final TextPainter _textPainter;
...@@ -137,8 +312,8 @@ class RenderParagraph extends RenderBox ...@@ -137,8 +312,8 @@ class RenderParagraph extends RenderBox
case RenderComparison.paint: case RenderComparison.paint:
_textPainter.text = value; _textPainter.text = value;
_cachedAttributedLabels = null; _cachedAttributedLabels = null;
_canComputeIntrinsicsCached = null;
_cachedCombinedSemanticsInfos = null; _cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value);
markNeedsPaint(); markNeedsPaint();
markNeedsSemanticsUpdate(); markNeedsSemanticsUpdate();
case RenderComparison.layout: case RenderComparison.layout:
...@@ -146,7 +321,7 @@ class RenderParagraph extends RenderBox ...@@ -146,7 +321,7 @@ class RenderParagraph extends RenderBox
_overflowShader = null; _overflowShader = null;
_cachedAttributedLabels = null; _cachedAttributedLabels = null;
_cachedCombinedSemanticsInfos = null; _cachedCombinedSemanticsInfos = null;
_extractPlaceholderSpans(value); _canComputeIntrinsicsCached = null;
markNeedsLayout(); markNeedsLayout();
_removeSelectionRegistrarSubscription(); _removeSelectionRegistrarSubscription();
_disposeSelectableFragments(); _disposeSelectableFragments();
...@@ -256,17 +431,6 @@ class RenderParagraph extends RenderBox ...@@ -256,17 +431,6 @@ class RenderParagraph extends RenderBox
super.dispose(); super.dispose();
} }
late List<PlaceholderSpan> _placeholderSpans;
void _extractPlaceholderSpans(InlineSpan span) {
_placeholderSpans = <PlaceholderSpan>[];
span.visitChildren((InlineSpan span) {
if (span is PlaceholderSpan) {
_placeholderSpans.add(span);
}
return true;
});
}
/// How the text should be aligned horizontally. /// How the text should be aligned horizontally.
TextAlign get textAlign => _textPainter.textAlign; TextAlign get textAlign => _textPainter.textAlign;
set textAlign(TextAlign value) { set textAlign(TextAlign value) {
...@@ -438,7 +602,10 @@ class RenderParagraph extends RenderBox ...@@ -438,7 +602,10 @@ class RenderParagraph extends RenderBox
if (!_canComputeIntrinsics()) { if (!_canComputeIntrinsics()) {
return 0.0; return 0.0;
} }
_computeChildrenWidthWithMinIntrinsics(height); _textPainter.setPlaceholderDimensions(layoutInlineChildren(
double.infinity,
(RenderBox child, BoxConstraints constraints) => Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
));
_layoutText(); // layout with infinite width. _layoutText(); // layout with infinite width.
return _textPainter.minIntrinsicWidth; return _textPainter.minIntrinsicWidth;
} }
...@@ -448,7 +615,12 @@ class RenderParagraph extends RenderBox ...@@ -448,7 +615,12 @@ class RenderParagraph extends RenderBox
if (!_canComputeIntrinsics()) { if (!_canComputeIntrinsics()) {
return 0.0; return 0.0;
} }
_computeChildrenWidthWithMaxIntrinsics(height); _textPainter.setPlaceholderDimensions(layoutInlineChildren(
double.infinity,
// Height and baseline is irrelevant as all text will be laid
// out in a single line. Therefore, using 0.0 as a dummy for the height.
(RenderBox child, BoxConstraints constraints) => Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
));
_layoutText(); // layout with infinite width. _layoutText(); // layout with infinite width.
return _textPainter.maxIntrinsicWidth; return _textPainter.maxIntrinsicWidth;
} }
...@@ -457,7 +629,7 @@ class RenderParagraph extends RenderBox ...@@ -457,7 +629,7 @@ class RenderParagraph extends RenderBox
if (!_canComputeIntrinsics()) { if (!_canComputeIntrinsics()) {
return 0.0; return 0.0;
} }
_computeChildrenHeightWithMinIntrinsics(width); _textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
_layoutText(minWidth: width, maxWidth: width); _layoutText(minWidth: width, maxWidth: width);
return _textPainter.height; return _textPainter.height;
} }
...@@ -486,84 +658,36 @@ class RenderParagraph extends RenderBox ...@@ -486,84 +658,36 @@ class RenderParagraph extends RenderBox
return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic); return _textPainter.computeDistanceToActualBaseline(TextBaseline.alphabetic);
} }
/// Whether all inline widget children of this [RenderBox] support dry layout
/// calculation.
bool _canComputeDryLayoutForInlineWidgets() {
// Dry layout cannot be calculated without a full layout for
// alignments that require the baseline (baseline, aboveBaseline,
// belowBaseline).
return text.visitChildren((InlineSpan span) {
return (span is! PlaceholderSpan) || switch (span.alignment) {
ui.PlaceholderAlignment.baseline ||
ui.PlaceholderAlignment.aboveBaseline ||
ui.PlaceholderAlignment.belowBaseline => false,
ui.PlaceholderAlignment.top ||
ui.PlaceholderAlignment.middle ||
ui.PlaceholderAlignment.bottom => true,
};
});
}
bool? _canComputeIntrinsicsCached;
// Intrinsics cannot be calculated without a full layout for // Intrinsics cannot be calculated without a full layout for
// alignments that require the baseline (baseline, aboveBaseline, // alignments that require the baseline (baseline, aboveBaseline,
// belowBaseline). // belowBaseline).
bool _canComputeIntrinsics() { bool _canComputeIntrinsics() {
for (final PlaceholderSpan span in _placeholderSpans) { final bool returnValue = _canComputeIntrinsicsCached ??= _canComputeDryLayoutForInlineWidgets();
switch (span.alignment) { assert(
case ui.PlaceholderAlignment.baseline: returnValue || RenderObject.debugCheckingIntrinsics,
case ui.PlaceholderAlignment.aboveBaseline: 'Intrinsics are not available for PlaceholderAlignment.baseline, '
case ui.PlaceholderAlignment.belowBaseline: 'PlaceholderAlignment.aboveBaseline, or 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>.filled(childCount, PlaceholderDimensions.empty);
int childIndex = 0;
while (child != null) {
// Height and baseline is irrelevant as all text will be laid
// out in a single line. Therefore, using 0.0 as a dummy for the height.
placeholderDimensions[childIndex] = PlaceholderDimensions(
size: Size(child.getMaxIntrinsicWidth(double.infinity), 0.0),
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>.filled(childCount, PlaceholderDimensions.empty);
int childIndex = 0;
while (child != null) {
// Height and baseline is irrelevant; only looking for the widest word or
// placeholder. Therefore, using 0.0 as a dummy for height.
placeholderDimensions[childIndex] = PlaceholderDimensions(
size: Size(child.getMinIntrinsicWidth(double.infinity), 0.0),
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>.filled(childCount, PlaceholderDimensions.empty);
int childIndex = 0;
// Takes textScaleFactor into account because the content of the placeholder
// span will be scaled up when it paints.
width = width / textScaleFactor;
while (child != null) {
final Size size = child.getDryLayout(BoxConstraints(maxWidth: width));
placeholderDimensions[childIndex] = PlaceholderDimensions(
size: size,
alignment: _placeholderSpans[childIndex].alignment,
baseline: _placeholderSpans[childIndex].baseline,
); );
child = childAfter(child); return returnValue;
childIndex += 1;
}
_textPainter.setPlaceholderDimensions(placeholderDimensions);
} }
@override @override
...@@ -571,48 +695,13 @@ class RenderParagraph extends RenderBox ...@@ -571,48 +695,13 @@ class RenderParagraph extends RenderBox
@override @override
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) { bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
// Hit test text spans.
bool hitText = false;
final TextPosition textPosition = _textPainter.getPositionForOffset(position); final TextPosition textPosition = _textPainter.getPositionForOffset(position);
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition); final Object? span = _textPainter.text!.getSpanForPosition(textPosition);
if (span != null && span is HitTestTarget) { if (span is HitTestTarget) {
result.add(HitTestEntry(span as HitTestTarget)); result.add(HitTestEntry(span));
hitText = true; return true;
}
// Hit test render object children
RenderBox? child = firstChild;
int childIndex = 0;
while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
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);
childIndex += 1;
} }
return hitText; return hitTestInlineChildren(result, position);
} }
bool _needsClipping = false; bool _needsClipping = false;
...@@ -629,9 +718,7 @@ class RenderParagraph extends RenderBox ...@@ -629,9 +718,7 @@ class RenderParagraph extends RenderBox
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis; final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
_textPainter.layout( _textPainter.layout(
minWidth: minWidth, minWidth: minWidth,
maxWidth: widthMatters ? maxWidth: widthMatters ? maxWidth : double.infinity,
maxWidth :
double.infinity,
); );
} }
...@@ -653,106 +740,15 @@ class RenderParagraph extends RenderBox ...@@ -653,106 +740,15 @@ class RenderParagraph extends RenderBox
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _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.
List<PlaceholderDimensions> _layoutChildren(BoxConstraints constraints, {bool dry = false}) {
if (childCount == 0) {
return <PlaceholderDimensions>[];
}
RenderBox? child = firstChild;
final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.filled(childCount, PlaceholderDimensions.empty);
int childIndex = 0;
// Only constrain the width to the maximum width of the paragraph.
// Leave height unconstrained, which will overflow if expanded past.
BoxConstraints boxConstraints = BoxConstraints(maxWidth: constraints.maxWidth);
// The content will be enlarged by textScaleFactor during painting phase.
// We reduce constraints by textScaleFactor, so that the content will fit
// into the box once it is enlarged.
boxConstraints = boxConstraints / textScaleFactor;
while (child != null) {
double? baselineOffset;
final Size childSize;
if (!dry) {
child.layout(
boxConstraints,
parentUsesSize: true,
);
childSize = child.size;
switch (_placeholderSpans[childIndex].alignment) {
case ui.PlaceholderAlignment.baseline:
baselineOffset = child.getDistanceToBaseline(
_placeholderSpans[childIndex].baseline!,
);
case ui.PlaceholderAlignment.aboveBaseline:
case ui.PlaceholderAlignment.belowBaseline:
case ui.PlaceholderAlignment.bottom:
case ui.PlaceholderAlignment.middle:
case ui.PlaceholderAlignment.top:
baselineOffset = null;
}
} else {
assert(_placeholderSpans[childIndex].alignment != ui.PlaceholderAlignment.baseline);
childSize = child.getDryLayout(boxConstraints);
}
placeholderDimensions[childIndex] = PlaceholderDimensions(
size: childSize,
alignment: _placeholderSpans[childIndex].alignment,
baseline: _placeholderSpans[childIndex].baseline,
baselineOffset: baselineOffset,
);
child = childAfter(child);
childIndex += 1;
}
return 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 && 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;
}
}
bool _canComputeDryLayout() {
// Dry layout cannot be calculated without a full layout for
// alignments that require the baseline (baseline, aboveBaseline,
// belowBaseline).
for (final PlaceholderSpan span in _placeholderSpans) {
switch (span.alignment) {
case ui.PlaceholderAlignment.baseline:
case ui.PlaceholderAlignment.aboveBaseline:
case ui.PlaceholderAlignment.belowBaseline:
return false;
case ui.PlaceholderAlignment.top:
case ui.PlaceholderAlignment.middle:
case ui.PlaceholderAlignment.bottom:
continue;
}
}
return true;
}
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
if (!_canComputeDryLayout()) { if (!_canComputeIntrinsics()) {
assert(debugCannotComputeDryLayout( assert(debugCannotComputeDryLayout(
reason: 'Dry layout not available for alignments that require baseline.', reason: 'Dry layout not available for alignments that require baseline.',
)); ));
return Size.zero; return Size.zero;
} }
_textPainter.setPlaceholderDimensions(_layoutChildren(constraints, dry: true)); _textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth); _layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
return constraints.constrain(_textPainter.size); return constraints.constrain(_textPainter.size);
} }
...@@ -760,9 +756,9 @@ class RenderParagraph extends RenderBox ...@@ -760,9 +756,9 @@ class RenderParagraph extends RenderBox
@override @override
void performLayout() { void performLayout() {
final BoxConstraints constraints = this.constraints; final BoxConstraints constraints = this.constraints;
_placeholderDimensions = _layoutChildren(constraints); _placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
_layoutTextWithConstraints(constraints); _layoutTextWithConstraints(constraints);
_setParentData(); positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
// We grab _textPainter.size and _textPainter.didExceedMaxLines here because // We grab _textPainter.size and _textPainter.didExceedMaxLines here because
// assigning to `size` will trigger us to validate our intrinsic sizes, // assigning to `size` will trigger us to validate our intrinsic sizes,
...@@ -830,6 +826,11 @@ class RenderParagraph extends RenderBox ...@@ -830,6 +826,11 @@ class RenderParagraph extends RenderBox
} }
} }
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
defaultApplyPaintTransform(child, transform);
}
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
// Ideally we could compute the min/max intrinsic width/height with a // Ideally we could compute the min/max intrinsic width/height with a
...@@ -866,30 +867,8 @@ class RenderParagraph extends RenderBox ...@@ -866,30 +867,8 @@ class RenderParagraph extends RenderBox
} }
_textPainter.paint(context.canvas, offset); _textPainter.paint(context.canvas, offset);
RenderBox? child = firstChild; paintInlineChildren(context, offset);
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 (_needsClipping) {
if (_overflowShader != null) { if (_overflowShader != null) {
context.canvas.translate(offset.dx, offset.dy); context.canvas.translate(offset.dx, offset.dy);
...@@ -905,7 +884,6 @@ class RenderParagraph extends RenderBox ...@@ -905,7 +884,6 @@ class RenderParagraph extends RenderBox
fragment.paint(context, offset); fragment.paint(context, offset);
} }
} }
super.paint(context, offset);
} }
/// Returns the offset at which to paint the caret. /// Returns the offset at which to paint the caret.
...@@ -1155,15 +1133,8 @@ class RenderParagraph extends RenderBox ...@@ -1155,15 +1133,8 @@ class RenderParagraph extends RenderBox
children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) { children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
final SemanticsNode childNode = children.elementAt(childIndex); final SemanticsNode childNode = children.elementAt(childIndex);
final TextParentData parentData = child!.parentData! as TextParentData; final TextParentData parentData = child!.parentData! as TextParentData;
assert(parentData.scale != null || parentData.offset == Offset.zero);
// parentData.scale may be null if the render object is truncated. // parentData.scale may be null if the render object is truncated.
if (parentData.scale != null) { if (parentData.offset != null) {
childNode.rect = Rect.fromLTWH(
childNode.rect.left,
childNode.rect.top,
childNode.rect.width * parentData.scale!,
childNode.rect.height * parentData.scale!,
);
newChildren.add(childNode); newChildren.add(childNode);
} }
childIndex += 1; childIndex += 1;
......
...@@ -70,60 +70,40 @@ mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChi ...@@ -70,60 +70,40 @@ mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChi
@override @override
double computeMinIntrinsicWidth(double height) { double computeMinIntrinsicWidth(double height) {
if (child != null) { return child?.getMinIntrinsicWidth(height) ?? 0.0;
return child!.getMinIntrinsicWidth(height);
}
return 0.0;
} }
@override @override
double computeMaxIntrinsicWidth(double height) { double computeMaxIntrinsicWidth(double height) {
if (child != null) { return child?.getMaxIntrinsicWidth(height) ?? 0.0;
return child!.getMaxIntrinsicWidth(height);
}
return 0.0;
} }
@override @override
double computeMinIntrinsicHeight(double width) { double computeMinIntrinsicHeight(double width) {
if (child != null) { return child?.getMinIntrinsicHeight(width) ?? 0.0;
return child!.getMinIntrinsicHeight(width);
}
return 0.0;
} }
@override @override
double computeMaxIntrinsicHeight(double width) { double computeMaxIntrinsicHeight(double width) {
if (child != null) { return child?.getMaxIntrinsicHeight(width) ?? 0.0;
return child!.getMaxIntrinsicHeight(width);
}
return 0.0;
} }
@override @override
double? computeDistanceToActualBaseline(TextBaseline baseline) { double? computeDistanceToActualBaseline(TextBaseline baseline) {
if (child != null) { return child?.getDistanceToActualBaseline(baseline)
return child!.getDistanceToActualBaseline(baseline); ?? super.computeDistanceToActualBaseline(baseline);
}
return super.computeDistanceToActualBaseline(baseline);
} }
@override @override
Size computeDryLayout(BoxConstraints constraints) { Size computeDryLayout(BoxConstraints constraints) {
if (child != null) { return child?.getDryLayout(constraints) ?? computeSizeForNoChild(constraints);
return child!.getDryLayout(constraints);
}
return computeSizeForNoChild(constraints);
} }
@override @override
void performLayout() { void performLayout() {
if (child != null) { size = (child?..layout(constraints, parentUsesSize: true))?.size
child!.layout(constraints, parentUsesSize: true); ?? computeSizeForNoChild(constraints);
size = child!.size; return;
} else {
size = computeSizeForNoChild(constraints);
}
} }
/// Calculate the size the [RenderProxyBox] would have under the given /// Calculate the size the [RenderProxyBox] would have under the given
...@@ -142,9 +122,11 @@ mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChi ...@@ -142,9 +122,11 @@ mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChi
@override @override
void paint(PaintingContext context, Offset offset) { void paint(PaintingContext context, Offset offset) {
if (child != null) { final RenderBox? child = this.child;
context.paintChild(child!, offset); if (child == null) {
return;
} }
context.paintChild(child, offset);
} }
} }
......
...@@ -5732,24 +5732,7 @@ class RichText extends MultiChildRenderObjectWidget { ...@@ -5732,24 +5732,7 @@ class RichText extends MultiChildRenderObjectWidget {
this.selectionColor, this.selectionColor,
}) : assert(maxLines == null || maxLines > 0), }) : assert(maxLines == null || maxLines > 0),
assert(selectionRegistrar == null || selectionColor != null), assert(selectionRegistrar == null || selectionColor != null),
super(children: _extractChildren(text)); super(children: WidgetSpan.extractFromInlineSpan(text, textScaleFactor));
// Traverses the InlineSpan tree and depth-first collects the list of
// child widgets that are created in WidgetSpans.
static List<Widget> _extractChildren(InlineSpan span) {
int index = 0;
final List<Widget> result = <Widget>[];
span.visitChildren((InlineSpan span) {
if (span is WidgetSpan) {
result.add(Semantics(
tagForChildren: PlaceholderSpanIndexSemanticsTag(index++),
child: span.child,
));
}
return true;
});
return result;
}
/// The text to display in this widget. /// The text to display in this widget.
final InlineSpan text; final InlineSpan text;
......
...@@ -4768,20 +4768,7 @@ class _Editable extends MultiChildRenderObjectWidget { ...@@ -4768,20 +4768,7 @@ class _Editable extends MultiChildRenderObjectWidget {
this.promptRectRange, this.promptRectRange,
this.promptRectColor, this.promptRectColor,
required this.clipBehavior, required this.clipBehavior,
}) : super(children: _extractChildren(inlineSpan)); }) : super(children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaleFactor));
// 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;
}
final InlineSpan inlineSpan; final InlineSpan inlineSpan;
final TextEditingValue value; final TextEditingValue value;
......
...@@ -4,8 +4,10 @@ ...@@ -4,8 +4,10 @@
import 'dart:ui' as ui show ParagraphBuilder, PlaceholderAlignment; import 'dart:ui' as ui show ParagraphBuilder, PlaceholderAlignment;
import 'package:flutter/painting.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/rendering.dart';
import 'basic.dart';
import 'framework.dart'; import 'framework.dart';
// Examples can assume: // Examples can assume:
...@@ -85,6 +87,39 @@ class WidgetSpan extends PlaceholderSpan { ...@@ -85,6 +87,39 @@ class WidgetSpan extends PlaceholderSpan {
), ),
); );
/// Helper function for extracting [WidgetSpan]s in preorder, from the given
/// [InlineSpan] as a list of widgets.
///
/// The `textScaleFactor` is the the number of font pixels for each logical
/// pixel.
///
/// This function is used by [EditableText] and [RichText] so calling it
/// directly is rarely necessary.
static List<Widget> extractFromInlineSpan(InlineSpan span, double textScaleFactor) {
final List<Widget> widgets = <Widget>[];
int index = 0;
// This assumes an InlineSpan tree's logical order is equivalent to preorder.
span.visitChildren((InlineSpan span) {
if (span is WidgetSpan) {
widgets.add(
_WidgetSpanParentData(
span: span,
child: Semantics(
tagForChildren: PlaceholderSpanIndexSemanticsTag(index++),
child: _AutoScaleInlineWidget(span: span, textScaleFactor: textScaleFactor, child: span.child),
),
),
);
}
assert(
span is WidgetSpan || span is! PlaceholderSpan,
'$span is a PlaceholderSpan but not a WidgetSpan subclass. This is currently not supported.',
);
return true;
});
return widgets;
}
/// The widget to embed inline within text. /// The widget to embed inline within text.
final Widget child; final Widget child;
...@@ -110,7 +145,6 @@ class WidgetSpan extends PlaceholderSpan { ...@@ -110,7 +145,6 @@ class WidgetSpan extends PlaceholderSpan {
currentDimensions.size.width, currentDimensions.size.width,
currentDimensions.size.height, currentDimensions.size.height,
alignment, alignment,
scale: textScaleFactor,
baseline: currentDimensions.baseline, baseline: currentDimensions.baseline,
baselineOffset: currentDimensions.baselineOffset, baselineOffset: currentDimensions.baselineOffset,
); );
...@@ -212,4 +246,174 @@ class WidgetSpan extends PlaceholderSpan { ...@@ -212,4 +246,174 @@ class WidgetSpan extends PlaceholderSpan {
// from being constructed. // from being constructed.
return true; return true;
} }
@override
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.add(DiagnosticsProperty<Widget>('widget', child));
}
}
// A ParentDataWidget that sets TextParentData.span.
class _WidgetSpanParentData extends ParentDataWidget<TextParentData> {
const _WidgetSpanParentData({ required this.span, required super.child });
final WidgetSpan span;
@override
void applyParentData(RenderObject renderObject) {
final TextParentData parentData = renderObject.parentData! as TextParentData;
parentData.span = span;
}
@override
Type get debugTypicalAncestorWidgetClass => RenderInlineChildrenContainerDefaults;
}
// A RenderObjectWidget that automatically applies text scaling on inline
// widgets.
//
// TODO(LongCatIsLooong): this shouldn't happen automatically, at least there
// should be a way to opt out: https://github.com/flutter/flutter/issues/126962
class _AutoScaleInlineWidget extends SingleChildRenderObjectWidget {
const _AutoScaleInlineWidget({ required this.span, required this.textScaleFactor, required super.child });
final WidgetSpan span;
final double textScaleFactor;
@override
_RenderScaledInlineWidget createRenderObject(BuildContext context) {
return _RenderScaledInlineWidget(span.alignment, span.baseline, textScaleFactor);
}
@override
void updateRenderObject(BuildContext context, _RenderScaledInlineWidget renderObject) {
renderObject
..alignment = span.alignment
..baseline = span.baseline
..scale = textScaleFactor;
}
}
class _RenderScaledInlineWidget extends RenderBox with RenderObjectWithChildMixin<RenderBox> {
_RenderScaledInlineWidget(this._alignment, this._baseline, this._scale);
double get scale => _scale;
double _scale;
set scale(double value) {
if (value == _scale) {
return;
}
assert(value > 0);
assert(value.isFinite);
_scale = value;
markNeedsLayout();
}
ui.PlaceholderAlignment get alignment => _alignment;
ui.PlaceholderAlignment _alignment;
set alignment(ui.PlaceholderAlignment value) {
if (_alignment == value) {
return;
}
_alignment = value;
markNeedsLayout();
}
TextBaseline? get baseline => _baseline;
TextBaseline? _baseline;
set baseline(TextBaseline? value) {
if (value == _baseline) {
return;
}
_baseline = value;
markNeedsLayout();
}
@override
double computeMaxIntrinsicHeight(double width) {
return (child?.computeMaxIntrinsicHeight(width / scale) ?? 0.0) * scale;
}
@override
double computeMaxIntrinsicWidth(double height) {
return (child?.computeMaxIntrinsicWidth(height / scale) ?? 0.0) * scale;
}
@override
double computeMinIntrinsicHeight(double width) {
return (child?.computeMinIntrinsicHeight(width / scale) ?? 0.0) * scale;
}
@override
double computeMinIntrinsicWidth(double height) {
return (child?.computeMinIntrinsicWidth(height / scale) ?? 0.0) * scale;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
return switch (child?.getDistanceToActualBaseline(baseline)) {
null => super.computeDistanceToActualBaseline(baseline),
final double childBaseline => scale * childBaseline,
};
}
@override
Size computeDryLayout(BoxConstraints constraints) {
assert(!constraints.hasBoundedHeight);
final Size unscaledSize = child?.computeDryLayout(BoxConstraints(maxWidth: constraints.maxWidth / scale)) ?? Size.zero;
return unscaledSize * scale;
}
@override
void performLayout() {
final RenderBox? child = this.child;
if (child == null) {
return;
}
assert(!constraints.hasBoundedHeight);
// 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 / scale), parentUsesSize: true);
size = child.size * scale;
}
@override
void applyPaintTransform(RenderBox child, Matrix4 transform) {
transform.scale(scale, scale);
}
@override
void paint(PaintingContext context, Offset offset) {
final RenderBox? child = this.child;
if (child == null) {
layer = null;
return;
}
if (scale == 1.0) {
context.paintChild(child, offset);
layer = null;
return;
}
layer = context.pushTransform(
needsCompositing,
offset,
Matrix4.diagonal3Values(scale, scale, 1.0),
(PaintingContext context, Offset offset) => context.paintChild(child, offset),
oldLayer: layer as TransformLayer?
);
}
@override
bool hitTestChildren(BoxHitTestResult result, {required Offset position}) {
final RenderBox? child = this.child;
if (child == null) {
return false;
}
return result.addWithPaintTransform(
transform: Matrix4.diagonal3Values(scale, scale, 1.0),
position: position,
hitTest: (BoxHitTestResult result, Offset transformedOffset) => child.hitTest(result, position: transformedOffset),
);
}
} }
...@@ -13,6 +13,25 @@ import 'mock_canvas.dart'; ...@@ -13,6 +13,25 @@ import 'mock_canvas.dart';
import 'recording_canvas.dart'; import 'recording_canvas.dart';
import 'rendering_tester.dart'; import 'rendering_tester.dart';
void _applyParentData(List<RenderBox> inlineRenderBoxes, InlineSpan span) {
int index = 0;
RenderBox? previousBox;
span.visitChildren((InlineSpan span) {
if (span is! WidgetSpan) {
return true;
}
final RenderBox box = inlineRenderBoxes[index];
box.parentData = TextParentData()
..span = span
..previousSibling = previousBox;
(previousBox?.parentData as TextParentData?)?.nextSibling = box;
index += 1;
previousBox = box;
return true;
});
}
class _FakeEditableTextState with TextSelectionDelegate { class _FakeEditableTextState with TextSelectionDelegate {
@override @override
TextEditingValue textEditingValue = TextEditingValue.empty; TextEditingValue textEditingValue = TextEditingValue.empty;
...@@ -1327,7 +1346,7 @@ void main() { ...@@ -1327,7 +1346,7 @@ void main() {
selection: const TextSelection.collapsed(offset: 3), selection: const TextSelection.collapsed(offset: 3),
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, editable.text!);
layout(editable); layout(editable);
editable.hasFocus = true; editable.hasFocus = true;
pumpFrame(); pumpFrame();
...@@ -1370,6 +1389,7 @@ void main() { ...@@ -1370,6 +1389,7 @@ void main() {
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, editable.text!);
layout(editable); layout(editable);
editable.hasFocus = true; editable.hasFocus = true;
pumpFrame(); pumpFrame();
...@@ -1415,6 +1435,7 @@ void main() { ...@@ -1415,6 +1435,7 @@ void main() {
); );
// Force a line wrap // Force a line wrap
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: const BoxConstraints(maxWidth: 75)); layout(editable, constraints: const BoxConstraints(maxWidth: 75));
editable.hasFocus = true; editable.hasFocus = true;
pumpFrame(); pumpFrame();
...@@ -1465,6 +1486,7 @@ void main() { ...@@ -1465,6 +1486,7 @@ void main() {
); );
// Force a line wrap // Force a line wrap
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: const BoxConstraints(maxWidth: 75)); layout(editable, constraints: const BoxConstraints(maxWidth: 75));
editable.hasFocus = true; editable.hasFocus = true;
pumpFrame(); pumpFrame();
...@@ -1520,6 +1542,7 @@ void main() { ...@@ -1520,6 +1542,7 @@ void main() {
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, editable.text!);
// Force a line wrap // Force a line wrap
layout(editable, constraints: const BoxConstraints(maxWidth: 75)); layout(editable, constraints: const BoxConstraints(maxWidth: 75));
editable.hasFocus = true; editable.hasFocus = true;
...@@ -1554,6 +1577,7 @@ void main() { ...@@ -1554,6 +1577,7 @@ void main() {
selectionColor: Colors.black, selectionColor: Colors.black,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
cursorColor: Colors.red, cursorColor: Colors.red,
cursorWidth: 0.0,
offset: viewportOffset, offset: viewportOffset,
textSelectionDelegate: delegate, textSelectionDelegate: delegate,
startHandleLayerLink: LayerLink(), startHandleLayerLink: LayerLink(),
...@@ -1571,12 +1595,12 @@ void main() { ...@@ -1571,12 +1595,12 @@ void main() {
textScaleFactor: 2.0, textScaleFactor: 2.0,
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: const BoxConstraints(maxWidth: screenWidth)); layout(editable, constraints: const BoxConstraints(maxWidth: screenWidth));
editable.hasFocus = true; expect(editable.computeMaxIntrinsicWidth(fixedHeight),
final double maxIntrinsicWidth = editable.computeMaxIntrinsicWidth(fixedHeight); 2.0 * 10.0 * 4 + 14.0 * 7 + 1.0,
pumpFrame(); reason: "intrinsic width = scale factor * width of 'test' + width of 'one two' + _caretMargin",
);
expect(maxIntrinsicWidth, 278);
}); });
test('hits correct WidgetSpan when not scrolled', () { test('hits correct WidgetSpan when not scrolled', () {
...@@ -1613,6 +1637,7 @@ void main() { ...@@ -1613,6 +1637,7 @@ void main() {
), ),
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0))); layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
// Prepare for painting after layout. // Prepare for painting after layout.
pumpFrame(phase: EnginePhase.compositingBits); pumpFrame(phase: EnginePhase.compositingBits);
......
...@@ -14,6 +14,25 @@ import 'rendering_tester.dart'; ...@@ -14,6 +14,25 @@ import 'rendering_tester.dart';
const String _kText = "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen's Navee!"; const String _kText = "I polished up that handle so carefullee\nThat now I am the Ruler of the Queen's Navee!";
void _applyParentData(List<RenderBox> inlineRenderBoxes, InlineSpan span) {
int index = 0;
RenderBox? previousBox;
span.visitChildren((InlineSpan span) {
if (span is! WidgetSpan) {
return true;
}
final RenderBox box = inlineRenderBoxes[index];
box.parentData = TextParentData()
..span = span
..previousSibling = previousBox;
(previousBox?.parentData as TextParentData?)?.nextSibling = box;
index += 1;
previousBox = box;
return true;
});
}
// A subclass of RenderParagraph that returns an empty list in getBoxesForSelection // A subclass of RenderParagraph that returns an empty list in getBoxesForSelection
// for a given TextSelection. // for a given TextSelection.
// This is intended to simulate SkParagraph's implementation of Paragraph.getBoxesForRange, // This is intended to simulate SkParagraph's implementation of Paragraph.getBoxesForRange,
...@@ -504,6 +523,7 @@ void main() { ...@@ -504,6 +523,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0)); layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection( final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
...@@ -544,6 +564,7 @@ void main() { ...@@ -544,6 +564,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0)); layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection( final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
...@@ -559,91 +580,6 @@ void main() { ...@@ -559,91 +580,6 @@ void main() {
expect(boxes[4], const TextBox.fromLTRBD(48.0, 0.0, 62.0, 14.0, TextDirection.ltr)); expect(boxes[4], const TextBox.fromLTRBD(48.0, 0.0, 62.0, 14.0, TextDirection.ltr));
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020 }, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
test('can compute IntrinsicHeight for widget span', () {
// Regression test for https://github.com/flutter/flutter/issues/59316
const double screenWidth = 100.0;
const String sentence = 'one two';
List<RenderBox> renderBoxes = <RenderBox>[
RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
];
RenderParagraph paragraph = RenderParagraph(
const TextSpan(
children: <InlineSpan> [
WidgetSpan(child: Text(sentence)),
],
),
children: renderBoxes,
textDirection: TextDirection.ltr,
);
layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
final double singleLineHeight = paragraph.computeMaxIntrinsicHeight(screenWidth);
expect(singleLineHeight, 14.0);
pumpFrame();
renderBoxes = <RenderBox>[
RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
];
paragraph = RenderParagraph(
const TextSpan(
children: <InlineSpan> [
WidgetSpan(child: Text(sentence)),
],
),
textScaleFactor: 2.0,
children: renderBoxes,
textDirection: TextDirection.ltr,
);
layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
final double maxIntrinsicHeight = paragraph.computeMaxIntrinsicHeight(screenWidth);
final double minIntrinsicHeight = paragraph.computeMinIntrinsicHeight(screenWidth);
// intrinsicHeight = singleLineHeight * textScaleFactor * two lines.
expect(maxIntrinsicHeight, singleLineHeight * 2.0 * 2);
expect(maxIntrinsicHeight, minIntrinsicHeight);
});
test('can compute IntrinsicWidth for widget span', () {
// Regression test for https://github.com/flutter/flutter/issues/59316
const double screenWidth = 1000.0;
const double fixedHeight = 1000.0;
const String sentence = 'one two';
List<RenderBox> renderBoxes = <RenderBox>[
RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
];
RenderParagraph paragraph = RenderParagraph(
const TextSpan(
children: <InlineSpan> [
WidgetSpan(child: Text(sentence)),
],
),
children: renderBoxes,
textDirection: TextDirection.ltr,
);
layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
final double widthForOneLine = paragraph.computeMaxIntrinsicWidth(fixedHeight);
expect(widthForOneLine, 98.0);
pumpFrame();
renderBoxes = <RenderBox>[
RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
];
paragraph = RenderParagraph(
const TextSpan(
children: <InlineSpan> [
WidgetSpan(child: Text(sentence)),
],
),
textScaleFactor: 2.0,
children: renderBoxes,
textDirection: TextDirection.ltr,
);
layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
final double maxIntrinsicWidth = paragraph.computeMaxIntrinsicWidth(fixedHeight);
// maxIntrinsicWidth = widthForOneLine * textScaleFactor
expect(maxIntrinsicWidth, widthForOneLine * 2.0);
});
test('inline widgets multiline test', () { test('inline widgets multiline test', () {
const TextSpan text = TextSpan( const TextSpan text = TextSpan(
text: 'a', text: 'a',
...@@ -676,6 +612,7 @@ void main() { ...@@ -676,6 +612,7 @@ void main() {
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0)); layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection( final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
...@@ -715,6 +652,7 @@ void main() { ...@@ -715,6 +652,7 @@ void main() {
children: renderBoxes, children: renderBoxes,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
); );
_applyParentData(renderBoxes, paragraph.text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth)); layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
final SemanticsNode result = SemanticsNode(); final SemanticsNode result = SemanticsNode();
final SemanticsNode truncatedChild = SemanticsNode(); final SemanticsNode truncatedChild = SemanticsNode();
...@@ -815,6 +753,7 @@ void main() { ...@@ -815,6 +753,7 @@ void main() {
children: renderBoxes, children: renderBoxes,
textDirection: TextDirection.ltr, textDirection: TextDirection.ltr,
); );
_applyParentData(renderBoxes, paragraph.text);
layout(paragraph); layout(paragraph);
final SemanticsNode node = SemanticsNode(); final SemanticsNode node = SemanticsNode();
...@@ -901,6 +840,7 @@ void main() { ...@@ -901,6 +840,7 @@ void main() {
registrar: registrar, registrar: registrar,
children: renderBoxes, children: renderBoxes,
); );
_applyParentData(renderBoxes, paragraph.text);
layout(paragraph); layout(paragraph);
// The widget span will register to the selection container without going // The widget span will register to the selection container without going
// through the render paragraph. // through the render paragraph.
......
...@@ -997,7 +997,7 @@ void main() { ...@@ -997,7 +997,7 @@ void main() {
TestSemantics( TestSemantics(
label: 'INTERRUPTION', label: 'INTERRUPTION',
textDirection: TextDirection.rtl, textDirection: TextDirection.rtl,
rect: const Rect.fromLTRB(0.0, 0.0, 40.0, 80.0), rect: const Rect.fromLTRB(0.0, 0.0, 20.0, 40.0),
), ),
TestSemantics( TestSemantics(
label: 'sky', label: 'sky',
...@@ -1537,6 +1537,59 @@ void main() { ...@@ -1537,6 +1537,59 @@ void main() {
expect(paragraph.getMinIntrinsicWidth(0.0), 200); expect(paragraph.getMinIntrinsicWidth(0.0), 200);
}); });
testWidgets('can compute intrinsic width and height for widget span with text scaling', (WidgetTester tester) async {
// Regression test for https://github.com/flutter/flutter/issues/59316
const Key textKey = Key('RichText');
Widget textWithNestedInlineSpans({ required double textScaleFactor, required double screenWidth }) {
return Directionality(
textDirection: TextDirection.ltr,
child: Center(
child: OverflowBox(
alignment: Alignment.topLeft,
maxWidth: screenWidth,
child: RichText(
key: textKey,
textScaleFactor: textScaleFactor,
text: const TextSpan(children: <InlineSpan>[
WidgetSpan(child: Text('one two')),
]),
),
),
),
);
}
// The render object is going to be reused across widget tree rebuilds.
late final RenderParagraph outerParagraph = tester.renderObject(find.byKey(textKey));
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 1.0, screenWidth: 100.0));
expect(
outerParagraph.getMaxIntrinsicHeight(100.0),
14.0,
reason: 'singleLineHeight = 14.0',
);
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 2.0, screenWidth: 100.0));
expect(
outerParagraph.getMinIntrinsicHeight(100.0),
14.0 * 2.0 * 2,
reason: 'intrinsicHeight = singleLineHeight * textScaleFactor * two lines.',
);
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 1.0, screenWidth: 1000.0));
expect(
outerParagraph.getMaxIntrinsicWidth(1000.0),
14.0 * 7,
reason: 'intrinsic width = 14.0 * 7',
);
await tester.pumpWidget(textWithNestedInlineSpans(textScaleFactor: 2.0, screenWidth: 1000.0));
expect(
outerParagraph.getMaxIntrinsicWidth(1000.0),
14.0 * 2.0 * 7,
reason: 'intrinsic width = glyph advance * textScaleFactor * num of glyphs',
);
});
testWidgets('Text uses TextStyle.overflow', (WidgetTester tester) async { testWidgets('Text uses TextStyle.overflow', (WidgetTester tester) async {
const TextOverflow overflow = TextOverflow.fade; const TextOverflow overflow = TextOverflow.fade;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment