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).
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -15,6 +15,7 @@ import 'package:flutter/services.dart';
|
||||
import 'box.dart';
|
||||
import 'custom_paint.dart';
|
||||
import 'layer.dart';
|
||||
import 'layout_helper.dart';
|
||||
import 'object.dart';
|
||||
import 'paragraph.dart';
|
||||
import 'viewport_offset.dart';
|
||||
@@ -265,7 +266,7 @@ class VerticalCaretMovementRun implements Iterator<TextPosition> {
|
||||
/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
|
||||
/// to actually blink the cursor, and other features not mentioned above are the
|
||||
/// 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.
|
||||
///
|
||||
/// The [textAlign] argument must not be null. It defaults to [TextAlign.start].
|
||||
@@ -385,14 +386,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
_updateForegroundPainter(foregroundPainter);
|
||||
_updatePainter(painter);
|
||||
addAll(children);
|
||||
_extractPlaceholderSpans(text);
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! TextParentData) {
|
||||
child.parentData = TextParentData();
|
||||
}
|
||||
}
|
||||
|
||||
/// Child render objects
|
||||
@@ -435,17 +428,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
_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
|
||||
/// [RenderEditable]'s text content.
|
||||
///
|
||||
@@ -826,7 +808,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
_textPainter.text = value;
|
||||
_cachedAttributedValue = null;
|
||||
_cachedCombinedSemanticsInfos = null;
|
||||
_extractPlaceholderSpans(value);
|
||||
_canComputeIntrinsicsCached = null;
|
||||
markNeedsTextLayout();
|
||||
markNeedsSemanticsUpdate();
|
||||
}
|
||||
@@ -1412,13 +1394,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
|
||||
final SemanticsNode childNode = children.elementAt(childIndex);
|
||||
final TextParentData parentData = child!.parentData! as TextParentData;
|
||||
assert(parentData.scale != null);
|
||||
childNode.rect = Rect.fromLTWH(
|
||||
childNode.rect.left,
|
||||
childNode.rect.top,
|
||||
childNode.rect.width * parentData.scale!,
|
||||
childNode.rect.height * parentData.scale!,
|
||||
);
|
||||
assert(parentData.offset != null);
|
||||
newChildren.add(childNode);
|
||||
childIndex += 1;
|
||||
}
|
||||
@@ -1931,9 +1907,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
@override
|
||||
@protected
|
||||
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
||||
// Hit test text spans.
|
||||
bool hitText = false;
|
||||
|
||||
final InlineSpan? textSpan = _textPainter.text;
|
||||
if (textSpan != null) {
|
||||
final Offset effectivePosition = position - _paintOffset;
|
||||
@@ -1941,42 +1914,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
final Object? span = textSpan.getSpanForPosition(textPosition);
|
||||
if (span is HitTestTarget) {
|
||||
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;
|
||||
}
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
return hitText;
|
||||
return hitTestInlineChildren(result, position);
|
||||
}
|
||||
|
||||
late TapGestureRecognizer _tap;
|
||||
@@ -2235,77 +2176,6 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
// restored to the original values before final layout and painting.
|
||||
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 }) {
|
||||
final double availableMaxWidth = math.max(0.0, maxWidth - _caretMargin);
|
||||
final double availableMinWidth = math.min(minWidth, availableMaxWidth);
|
||||
@@ -2377,34 +2247,31 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
bool _canComputeDryLayoutForInlineWidgets() {
|
||||
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,
|
||||
};
|
||||
}) ?? true;
|
||||
}
|
||||
|
||||
bool? _canComputeIntrinsicsCached;
|
||||
bool get _canComputeIntrinsics => _canComputeIntrinsicsCached ??= _canComputeDryLayoutForInlineWidgets();
|
||||
|
||||
@override
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
if (!_canComputeDryLayout()) {
|
||||
if (!_canComputeIntrinsics) {
|
||||
assert(debugCannotComputeDryLayout(
|
||||
reason: 'Dry layout not available for alignments that require baseline.',
|
||||
));
|
||||
return Size.zero;
|
||||
}
|
||||
_textPainter.setPlaceholderDimensions(_layoutChildren(constraints, dry: true));
|
||||
_textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
|
||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||
final double width = forceLine ? constraints.maxWidth : constraints
|
||||
.constrainWidth(_textPainter.size.width + _caretMargin);
|
||||
@@ -2414,10 +2281,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
@override
|
||||
void performLayout() {
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
_placeholderDimensions = _layoutChildren(constraints);
|
||||
_placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
|
||||
_textPainter.setPlaceholderDimensions(_placeholderDimensions);
|
||||
_computeTextMetricsIfNeeded();
|
||||
_setParentData();
|
||||
positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
|
||||
_computeCaretPrototype();
|
||||
// We grab _textPainter.size here because assigning to `size` on the next
|
||||
// line will trigger us to validate our intrinsic sizes, which will change
|
||||
@@ -2592,31 +2459,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin,
|
||||
}
|
||||
|
||||
_textPainter.paint(context.canvas, effectiveOffset);
|
||||
|
||||
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;
|
||||
}
|
||||
paintInlineChildren(context, offset);
|
||||
|
||||
if (foregroundChild != null) {
|
||||
context.paintChild(foregroundChild, offset);
|
||||
@@ -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
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
_computeTextMetricsIfNeeded();
|
||||
|
||||
@@ -13,29 +13,13 @@ import 'package:flutter/services.dart';
|
||||
|
||||
import 'box.dart';
|
||||
import 'debug.dart';
|
||||
import 'editable.dart';
|
||||
import 'layer.dart';
|
||||
import 'layout_helper.dart';
|
||||
import 'object.dart';
|
||||
import 'selection.dart';
|
||||
|
||||
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
|
||||
/// corresponding semantics nodes.
|
||||
///
|
||||
@@ -62,11 +46,210 @@ class PlaceholderSpanIndexSemanticsTag extends SemanticsTag {
|
||||
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.
|
||||
class RenderParagraph extends RenderBox
|
||||
with ContainerRenderObjectMixin<RenderBox, TextParentData>,
|
||||
RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
|
||||
RelayoutWhenSystemFontsChangeMixin {
|
||||
class RenderParagraph extends RenderBox with ContainerRenderObjectMixin<RenderBox, TextParentData>, RenderInlineChildrenContainerDefaults, RelayoutWhenSystemFontsChangeMixin {
|
||||
/// Creates a paragraph render object.
|
||||
///
|
||||
/// The [text], [textAlign], [textDirection], [overflow], [softWrap], and
|
||||
@@ -106,17 +289,9 @@ class RenderParagraph extends RenderBox
|
||||
textHeightBehavior: textHeightBehavior,
|
||||
) {
|
||||
addAll(children);
|
||||
_extractPlaceholderSpans(text);
|
||||
this.registrar = registrar;
|
||||
}
|
||||
|
||||
@override
|
||||
void setupParentData(RenderBox child) {
|
||||
if (child.parentData is! TextParentData) {
|
||||
child.parentData = TextParentData();
|
||||
}
|
||||
}
|
||||
|
||||
static final String _placeholderCharacter = String.fromCharCode(PlaceholderSpan.placeholderCodeUnit);
|
||||
final TextPainter _textPainter;
|
||||
|
||||
@@ -137,8 +312,8 @@ class RenderParagraph extends RenderBox
|
||||
case RenderComparison.paint:
|
||||
_textPainter.text = value;
|
||||
_cachedAttributedLabels = null;
|
||||
_canComputeIntrinsicsCached = null;
|
||||
_cachedCombinedSemanticsInfos = null;
|
||||
_extractPlaceholderSpans(value);
|
||||
markNeedsPaint();
|
||||
markNeedsSemanticsUpdate();
|
||||
case RenderComparison.layout:
|
||||
@@ -146,7 +321,7 @@ class RenderParagraph extends RenderBox
|
||||
_overflowShader = null;
|
||||
_cachedAttributedLabels = null;
|
||||
_cachedCombinedSemanticsInfos = null;
|
||||
_extractPlaceholderSpans(value);
|
||||
_canComputeIntrinsicsCached = null;
|
||||
markNeedsLayout();
|
||||
_removeSelectionRegistrarSubscription();
|
||||
_disposeSelectableFragments();
|
||||
@@ -256,17 +431,6 @@ class RenderParagraph extends RenderBox
|
||||
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.
|
||||
TextAlign get textAlign => _textPainter.textAlign;
|
||||
set textAlign(TextAlign value) {
|
||||
@@ -438,7 +602,10 @@ class RenderParagraph extends RenderBox
|
||||
if (!_canComputeIntrinsics()) {
|
||||
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.
|
||||
return _textPainter.minIntrinsicWidth;
|
||||
}
|
||||
@@ -448,7 +615,12 @@ class RenderParagraph extends RenderBox
|
||||
if (!_canComputeIntrinsics()) {
|
||||
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.
|
||||
return _textPainter.maxIntrinsicWidth;
|
||||
}
|
||||
@@ -457,7 +629,7 @@ class RenderParagraph extends RenderBox
|
||||
if (!_canComputeIntrinsics()) {
|
||||
return 0.0;
|
||||
}
|
||||
_computeChildrenHeightWithMinIntrinsics(width);
|
||||
_textPainter.setPlaceholderDimensions(layoutInlineChildren(width, ChildLayoutHelper.dryLayoutChild));
|
||||
_layoutText(minWidth: width, maxWidth: width);
|
||||
return _textPainter.height;
|
||||
}
|
||||
@@ -486,84 +658,36 @@ class RenderParagraph extends RenderBox
|
||||
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
|
||||
// alignments that require the baseline (baseline, aboveBaseline,
|
||||
// belowBaseline).
|
||||
bool _canComputeIntrinsics() {
|
||||
for (final PlaceholderSpan span in _placeholderSpans) {
|
||||
switch (span.alignment) {
|
||||
case ui.PlaceholderAlignment.baseline:
|
||||
case ui.PlaceholderAlignment.aboveBaseline:
|
||||
case ui.PlaceholderAlignment.belowBaseline:
|
||||
assert(
|
||||
RenderObject.debugCheckingIntrinsics,
|
||||
'Intrinsics are not available for PlaceholderAlignment.baseline, '
|
||||
'PlaceholderAlignment.aboveBaseline, or PlaceholderAlignment.belowBaseline.',
|
||||
);
|
||||
return false;
|
||||
case ui.PlaceholderAlignment.top:
|
||||
case ui.PlaceholderAlignment.middle:
|
||||
case ui.PlaceholderAlignment.bottom:
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void _computeChildrenWidthWithMaxIntrinsics(double height) {
|
||||
RenderBox? child = firstChild;
|
||||
final List<PlaceholderDimensions> placeholderDimensions = List<PlaceholderDimensions>.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,
|
||||
final bool returnValue = _canComputeIntrinsicsCached ??= _canComputeDryLayoutForInlineWidgets();
|
||||
assert(
|
||||
returnValue || RenderObject.debugCheckingIntrinsics,
|
||||
'Intrinsics are not available for PlaceholderAlignment.baseline, '
|
||||
'PlaceholderAlignment.aboveBaseline, or PlaceholderAlignment.belowBaseline.',
|
||||
);
|
||||
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);
|
||||
childIndex += 1;
|
||||
}
|
||||
_textPainter.setPlaceholderDimensions(placeholderDimensions);
|
||||
return returnValue;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -571,48 +695,13 @@ class RenderParagraph extends RenderBox
|
||||
|
||||
@override
|
||||
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
||||
// Hit test text spans.
|
||||
bool hitText = false;
|
||||
final TextPosition textPosition = _textPainter.getPositionForOffset(position);
|
||||
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
|
||||
if (span != null && span is HitTestTarget) {
|
||||
result.add(HitTestEntry(span as HitTestTarget));
|
||||
hitText = true;
|
||||
final Object? span = _textPainter.text!.getSpanForPosition(textPosition);
|
||||
if (span is HitTestTarget) {
|
||||
result.add(HitTestEntry(span));
|
||||
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;
|
||||
@@ -629,9 +718,7 @@ class RenderParagraph extends RenderBox
|
||||
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
|
||||
_textPainter.layout(
|
||||
minWidth: minWidth,
|
||||
maxWidth: widthMatters ?
|
||||
maxWidth :
|
||||
double.infinity,
|
||||
maxWidth: widthMatters ? maxWidth : double.infinity,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -653,106 +740,15 @@ class RenderParagraph extends RenderBox
|
||||
_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
|
||||
Size computeDryLayout(BoxConstraints constraints) {
|
||||
if (!_canComputeDryLayout()) {
|
||||
if (!_canComputeIntrinsics()) {
|
||||
assert(debugCannotComputeDryLayout(
|
||||
reason: 'Dry layout not available for alignments that require baseline.',
|
||||
));
|
||||
return Size.zero;
|
||||
}
|
||||
_textPainter.setPlaceholderDimensions(_layoutChildren(constraints, dry: true));
|
||||
_textPainter.setPlaceholderDimensions(layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.dryLayoutChild));
|
||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||
return constraints.constrain(_textPainter.size);
|
||||
}
|
||||
@@ -760,9 +756,9 @@ class RenderParagraph extends RenderBox
|
||||
@override
|
||||
void performLayout() {
|
||||
final BoxConstraints constraints = this.constraints;
|
||||
_placeholderDimensions = _layoutChildren(constraints);
|
||||
_placeholderDimensions = layoutInlineChildren(constraints.maxWidth, ChildLayoutHelper.layoutChild);
|
||||
_layoutTextWithConstraints(constraints);
|
||||
_setParentData();
|
||||
positionInlineChildren(_textPainter.inlinePlaceholderBoxes!);
|
||||
|
||||
// We grab _textPainter.size and _textPainter.didExceedMaxLines here because
|
||||
// assigning to `size` will trigger us to validate our intrinsic sizes,
|
||||
@@ -830,6 +826,11 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void applyPaintTransform(RenderBox child, Matrix4 transform) {
|
||||
defaultApplyPaintTransform(child, transform);
|
||||
}
|
||||
|
||||
@override
|
||||
void paint(PaintingContext context, Offset offset) {
|
||||
// Ideally we could compute the min/max intrinsic width/height with a
|
||||
@@ -866,30 +867,8 @@ class RenderParagraph extends RenderBox
|
||||
}
|
||||
_textPainter.paint(context.canvas, offset);
|
||||
|
||||
RenderBox? child = firstChild;
|
||||
int childIndex = 0;
|
||||
// childIndex might be out of index of placeholder boxes. This can happen
|
||||
// if engine truncates children due to ellipsis. Sadly, we would not know
|
||||
// it until we finish layout, and RenderObject is in immutable state at
|
||||
// this point.
|
||||
while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
|
||||
final TextParentData textParentData = child.parentData! as TextParentData;
|
||||
paintInlineChildren(context, offset);
|
||||
|
||||
final double scale = textParentData.scale!;
|
||||
context.pushTransform(
|
||||
needsCompositing,
|
||||
offset + textParentData.offset,
|
||||
Matrix4.diagonal3Values(scale, scale, scale),
|
||||
(PaintingContext context, Offset offset) {
|
||||
context.paintChild(
|
||||
child!,
|
||||
offset,
|
||||
);
|
||||
},
|
||||
);
|
||||
child = childAfter(child);
|
||||
childIndex += 1;
|
||||
}
|
||||
if (_needsClipping) {
|
||||
if (_overflowShader != null) {
|
||||
context.canvas.translate(offset.dx, offset.dy);
|
||||
@@ -905,7 +884,6 @@ class RenderParagraph extends RenderBox
|
||||
fragment.paint(context, offset);
|
||||
}
|
||||
}
|
||||
super.paint(context, offset);
|
||||
}
|
||||
|
||||
/// Returns the offset at which to paint the caret.
|
||||
@@ -1155,15 +1133,8 @@ class RenderParagraph extends RenderBox
|
||||
children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
|
||||
final SemanticsNode childNode = children.elementAt(childIndex);
|
||||
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.
|
||||
if (parentData.scale != null) {
|
||||
childNode.rect = Rect.fromLTWH(
|
||||
childNode.rect.left,
|
||||
childNode.rect.top,
|
||||
childNode.rect.width * parentData.scale!,
|
||||
childNode.rect.height * parentData.scale!,
|
||||
);
|
||||
if (parentData.offset != null) {
|
||||
newChildren.add(childNode);
|
||||
}
|
||||
childIndex += 1;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user