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 {
void debugFillProperties(DiagnosticPropertiesBuilder properties) {
super.debugFillProperties(properties);
properties.defaultDiagnosticsTreeStyle = DiagnosticsTreeStyle.whitespace;
if (style != null) {
style!.debugFillProperties(properties);
}
style?.debugFillProperties(properties);
}
}
......@@ -15,12 +15,16 @@ import 'text_style.dart';
/// An immutable placeholder that is embedded inline within text.
///
/// [PlaceholderSpan] represents a placeholder that acts as a stand-in for other
/// content. A [PlaceholderSpan] by itself does not contain useful
/// information to change a [TextSpan]. Instead, this class must be extended
/// to define contents.
/// content. A [PlaceholderSpan] by itself does not contain useful information
/// to change a [TextSpan]. [WidgetSpan] from the widgets library extends
/// [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:
///
......@@ -89,4 +93,10 @@ abstract class PlaceholderSpan extends InlineSpan {
properties.add(EnumProperty<ui.PlaceholderAlignment>('alignment', alignment, 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 {
@override
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 {
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].
///
/// The number of [PlaceholderDimensions] provided should be the same as the
......@@ -1029,7 +1026,6 @@ class TextPainter {
ui.Paragraph _createParagraph(InlineSpan text) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
text.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
_inlinePlaceholderScales = builder.placeholderScales;
assert(() {
_debugMarkNeedsLayoutCallStack = null;
return true;
......
......@@ -2170,8 +2170,7 @@ abstract class RenderBox extends RenderObject {
double? getDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugDoingBaseline, 'Please see the documentation for computeDistanceToActualBaseline for the required calling conventions of this method.');
_cachedBaselines ??= <TextBaseline, double?>{};
_cachedBaselines!.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline));
return _cachedBaselines![baseline];
return _cachedBaselines!.putIfAbsent(baseline, () => computeDistanceToActualBaseline(baseline));
}
/// Returns the distance from the y-coordinate of the position of the box to
......
......@@ -70,60 +70,40 @@ mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChi
@override
double computeMinIntrinsicWidth(double height) {
if (child != null) {
return child!.getMinIntrinsicWidth(height);
}
return 0.0;
return child?.getMinIntrinsicWidth(height) ?? 0.0;
}
@override
double computeMaxIntrinsicWidth(double height) {
if (child != null) {
return child!.getMaxIntrinsicWidth(height);
}
return 0.0;
return child?.getMaxIntrinsicWidth(height) ?? 0.0;
}
@override
double computeMinIntrinsicHeight(double width) {
if (child != null) {
return child!.getMinIntrinsicHeight(width);
}
return 0.0;
return child?.getMinIntrinsicHeight(width) ?? 0.0;
}
@override
double computeMaxIntrinsicHeight(double width) {
if (child != null) {
return child!.getMaxIntrinsicHeight(width);
}
return 0.0;
return child?.getMaxIntrinsicHeight(width) ?? 0.0;
}
@override
double? computeDistanceToActualBaseline(TextBaseline baseline) {
if (child != null) {
return child!.getDistanceToActualBaseline(baseline);
}
return super.computeDistanceToActualBaseline(baseline);
return child?.getDistanceToActualBaseline(baseline)
?? super.computeDistanceToActualBaseline(baseline);
}
@override
Size computeDryLayout(BoxConstraints constraints) {
if (child != null) {
return child!.getDryLayout(constraints);
}
return computeSizeForNoChild(constraints);
return child?.getDryLayout(constraints) ?? computeSizeForNoChild(constraints);
}
@override
void performLayout() {
if (child != null) {
child!.layout(constraints, parentUsesSize: true);
size = child!.size;
} else {
size = computeSizeForNoChild(constraints);
}
size = (child?..layout(constraints, parentUsesSize: true))?.size
?? computeSizeForNoChild(constraints);
return;
}
/// Calculate the size the [RenderProxyBox] would have under the given
......@@ -142,9 +122,11 @@ mixin RenderProxyBoxMixin<T extends RenderBox> on RenderBox, RenderObjectWithChi
@override
void paint(PaintingContext context, Offset offset) {
if (child != null) {
context.paintChild(child!, offset);
final RenderBox? child = this.child;
if (child == null) {
return;
}
context.paintChild(child, offset);
}
}
......
......@@ -5732,24 +5732,7 @@ class RichText extends MultiChildRenderObjectWidget {
this.selectionColor,
}) : assert(maxLines == null || maxLines > 0),
assert(selectionRegistrar == null || selectionColor != null),
super(children: _extractChildren(text));
// Traverses the InlineSpan tree and depth-first collects the list of
// child widgets that are created in WidgetSpans.
static List<Widget> _extractChildren(InlineSpan span) {
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;
}
super(children: WidgetSpan.extractFromInlineSpan(text, textScaleFactor));
/// The text to display in this widget.
final InlineSpan text;
......
......@@ -4768,20 +4768,7 @@ class _Editable extends MultiChildRenderObjectWidget {
this.promptRectRange,
this.promptRectColor,
required this.clipBehavior,
}) : super(children: _extractChildren(inlineSpan));
// 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;
}
}) : super(children: WidgetSpan.extractFromInlineSpan(inlineSpan, textScaleFactor));
final InlineSpan inlineSpan;
final TextEditingValue value;
......
......@@ -4,8 +4,10 @@
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';
// Examples can assume:
......@@ -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.
final Widget child;
......@@ -110,7 +145,6 @@ class WidgetSpan extends PlaceholderSpan {
currentDimensions.size.width,
currentDimensions.size.height,
alignment,
scale: textScaleFactor,
baseline: currentDimensions.baseline,
baselineOffset: currentDimensions.baselineOffset,
);
......@@ -212,4 +246,174 @@ class WidgetSpan extends PlaceholderSpan {
// from being constructed.
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';
import 'recording_canvas.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 {
@override
TextEditingValue textEditingValue = TextEditingValue.empty;
......@@ -1327,7 +1346,7 @@ void main() {
selection: const TextSelection.collapsed(offset: 3),
children: renderBoxes,
);
_applyParentData(renderBoxes, editable.text!);
layout(editable);
editable.hasFocus = true;
pumpFrame();
......@@ -1370,6 +1389,7 @@ void main() {
children: renderBoxes,
);
_applyParentData(renderBoxes, editable.text!);
layout(editable);
editable.hasFocus = true;
pumpFrame();
......@@ -1415,6 +1435,7 @@ void main() {
);
// Force a line wrap
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: const BoxConstraints(maxWidth: 75));
editable.hasFocus = true;
pumpFrame();
......@@ -1465,6 +1486,7 @@ void main() {
);
// Force a line wrap
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: const BoxConstraints(maxWidth: 75));
editable.hasFocus = true;
pumpFrame();
......@@ -1520,6 +1542,7 @@ void main() {
children: renderBoxes,
);
_applyParentData(renderBoxes, editable.text!);
// Force a line wrap
layout(editable, constraints: const BoxConstraints(maxWidth: 75));
editable.hasFocus = true;
......@@ -1554,6 +1577,7 @@ void main() {
selectionColor: Colors.black,
textDirection: TextDirection.ltr,
cursorColor: Colors.red,
cursorWidth: 0.0,
offset: viewportOffset,
textSelectionDelegate: delegate,
startHandleLayerLink: LayerLink(),
......@@ -1571,12 +1595,12 @@ void main() {
textScaleFactor: 2.0,
children: renderBoxes,
);
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: const BoxConstraints(maxWidth: screenWidth));
editable.hasFocus = true;
final double maxIntrinsicWidth = editable.computeMaxIntrinsicWidth(fixedHeight);
pumpFrame();
expect(maxIntrinsicWidth, 278);
expect(editable.computeMaxIntrinsicWidth(fixedHeight),
2.0 * 10.0 * 4 + 14.0 * 7 + 1.0,
reason: "intrinsic width = scale factor * width of 'test' + width of 'one two' + _caretMargin",
);
});
test('hits correct WidgetSpan when not scrolled', () {
......@@ -1613,6 +1637,7 @@ void main() {
),
children: renderBoxes,
);
_applyParentData(renderBoxes, editable.text!);
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
// Prepare for painting after layout.
pumpFrame(phase: EnginePhase.compositingBits);
......
......@@ -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!";
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
// for a given TextSelection.
// This is intended to simulate SkParagraph's implementation of Paragraph.getBoxesForRange,
......@@ -504,6 +523,7 @@ void main() {
textDirection: TextDirection.ltr,
children: renderBoxes,
);
_applyParentData(renderBoxes, text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
......@@ -544,6 +564,7 @@ void main() {
textDirection: TextDirection.ltr,
children: renderBoxes,
);
_applyParentData(renderBoxes, text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 100.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
......@@ -559,91 +580,6 @@ void main() {
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
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', () {
const TextSpan text = TextSpan(
text: 'a',
......@@ -676,6 +612,7 @@ void main() {
textDirection: TextDirection.ltr,
children: renderBoxes,
);
_applyParentData(renderBoxes, text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: 50.0));
final List<ui.TextBox> boxes = paragraph.getBoxesForSelection(
......@@ -715,6 +652,7 @@ void main() {
children: renderBoxes,
textDirection: TextDirection.ltr,
);
_applyParentData(renderBoxes, paragraph.text);
layout(paragraph, constraints: const BoxConstraints(maxWidth: screenWidth));
final SemanticsNode result = SemanticsNode();
final SemanticsNode truncatedChild = SemanticsNode();
......@@ -815,6 +753,7 @@ void main() {
children: renderBoxes,
textDirection: TextDirection.ltr,
);
_applyParentData(renderBoxes, paragraph.text);
layout(paragraph);
final SemanticsNode node = SemanticsNode();
......@@ -901,6 +840,7 @@ void main() {
registrar: registrar,
children: renderBoxes,
);
_applyParentData(renderBoxes, paragraph.text);
layout(paragraph);
// The widget span will register to the selection container without going
// through the render paragraph.
......
......@@ -997,7 +997,7 @@ void main() {
TestSemantics(
label: 'INTERRUPTION',
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(
label: 'sky',
......@@ -1537,6 +1537,59 @@ void main() {
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 {
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