Support WidgetSpan in RenderEditable (#83537)
This commit is contained in:
parent
18b157886c
commit
e70a1d1d7a
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import 'dart:collection';
|
import 'dart:collection';
|
||||||
import 'dart:math' as math;
|
import 'dart:math' as math;
|
||||||
import 'dart:ui' as ui show TextBox, BoxHeightStyle, BoxWidthStyle;
|
import 'dart:ui' as ui show TextBox, BoxHeightStyle, BoxWidthStyle, PlaceholderAlignment;
|
||||||
|
|
||||||
import 'package:characters/characters.dart';
|
import 'package:characters/characters.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -12,10 +12,13 @@ import 'package:flutter/gestures.dart';
|
|||||||
import 'package:flutter/semantics.dart';
|
import 'package:flutter/semantics.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
import 'package:vector_math/vector_math_64.dart';
|
||||||
|
|
||||||
import 'box.dart';
|
import 'box.dart';
|
||||||
import 'custom_paint.dart';
|
import 'custom_paint.dart';
|
||||||
import 'layer.dart';
|
import 'layer.dart';
|
||||||
import 'object.dart';
|
import 'object.dart';
|
||||||
|
import 'paragraph.dart';
|
||||||
import 'viewport_offset.dart';
|
import 'viewport_offset.dart';
|
||||||
|
|
||||||
const double _kCaretGap = 1.0; // pixels
|
const double _kCaretGap = 1.0; // pixels
|
||||||
@ -136,7 +139,7 @@ bool _isWhitespace(int codeUnit) {
|
|||||||
/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
|
/// Keyboard handling, IME handling, scrolling, toggling the [showCursor] value
|
||||||
/// to actually blink the cursor, and other features not mentioned above are the
|
/// to actually blink the cursor, and other features not mentioned above are the
|
||||||
/// responsibility of higher layers and not handled by this object.
|
/// responsibility of higher layers and not handled by this object.
|
||||||
class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin, ContainerRenderObjectMixin<RenderBox, TextParentData>, RenderBoxContainerDefaultsMixin<RenderBox, TextParentData> {
|
||||||
/// Creates a render object that implements the visual aspects of a text field.
|
/// Creates a render object that implements the visual aspects of a text field.
|
||||||
///
|
///
|
||||||
/// The [textAlign] argument must not be null. It defaults to [TextAlign.start].
|
/// The [textAlign] argument must not be null. It defaults to [TextAlign.start].
|
||||||
@ -152,7 +155,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
/// The [offset] is required and must not be null. You can use [new
|
/// The [offset] is required and must not be null. You can use [new
|
||||||
/// ViewportOffset.zero] if you have no need for scrolling.
|
/// ViewportOffset.zero] if you have no need for scrolling.
|
||||||
RenderEditable({
|
RenderEditable({
|
||||||
TextSpan? text,
|
InlineSpan? text,
|
||||||
required TextDirection textDirection,
|
required TextDirection textDirection,
|
||||||
TextAlign textAlign = TextAlign.start,
|
TextAlign textAlign = TextAlign.start,
|
||||||
Color? cursorColor,
|
Color? cursorColor,
|
||||||
@ -199,6 +202,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
required this.textSelectionDelegate,
|
required this.textSelectionDelegate,
|
||||||
RenderEditablePainter? painter,
|
RenderEditablePainter? painter,
|
||||||
RenderEditablePainter? foregroundPainter,
|
RenderEditablePainter? foregroundPainter,
|
||||||
|
List<RenderBox>? children,
|
||||||
}) : assert(textAlign != null),
|
}) : assert(textAlign != null),
|
||||||
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
|
assert(textDirection != null, 'RenderEditable created without a textDirection.'),
|
||||||
assert(maxLines == null || maxLines > 0),
|
assert(maxLines == null || maxLines > 0),
|
||||||
@ -277,6 +281,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
|
|
||||||
_updateForegroundPainter(foregroundPainter);
|
_updateForegroundPainter(foregroundPainter);
|
||||||
_updatePainter(painter);
|
_updatePainter(painter);
|
||||||
|
addAll(children);
|
||||||
|
_extractPlaceholderSpans(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void setupParentData(RenderBox child) {
|
||||||
|
if (child.parentData is! TextParentData)
|
||||||
|
child.parentData = TextParentData();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Child render objects
|
/// Child render objects
|
||||||
@ -310,6 +322,17 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
_foregroundPainter = newPainter;
|
_foregroundPainter = newPainter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
late List<PlaceholderSpan> _placeholderSpans;
|
||||||
|
void _extractPlaceholderSpans(InlineSpan? span) {
|
||||||
|
_placeholderSpans = <PlaceholderSpan>[];
|
||||||
|
span?.visitChildren((InlineSpan span) {
|
||||||
|
if (span is PlaceholderSpan) {
|
||||||
|
_placeholderSpans.add(span);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/// The [RenderEditablePainter] to use for painting above this
|
/// The [RenderEditablePainter] to use for painting above this
|
||||||
/// [RenderEditable]'s text content.
|
/// [RenderEditable]'s text content.
|
||||||
///
|
///
|
||||||
@ -2295,13 +2318,14 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The text to display.
|
/// The text to display.
|
||||||
TextSpan? get text => _textPainter.text as TextSpan?;
|
InlineSpan? get text => _textPainter.text;
|
||||||
final TextPainter _textPainter;
|
final TextPainter _textPainter;
|
||||||
set text(TextSpan? value) {
|
set text(InlineSpan? value) {
|
||||||
if (_textPainter.text == value)
|
if (_textPainter.text == value)
|
||||||
return;
|
return;
|
||||||
_textPainter.text = value;
|
_textPainter.text = value;
|
||||||
_cachedPlainText = null;
|
_cachedPlainText = null;
|
||||||
|
_extractPlaceholderSpans(value);
|
||||||
markNeedsTextLayout();
|
markNeedsTextLayout();
|
||||||
markNeedsSemanticsUpdate();
|
markNeedsSemanticsUpdate();
|
||||||
}
|
}
|
||||||
@ -2831,74 +2855,96 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
Rect currentRect;
|
Rect currentRect;
|
||||||
double ordinal = 0.0;
|
double ordinal = 0.0;
|
||||||
int start = 0;
|
int start = 0;
|
||||||
|
int placeholderIndex = 0;
|
||||||
|
int childIndex = 0;
|
||||||
|
RenderBox? child = firstChild;
|
||||||
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
|
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
|
||||||
for (final InlineSpanSemanticsInformation info in combineSemanticsInfo(_semanticsInfo!)) {
|
for (final InlineSpanSemanticsInformation info in combineSemanticsInfo(_semanticsInfo!)) {
|
||||||
assert(!info.isPlaceholder);
|
|
||||||
final TextSelection selection = TextSelection(
|
final TextSelection selection = TextSelection(
|
||||||
baseOffset: start,
|
baseOffset: start,
|
||||||
extentOffset: start + info.text.length,
|
extentOffset: start + info.text.length,
|
||||||
);
|
);
|
||||||
start += info.text.length;
|
start += info.text.length;
|
||||||
|
|
||||||
final TextDirection initialDirection = currentDirection;
|
if (info.isPlaceholder) {
|
||||||
final List<ui.TextBox> rects = _textPainter.getBoxesForSelection(selection);
|
// A placeholder span may have 0 to multiple semantics nodes, we need
|
||||||
if (rects.isEmpty) {
|
// to annotate all of the semantics nodes belong to this span.
|
||||||
continue;
|
while (children.length > childIndex &&
|
||||||
}
|
children.elementAt(childIndex).isTagged(PlaceholderSpanIndexSemanticsTag(placeholderIndex))) {
|
||||||
Rect rect = rects.first.toRect();
|
final SemanticsNode childNode = children.elementAt(childIndex);
|
||||||
currentDirection = rects.first.direction;
|
final TextParentData parentData = child!.parentData! as TextParentData;
|
||||||
for (final ui.TextBox textBox in rects.skip(1)) {
|
childNode.rect = Rect.fromLTWH(
|
||||||
rect = rect.expandToInclude(textBox.toRect());
|
childNode.rect.left,
|
||||||
currentDirection = textBox.direction;
|
childNode.rect.top,
|
||||||
}
|
childNode.rect.width * parentData.scale!,
|
||||||
// Any of the text boxes may have had infinite dimensions.
|
childNode.rect.height * parentData.scale!,
|
||||||
// We shouldn't pass infinite dimensions up to the bridges.
|
);
|
||||||
rect = Rect.fromLTWH(
|
newChildren.add(childNode);
|
||||||
math.max(0.0, rect.left),
|
childIndex += 1;
|
||||||
math.max(0.0, rect.top),
|
|
||||||
math.min(rect.width, constraints.maxWidth),
|
|
||||||
math.min(rect.height, constraints.maxHeight),
|
|
||||||
);
|
|
||||||
// Round the current rectangle to make this API testable and add some
|
|
||||||
// padding so that the accessibility rects do not overlap with the text.
|
|
||||||
currentRect = Rect.fromLTRB(
|
|
||||||
rect.left.floorToDouble() - 4.0,
|
|
||||||
rect.top.floorToDouble() - 4.0,
|
|
||||||
rect.right.ceilToDouble() + 4.0,
|
|
||||||
rect.bottom.ceilToDouble() + 4.0,
|
|
||||||
);
|
|
||||||
final SemanticsConfiguration configuration = SemanticsConfiguration()
|
|
||||||
..sortKey = OrdinalSortKey(ordinal++)
|
|
||||||
..textDirection = initialDirection
|
|
||||||
..label = info.semanticsLabel ?? info.text;
|
|
||||||
final GestureRecognizer? recognizer = info.recognizer;
|
|
||||||
if (recognizer != null) {
|
|
||||||
if (recognizer is TapGestureRecognizer) {
|
|
||||||
if (recognizer.onTap != null) {
|
|
||||||
configuration.onTap = recognizer.onTap;
|
|
||||||
configuration.isLink = true;
|
|
||||||
}
|
|
||||||
} else if (recognizer is DoubleTapGestureRecognizer) {
|
|
||||||
if (recognizer.onDoubleTap != null) {
|
|
||||||
configuration.onTap = recognizer.onDoubleTap;
|
|
||||||
configuration.isLink = true;
|
|
||||||
}
|
|
||||||
} else if (recognizer is LongPressGestureRecognizer) {
|
|
||||||
if (recognizer.onLongPress != null) {
|
|
||||||
configuration.onLongPress = recognizer.onLongPress;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
assert(false, '${recognizer.runtimeType} is not supported.');
|
|
||||||
}
|
}
|
||||||
|
child = childAfter(child!);
|
||||||
|
placeholderIndex += 1;
|
||||||
|
} else {
|
||||||
|
final TextDirection initialDirection = currentDirection;
|
||||||
|
final List<ui.TextBox> rects = _textPainter.getBoxesForSelection(selection);
|
||||||
|
if (rects.isEmpty) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Rect rect = rects.first.toRect();
|
||||||
|
currentDirection = rects.first.direction;
|
||||||
|
for (final ui.TextBox textBox in rects.skip(1)) {
|
||||||
|
rect = rect.expandToInclude(textBox.toRect());
|
||||||
|
currentDirection = textBox.direction;
|
||||||
|
}
|
||||||
|
// Any of the text boxes may have had infinite dimensions.
|
||||||
|
// We shouldn't pass infinite dimensions up to the bridges.
|
||||||
|
rect = Rect.fromLTWH(
|
||||||
|
math.max(0.0, rect.left),
|
||||||
|
math.max(0.0, rect.top),
|
||||||
|
math.min(rect.width, constraints.maxWidth),
|
||||||
|
math.min(rect.height, constraints.maxHeight),
|
||||||
|
);
|
||||||
|
// Round the current rectangle to make this API testable and add some
|
||||||
|
// padding so that the accessibility rects do not overlap with the text.
|
||||||
|
currentRect = Rect.fromLTRB(
|
||||||
|
rect.left.floorToDouble() - 4.0,
|
||||||
|
rect.top.floorToDouble() - 4.0,
|
||||||
|
rect.right.ceilToDouble() + 4.0,
|
||||||
|
rect.bottom.ceilToDouble() + 4.0,
|
||||||
|
);
|
||||||
|
final SemanticsConfiguration configuration = SemanticsConfiguration()
|
||||||
|
..sortKey = OrdinalSortKey(ordinal++)
|
||||||
|
..textDirection = initialDirection
|
||||||
|
..label = info.semanticsLabel ?? info.text;
|
||||||
|
final GestureRecognizer? recognizer = info.recognizer;
|
||||||
|
if (recognizer != null) {
|
||||||
|
if (recognizer is TapGestureRecognizer) {
|
||||||
|
if (recognizer.onTap != null) {
|
||||||
|
configuration.onTap = recognizer.onTap;
|
||||||
|
configuration.isLink = true;
|
||||||
|
}
|
||||||
|
} else if (recognizer is DoubleTapGestureRecognizer) {
|
||||||
|
if (recognizer.onDoubleTap != null) {
|
||||||
|
configuration.onTap = recognizer.onDoubleTap;
|
||||||
|
configuration.isLink = true;
|
||||||
|
}
|
||||||
|
} else if (recognizer is LongPressGestureRecognizer) {
|
||||||
|
if (recognizer.onLongPress != null) {
|
||||||
|
configuration.onLongPress = recognizer.onLongPress;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(false, '${recognizer.runtimeType} is not supported.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
|
||||||
|
? _cachedChildNodes!.removeFirst()
|
||||||
|
: SemanticsNode();
|
||||||
|
newChild
|
||||||
|
..updateWith(config: configuration)
|
||||||
|
..rect = currentRect;
|
||||||
|
newChildCache.addLast(newChild);
|
||||||
|
newChildren.add(newChild);
|
||||||
}
|
}
|
||||||
final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
|
|
||||||
? _cachedChildNodes!.removeFirst()
|
|
||||||
: SemanticsNode();
|
|
||||||
newChild
|
|
||||||
..updateWith(config: configuration)
|
|
||||||
..rect = currentRect;
|
|
||||||
newChildCache.addLast(newChild);
|
|
||||||
newChildren.add(newChild);
|
|
||||||
}
|
}
|
||||||
_cachedChildNodes = newChildCache;
|
_cachedChildNodes = newChildCache;
|
||||||
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
|
node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
|
||||||
@ -3052,6 +3098,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
redepthChild(foregroundChild);
|
redepthChild(foregroundChild);
|
||||||
if (backgroundChild != null)
|
if (backgroundChild != null)
|
||||||
redepthChild(backgroundChild);
|
redepthChild(backgroundChild);
|
||||||
|
super.redepthChildren();
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -3062,6 +3109,7 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
visitor(foregroundChild);
|
visitor(foregroundChild);
|
||||||
if (backgroundChild != null)
|
if (backgroundChild != null)
|
||||||
visitor(backgroundChild);
|
visitor(backgroundChild);
|
||||||
|
super.visitChildren(visitor);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool get _isMultiline => maxLines != 1;
|
bool get _isMultiline => maxLines != 1;
|
||||||
@ -3268,14 +3316,49 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
@override
|
@override
|
||||||
@protected
|
@protected
|
||||||
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
||||||
|
// Hit test text spans.
|
||||||
|
bool hitText = false;
|
||||||
final Offset effectivePosition = position - _paintOffset;
|
final Offset effectivePosition = position - _paintOffset;
|
||||||
final TextPosition textPosition = _textPainter.getPositionForOffset(effectivePosition);
|
final TextPosition textPosition = _textPainter.getPositionForOffset(effectivePosition);
|
||||||
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
|
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
|
||||||
if (span != null && span is HitTestTarget) {
|
if (span != null && span is HitTestTarget) {
|
||||||
result.add(HitTestEntry(span as HitTestTarget));
|
result.add(HitTestEntry(span as HitTestTarget));
|
||||||
return true;
|
hitText = true;
|
||||||
}
|
}
|
||||||
return false;
|
|
||||||
|
// 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
late TapGestureRecognizer _tap;
|
late TapGestureRecognizer _tap;
|
||||||
@ -3532,6 +3615,82 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
return TextSelection(baseOffset: line.start, extentOffset: line.end);
|
return TextSelection(baseOffset: line.start, extentOffset: line.end);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Placeholder dimensions representing the sizes of child inline widgets.
|
||||||
|
//
|
||||||
|
// These need to be cached because the text painter's placeholder dimensions
|
||||||
|
// will be overwritten during intrinsic width/height calculations and must be
|
||||||
|
// restored to the original values before final layout and painting.
|
||||||
|
List<PlaceholderDimensions>? _placeholderDimensions;
|
||||||
|
|
||||||
|
// 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, growable: false);
|
||||||
|
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!,
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
baselineOffset = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(_placeholderSpans[childIndex].alignment != ui.PlaceholderAlignment.baseline);
|
||||||
|
childSize = child.getDryLayout(boxConstraints);
|
||||||
|
}
|
||||||
|
placeholderDimensions[childIndex] = PlaceholderDimensions(
|
||||||
|
size: childSize,
|
||||||
|
alignment: _placeholderSpans[childIndex].alignment,
|
||||||
|
baseline: _placeholderSpans[childIndex].baseline,
|
||||||
|
baselineOffset: baselineOffset,
|
||||||
|
);
|
||||||
|
child = childAfter(child);
|
||||||
|
childIndex += 1;
|
||||||
|
}
|
||||||
|
return placeholderDimensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setParentData() {
|
||||||
|
RenderBox? child = firstChild;
|
||||||
|
int childIndex = 0;
|
||||||
|
while (child != null && childIndex < _textPainter.inlinePlaceholderBoxes!.length) {
|
||||||
|
final TextParentData textParentData = child.parentData! as TextParentData;
|
||||||
|
textParentData.offset = Offset(
|
||||||
|
_textPainter.inlinePlaceholderBoxes![childIndex].left,
|
||||||
|
_textPainter.inlinePlaceholderBoxes![childIndex].top,
|
||||||
|
);
|
||||||
|
textParentData.scale = _textPainter.inlinePlaceholderScales![childIndex];
|
||||||
|
child = childAfter(child);
|
||||||
|
childIndex += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
|
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
|
||||||
assert(maxWidth != null && minWidth != null);
|
assert(maxWidth != null && minWidth != null);
|
||||||
if (_textLayoutLastMaxWidth == maxWidth && _textLayoutLastMinWidth == minWidth)
|
if (_textLayoutLastMaxWidth == maxWidth && _textLayoutLastMinWidth == minWidth)
|
||||||
@ -3592,8 +3751,34 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Size computeDryLayout(BoxConstraints constraints) {
|
Size computeDryLayout(BoxConstraints constraints) {
|
||||||
|
if (!_canComputeDryLayout()) {
|
||||||
|
assert(debugCannotComputeDryLayout(
|
||||||
|
reason: 'Dry layout not available for alignments that require baseline.',
|
||||||
|
));
|
||||||
|
return Size.zero;
|
||||||
|
}
|
||||||
|
_textPainter.setPlaceholderDimensions(_layoutChildren(constraints, dry: true));
|
||||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||||
final double width = forceLine ? constraints.maxWidth : constraints
|
final double width = forceLine ? constraints.maxWidth : constraints
|
||||||
.constrainWidth(_textPainter.size.width + _caretMargin);
|
.constrainWidth(_textPainter.size.width + _caretMargin);
|
||||||
@ -3603,7 +3788,10 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
@override
|
@override
|
||||||
void performLayout() {
|
void performLayout() {
|
||||||
final BoxConstraints constraints = this.constraints;
|
final BoxConstraints constraints = this.constraints;
|
||||||
|
_placeholderDimensions = _layoutChildren(constraints);
|
||||||
|
_textPainter.setPlaceholderDimensions(_placeholderDimensions);
|
||||||
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
|
||||||
|
_setParentData();
|
||||||
_computeCaretPrototype();
|
_computeCaretPrototype();
|
||||||
// We grab _textPainter.size here because assigning to `size` on the next
|
// We grab _textPainter.size here because assigning to `size` on the next
|
||||||
// line will trigger us to validate our intrinsic sizes, which will change
|
// line will trigger us to validate our intrinsic sizes, which will change
|
||||||
@ -3739,6 +3927,31 @@ class RenderEditable extends RenderBox with RelayoutWhenSystemFontsChangeMixin {
|
|||||||
|
|
||||||
_textPainter.paint(context.canvas, effectiveOffset);
|
_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;
|
||||||
|
}
|
||||||
|
|
||||||
if (foregroundChild != null)
|
if (foregroundChild != null)
|
||||||
context.paintChild(foregroundChild, offset);
|
context.paintChild(foregroundChild, offset);
|
||||||
}
|
}
|
||||||
|
@ -18,7 +18,7 @@ import 'object.dart';
|
|||||||
|
|
||||||
const String _kEllipsis = '\u2026';
|
const String _kEllipsis = '\u2026';
|
||||||
|
|
||||||
/// Parent data for use with [RenderParagraph].
|
/// Parent data for use with [RenderParagraph] and [RenderEditable].
|
||||||
class TextParentData extends ContainerBoxParentData<RenderBox> {
|
class TextParentData extends ContainerBoxParentData<RenderBox> {
|
||||||
/// The scaling of the text.
|
/// The scaling of the text.
|
||||||
double? scale;
|
double? scale;
|
||||||
@ -434,14 +434,12 @@ class RenderParagraph extends RenderBox
|
|||||||
@override
|
@override
|
||||||
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
bool hitTestChildren(BoxHitTestResult result, { required Offset position }) {
|
||||||
// Hit test text spans.
|
// Hit test text spans.
|
||||||
late final bool hitText;
|
bool hitText = false;
|
||||||
final TextPosition textPosition = _textPainter.getPositionForOffset(position);
|
final TextPosition textPosition = _textPainter.getPositionForOffset(position);
|
||||||
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
|
final InlineSpan? span = _textPainter.text!.getSpanForPosition(textPosition);
|
||||||
if (span != null && span is HitTestTarget) {
|
if (span != null && span is HitTestTarget) {
|
||||||
result.add(HitTestEntry(span as HitTestTarget));
|
result.add(HitTestEntry(span as HitTestTarget));
|
||||||
hitText = true;
|
hitText = true;
|
||||||
} else {
|
|
||||||
hitText = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hit test render object children
|
// Hit test render object children
|
||||||
@ -545,16 +543,14 @@ class RenderParagraph extends RenderBox
|
|||||||
);
|
);
|
||||||
childSize = child.size;
|
childSize = child.size;
|
||||||
switch (_placeholderSpans[childIndex].alignment) {
|
switch (_placeholderSpans[childIndex].alignment) {
|
||||||
case ui.PlaceholderAlignment.baseline: {
|
case ui.PlaceholderAlignment.baseline:
|
||||||
baselineOffset = child.getDistanceToBaseline(
|
baselineOffset = child.getDistanceToBaseline(
|
||||||
_placeholderSpans[childIndex].baseline!,
|
_placeholderSpans[childIndex].baseline!,
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
default:
|
||||||
default: {
|
|
||||||
baselineOffset = null;
|
baselineOffset = null;
|
||||||
break;
|
break;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
assert(_placeholderSpans[childIndex].alignment != ui.PlaceholderAlignment.baseline);
|
assert(_placeholderSpans[childIndex].alignment != ui.PlaceholderAlignment.baseline);
|
||||||
@ -597,14 +593,12 @@ class RenderParagraph extends RenderBox
|
|||||||
switch (span.alignment) {
|
switch (span.alignment) {
|
||||||
case ui.PlaceholderAlignment.baseline:
|
case ui.PlaceholderAlignment.baseline:
|
||||||
case ui.PlaceholderAlignment.aboveBaseline:
|
case ui.PlaceholderAlignment.aboveBaseline:
|
||||||
case ui.PlaceholderAlignment.belowBaseline: {
|
case ui.PlaceholderAlignment.belowBaseline:
|
||||||
return false;
|
return false;
|
||||||
}
|
|
||||||
case ui.PlaceholderAlignment.top:
|
case ui.PlaceholderAlignment.top:
|
||||||
case ui.PlaceholderAlignment.middle:
|
case ui.PlaceholderAlignment.middle:
|
||||||
case ui.PlaceholderAlignment.bottom: {
|
case ui.PlaceholderAlignment.bottom:
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
@ -31,6 +31,7 @@ import 'text.dart';
|
|||||||
import 'text_editing_action.dart';
|
import 'text_editing_action.dart';
|
||||||
import 'text_selection.dart';
|
import 'text_selection.dart';
|
||||||
import 'ticker_provider.dart';
|
import 'ticker_provider.dart';
|
||||||
|
import 'widget_span.dart';
|
||||||
|
|
||||||
export 'package:flutter/services.dart' show SelectionChangedCause, TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
|
export 'package:flutter/services.dart' show SelectionChangedCause, TextEditingValue, TextSelection, TextInputType, SmartQuotesType, SmartDashesType;
|
||||||
|
|
||||||
@ -197,9 +198,8 @@ class TextEditingController extends ValueNotifier<TextEditingValue> {
|
|||||||
if (!value.isComposingRangeValid || !withComposing) {
|
if (!value.isComposingRangeValid || !withComposing) {
|
||||||
return TextSpan(style: style, text: text);
|
return TextSpan(style: style, text: text);
|
||||||
}
|
}
|
||||||
final TextStyle composingStyle = style!.merge(
|
final TextStyle composingStyle = style?.merge(const TextStyle(decoration: TextDecoration.underline))
|
||||||
const TextStyle(decoration: TextDecoration.underline),
|
?? const TextStyle(decoration: TextDecoration.underline);
|
||||||
);
|
|
||||||
return TextSpan(
|
return TextSpan(
|
||||||
style: style,
|
style: style,
|
||||||
children: <TextSpan>[
|
children: <TextSpan>[
|
||||||
@ -2651,7 +2651,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
key: _editableKey,
|
key: _editableKey,
|
||||||
startHandleLayerLink: _startHandleLayerLink,
|
startHandleLayerLink: _startHandleLayerLink,
|
||||||
endHandleLayerLink: _endHandleLayerLink,
|
endHandleLayerLink: _endHandleLayerLink,
|
||||||
textSpan: buildTextSpan(),
|
inlineSpan: buildTextSpan(),
|
||||||
value: _value,
|
value: _value,
|
||||||
cursorColor: _cursorColor,
|
cursorColor: _cursorColor,
|
||||||
backgroundCursorColor: widget.backgroundCursorColor,
|
backgroundCursorColor: widget.backgroundCursorColor,
|
||||||
@ -2730,10 +2730,10 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _Editable extends LeafRenderObjectWidget {
|
class _Editable extends MultiChildRenderObjectWidget {
|
||||||
const _Editable({
|
_Editable({
|
||||||
Key? key,
|
Key? key,
|
||||||
required this.textSpan,
|
required this.inlineSpan,
|
||||||
required this.value,
|
required this.value,
|
||||||
required this.startHandleLayerLink,
|
required this.startHandleLayerLink,
|
||||||
required this.endHandleLayerLink,
|
required this.endHandleLayerLink,
|
||||||
@ -2778,9 +2778,22 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
required this.clipBehavior,
|
required this.clipBehavior,
|
||||||
}) : assert(textDirection != null),
|
}) : assert(textDirection != null),
|
||||||
assert(rendererIgnoresPointer != null),
|
assert(rendererIgnoresPointer != null),
|
||||||
super(key: key);
|
super(key: key, children: _extractChildren(inlineSpan));
|
||||||
|
|
||||||
final TextSpan textSpan;
|
// Traverses the InlineSpan tree and depth-first collects the list of
|
||||||
|
// child widgets that are created in WidgetSpans.
|
||||||
|
static List<Widget> _extractChildren(InlineSpan span) {
|
||||||
|
final List<Widget> result = <Widget>[];
|
||||||
|
span.visitChildren((InlineSpan span) {
|
||||||
|
if (span is WidgetSpan) {
|
||||||
|
result.add(span.child);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
final InlineSpan inlineSpan;
|
||||||
final TextEditingValue value;
|
final TextEditingValue value;
|
||||||
final Color? cursorColor;
|
final Color? cursorColor;
|
||||||
final LayerLink startHandleLayerLink;
|
final LayerLink startHandleLayerLink;
|
||||||
@ -2827,7 +2840,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
@override
|
@override
|
||||||
RenderEditable createRenderObject(BuildContext context) {
|
RenderEditable createRenderObject(BuildContext context) {
|
||||||
return RenderEditable(
|
return RenderEditable(
|
||||||
text: textSpan,
|
text: inlineSpan,
|
||||||
cursorColor: cursorColor,
|
cursorColor: cursorColor,
|
||||||
startHandleLayerLink: startHandleLayerLink,
|
startHandleLayerLink: startHandleLayerLink,
|
||||||
endHandleLayerLink: endHandleLayerLink,
|
endHandleLayerLink: endHandleLayerLink,
|
||||||
@ -2872,7 +2885,7 @@ class _Editable extends LeafRenderObjectWidget {
|
|||||||
@override
|
@override
|
||||||
void updateRenderObject(BuildContext context, RenderEditable renderObject) {
|
void updateRenderObject(BuildContext context, RenderEditable renderObject) {
|
||||||
renderObject
|
renderObject
|
||||||
..text = textSpan
|
..text = inlineSpan
|
||||||
..cursorColor = cursorColor
|
..cursorColor = cursorColor
|
||||||
..startHandleLayerLink = startHandleLayerLink
|
..startHandleLayerLink = startHandleLayerLink
|
||||||
..endHandleLayerLink = endHandleLayerLink
|
..endHandleLayerLink = endHandleLayerLink
|
||||||
|
@ -1056,12 +1056,12 @@ void main() {
|
|||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
String editText = findRenderEditable(tester).text!.text!;
|
String editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
||||||
expect(editText.substring(editText.length - 1), newChar);
|
expect(editText.substring(editText.length - 1), newChar);
|
||||||
|
|
||||||
await tester.pump(const Duration(seconds: 2));
|
await tester.pump(const Duration(seconds: 2));
|
||||||
|
|
||||||
editText = findRenderEditable(tester).text!.text!;
|
editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
||||||
expect(editText.substring(editText.length - 1), '\u2022');
|
expect(editText.substring(editText.length - 1), '\u2022');
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{ TargetPlatform.iOS, TargetPlatform.android }));
|
||||||
|
|
||||||
@ -1096,7 +1096,7 @@ void main() {
|
|||||||
|
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
final String editText = findRenderEditable(tester).text!.text!;
|
final String editText = (findRenderEditable(tester).text! as TextSpan).text!;
|
||||||
expect(editText.substring(editText.length - 1), '\u2022');
|
expect(editText.substring(editText.length - 1), '\u2022');
|
||||||
}, variant: const TargetPlatformVariant(<TargetPlatform>{
|
}, variant: const TargetPlatformVariant(<TargetPlatform>{
|
||||||
TargetPlatform.macOS,
|
TargetPlatform.macOS,
|
||||||
|
@ -3212,11 +3212,11 @@ void main() {
|
|||||||
pumpFrame(phase: EnginePhase.paint);
|
pumpFrame(phase: EnginePhase.paint);
|
||||||
expect(currentPainter.paintCount, 1);
|
expect(currentPainter.paintCount, 1);
|
||||||
|
|
||||||
editable.foregroundPainter = currentPainter = _TestRenderEditablePainter()..repaint = false;
|
editable.foregroundPainter = (currentPainter = _TestRenderEditablePainter()..repaint = false);
|
||||||
pumpFrame(phase: EnginePhase.paint);
|
pumpFrame(phase: EnginePhase.paint);
|
||||||
expect(currentPainter.paintCount, 0);
|
expect(currentPainter.paintCount, 0);
|
||||||
|
|
||||||
editable.foregroundPainter = currentPainter = _TestRenderEditablePainter()..repaint = true;
|
editable.foregroundPainter = (currentPainter = _TestRenderEditablePainter()..repaint = true);
|
||||||
pumpFrame(phase: EnginePhase.paint);
|
pumpFrame(phase: EnginePhase.paint);
|
||||||
expect(currentPainter.paintCount, 1);
|
expect(currentPainter.paintCount, 1);
|
||||||
});
|
});
|
||||||
@ -3231,11 +3231,11 @@ void main() {
|
|||||||
pumpFrame(phase: EnginePhase.paint);
|
pumpFrame(phase: EnginePhase.paint);
|
||||||
expect(currentPainter.paintCount, 1);
|
expect(currentPainter.paintCount, 1);
|
||||||
|
|
||||||
editable.painter = currentPainter = _TestRenderEditablePainter()..repaint = false;
|
editable.painter = (currentPainter = _TestRenderEditablePainter()..repaint = false);
|
||||||
pumpFrame(phase: EnginePhase.paint);
|
pumpFrame(phase: EnginePhase.paint);
|
||||||
expect(currentPainter.paintCount, 0);
|
expect(currentPainter.paintCount, 0);
|
||||||
|
|
||||||
editable.painter = currentPainter = _TestRenderEditablePainter()..repaint = true;
|
editable.painter = (currentPainter = _TestRenderEditablePainter()..repaint = true);
|
||||||
pumpFrame(phase: EnginePhase.paint);
|
pumpFrame(phase: EnginePhase.paint);
|
||||||
expect(currentPainter.paintCount, 1);
|
expect(currentPainter.paintCount, 1);
|
||||||
});
|
});
|
||||||
@ -3547,6 +3547,387 @@ void main() {
|
|||||||
expect(textEditingValue.composing, const TextRange(start: 2, end: 5));
|
expect(textEditingValue.composing, const TextRange(start: 2, end: 5));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('WidgetSpan support', () {
|
||||||
|
test('able to render basic WidgetSpan', () async {
|
||||||
|
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||||||
|
..textEditingValue = const TextEditingValue(
|
||||||
|
text: 'test',
|
||||||
|
selection: TextSelection.collapsed(offset: 3),
|
||||||
|
);
|
||||||
|
final List<RenderBox> renderBoxes = <RenderBox>[
|
||||||
|
RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
|
||||||
|
];
|
||||||
|
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||||||
|
final RenderEditable editable = RenderEditable(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionColor: Colors.black,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
offset: viewportOffset,
|
||||||
|
textSelectionDelegate: delegate,
|
||||||
|
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||||||
|
startHandleLayerLink: LayerLink(),
|
||||||
|
endHandleLayerLink: LayerLink(),
|
||||||
|
text: TextSpan(
|
||||||
|
style: const TextStyle(
|
||||||
|
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||||
|
),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
const TextSpan(text: 'test'),
|
||||||
|
WidgetSpan(child: Container(width: 10, height: 10, color: Colors.blue)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selection: const TextSelection.collapsed(offset: 3),
|
||||||
|
children: renderBoxes,
|
||||||
|
);
|
||||||
|
|
||||||
|
layout(editable);
|
||||||
|
editable.hasFocus = true;
|
||||||
|
pumpFrame();
|
||||||
|
|
||||||
|
final Rect composingRect = editable.getRectForComposingRange(const TextRange(start: 4, end: 5))!;
|
||||||
|
expect(composingRect, const Rect.fromLTRB(40.0, 0.0, 54.0, 14.0));
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||||||
|
|
||||||
|
test('able to render multiple WidgetSpans', () async {
|
||||||
|
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||||||
|
..textEditingValue = const TextEditingValue(
|
||||||
|
text: 'test',
|
||||||
|
selection: TextSelection.collapsed(offset: 3),
|
||||||
|
);
|
||||||
|
final List<RenderBox> renderBoxes = <RenderBox>[
|
||||||
|
RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'c'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'd'), textDirection: TextDirection.ltr),
|
||||||
|
];
|
||||||
|
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||||||
|
final RenderEditable editable = RenderEditable(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionColor: Colors.black,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
offset: viewportOffset,
|
||||||
|
textSelectionDelegate: delegate,
|
||||||
|
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||||||
|
startHandleLayerLink: LayerLink(),
|
||||||
|
endHandleLayerLink: LayerLink(),
|
||||||
|
text: TextSpan(
|
||||||
|
style: const TextStyle(
|
||||||
|
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||||
|
),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
const TextSpan(text: 'test'),
|
||||||
|
WidgetSpan(child: Container(width: 10, height: 10, color: Colors.blue)),
|
||||||
|
WidgetSpan(child: Container(width: 10, height: 10, color: Colors.blue)),
|
||||||
|
WidgetSpan(child: Container(width: 10, height: 10, color: Colors.blue)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selection: const TextSelection.collapsed(offset: 3),
|
||||||
|
children: renderBoxes,
|
||||||
|
);
|
||||||
|
|
||||||
|
layout(editable);
|
||||||
|
editable.hasFocus = true;
|
||||||
|
pumpFrame();
|
||||||
|
|
||||||
|
final Rect composingRect = editable.getRectForComposingRange(const TextRange(start: 4, end: 7))!;
|
||||||
|
expect(composingRect, const Rect.fromLTRB(40.0, 0.0, 82.0, 14.0));
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||||||
|
|
||||||
|
test('able to render WidgetSpans with line wrap', () async {
|
||||||
|
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||||||
|
..textEditingValue = const TextEditingValue(
|
||||||
|
text: 'test',
|
||||||
|
selection: TextSelection.collapsed(offset: 3),
|
||||||
|
);
|
||||||
|
final List<RenderBox> renderBoxes = <RenderBox>[
|
||||||
|
RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'c'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'd'), textDirection: TextDirection.ltr),
|
||||||
|
];
|
||||||
|
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||||||
|
final RenderEditable editable = RenderEditable(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionColor: Colors.black,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
offset: viewportOffset,
|
||||||
|
textSelectionDelegate: delegate,
|
||||||
|
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||||||
|
startHandleLayerLink: LayerLink(),
|
||||||
|
endHandleLayerLink: LayerLink(),
|
||||||
|
text: const TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||||
|
),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(text: 'test'),
|
||||||
|
WidgetSpan(child: Text('b')),
|
||||||
|
WidgetSpan(child: Text('c')),
|
||||||
|
WidgetSpan(child: Text('d')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selection: const TextSelection.collapsed(offset: 3),
|
||||||
|
maxLines: 2,
|
||||||
|
minLines: 2,
|
||||||
|
children: renderBoxes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force a line wrap
|
||||||
|
layout(editable, constraints: const BoxConstraints(maxWidth: 75));
|
||||||
|
editable.hasFocus = true;
|
||||||
|
pumpFrame();
|
||||||
|
|
||||||
|
Rect composingRect = editable.getRectForComposingRange(const TextRange(start: 4, end: 6))!;
|
||||||
|
expect(composingRect, const Rect.fromLTRB(40.0, 0.0, 68.0, 14.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 6, end: 7))!;
|
||||||
|
expect(composingRect, const Rect.fromLTRB(0.0, 14.0, 14.0, 28.0));
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||||||
|
|
||||||
|
test('able to render WidgetSpans with line wrap alternating spans', () async {
|
||||||
|
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||||||
|
..textEditingValue = const TextEditingValue(
|
||||||
|
text: 'test',
|
||||||
|
selection: TextSelection.collapsed(offset: 3),
|
||||||
|
);
|
||||||
|
final List<RenderBox> renderBoxes = <RenderBox>[
|
||||||
|
RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'c'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'd'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'e'), textDirection: TextDirection.ltr),
|
||||||
|
];
|
||||||
|
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||||||
|
final RenderEditable editable = RenderEditable(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionColor: Colors.black,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
offset: viewportOffset,
|
||||||
|
textSelectionDelegate: delegate,
|
||||||
|
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||||||
|
startHandleLayerLink: LayerLink(),
|
||||||
|
endHandleLayerLink: LayerLink(),
|
||||||
|
text: const TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||||
|
),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(text: 'test'),
|
||||||
|
WidgetSpan(child: Text('b')),
|
||||||
|
WidgetSpan(child: Text('c')),
|
||||||
|
WidgetSpan(child: Text('d')),
|
||||||
|
TextSpan(text: 'HI'),
|
||||||
|
WidgetSpan(child: Text('e')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selection: const TextSelection.collapsed(offset: 3),
|
||||||
|
maxLines: 2,
|
||||||
|
minLines: 2,
|
||||||
|
children: renderBoxes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force a line wrap
|
||||||
|
layout(editable, constraints: const BoxConstraints(maxWidth: 75));
|
||||||
|
editable.hasFocus = true;
|
||||||
|
pumpFrame();
|
||||||
|
|
||||||
|
Rect composingRect = editable.getRectForComposingRange(const TextRange(start: 4, end: 6))!;
|
||||||
|
expect(composingRect, const Rect.fromLTRB(40.0, 0.0, 68.0, 14.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 6, end: 7))!;
|
||||||
|
expect(composingRect, const Rect.fromLTRB(0.0, 14.0, 14.0, 28.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 7, end: 8))!; // H
|
||||||
|
expect(composingRect, const Rect.fromLTRB(14.0, 18.0, 24.0, 28.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 8, end: 9))!; // I
|
||||||
|
expect(composingRect, const Rect.fromLTRB(24.0, 18.0, 34.0, 28.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 9, end: 10))!;
|
||||||
|
expect(composingRect, const Rect.fromLTRB(34.0, 14.0, 48.0, 28.0));
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||||||
|
|
||||||
|
test('able to render WidgetSpans nested spans', () async {
|
||||||
|
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||||||
|
..textEditingValue = const TextEditingValue(
|
||||||
|
text: 'test',
|
||||||
|
selection: TextSelection.collapsed(offset: 3),
|
||||||
|
);
|
||||||
|
final List<RenderBox> renderBoxes = <RenderBox>[
|
||||||
|
RenderParagraph(const TextSpan(text: 'a'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'c'), textDirection: TextDirection.ltr),
|
||||||
|
];
|
||||||
|
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||||||
|
final RenderEditable editable = RenderEditable(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionColor: Colors.black,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
offset: viewportOffset,
|
||||||
|
textSelectionDelegate: delegate,
|
||||||
|
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||||||
|
startHandleLayerLink: LayerLink(),
|
||||||
|
endHandleLayerLink: LayerLink(),
|
||||||
|
text: const TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||||
|
),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(text: 'test'),
|
||||||
|
WidgetSpan(child: Text('a')),
|
||||||
|
TextSpan(children: <InlineSpan>[
|
||||||
|
WidgetSpan(child: Text('b')),
|
||||||
|
WidgetSpan(child: Text('c')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selection: const TextSelection.collapsed(offset: 3),
|
||||||
|
maxLines: 2,
|
||||||
|
minLines: 2,
|
||||||
|
children: renderBoxes,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Force a line wrap
|
||||||
|
layout(editable, constraints: const BoxConstraints(maxWidth: 75));
|
||||||
|
editable.hasFocus = true;
|
||||||
|
pumpFrame();
|
||||||
|
|
||||||
|
Rect? composingRect = editable.getRectForComposingRange(const TextRange(start: 4, end: 5));
|
||||||
|
expect(composingRect, const Rect.fromLTRB(40.0, 0.0, 54.0, 14.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 5, end: 6));
|
||||||
|
expect(composingRect, const Rect.fromLTRB(54.0, 0.0, 68.0, 14.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 6, end: 7));
|
||||||
|
expect(composingRect, const Rect.fromLTRB(0.0, 14.0, 14.0, 28.0));
|
||||||
|
composingRect = editable.getRectForComposingRange(const TextRange(start: 7, end: 8));
|
||||||
|
expect(composingRect, null);
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61021
|
||||||
|
|
||||||
|
test('can compute IntrinsicWidth for WidgetSpans', () {
|
||||||
|
// 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';
|
||||||
|
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||||||
|
..textEditingValue = const TextEditingValue(
|
||||||
|
text: 'test',
|
||||||
|
selection: TextSelection.collapsed(offset: 3),
|
||||||
|
);
|
||||||
|
final List<RenderBox> renderBoxes = <RenderBox>[
|
||||||
|
RenderParagraph(const TextSpan(text: sentence), textDirection: TextDirection.ltr),
|
||||||
|
];
|
||||||
|
final ViewportOffset viewportOffset = ViewportOffset.zero();
|
||||||
|
final RenderEditable editable = RenderEditable(
|
||||||
|
backgroundCursorColor: Colors.grey,
|
||||||
|
selectionColor: Colors.black,
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
cursorColor: Colors.red,
|
||||||
|
offset: viewportOffset,
|
||||||
|
textSelectionDelegate: delegate,
|
||||||
|
onSelectionChanged: (TextSelection selection, RenderEditable renderObject, SelectionChangedCause cause) {},
|
||||||
|
startHandleLayerLink: LayerLink(),
|
||||||
|
endHandleLayerLink: LayerLink(),
|
||||||
|
text: const TextSpan(
|
||||||
|
style: TextStyle(
|
||||||
|
height: 1.0, fontSize: 10.0, fontFamily: 'Ahem',
|
||||||
|
),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(text: 'test'),
|
||||||
|
WidgetSpan(child: Text('a')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
selection: const TextSelection.collapsed(offset: 3),
|
||||||
|
maxLines: 2,
|
||||||
|
minLines: 2,
|
||||||
|
textScaleFactor: 2.0,
|
||||||
|
children: renderBoxes,
|
||||||
|
);
|
||||||
|
layout(editable, constraints: const BoxConstraints(maxWidth: screenWidth));
|
||||||
|
editable.hasFocus = true;
|
||||||
|
final double maxIntrinsicWidth = editable.computeMaxIntrinsicWidth(fixedHeight);
|
||||||
|
pumpFrame();
|
||||||
|
|
||||||
|
expect(maxIntrinsicWidth, 278);
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
|
||||||
|
|
||||||
|
test('hits correct WidgetSpan when not scrolled', () {
|
||||||
|
final TextSelectionDelegate delegate = FakeEditableTextState()
|
||||||
|
..textEditingValue = const TextEditingValue(
|
||||||
|
text: 'test',
|
||||||
|
selection: TextSelection.collapsed(offset: 3),
|
||||||
|
);
|
||||||
|
final List<RenderBox> renderBoxes = <RenderBox>[
|
||||||
|
RenderParagraph(const TextSpan(text: 'a'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'b'), textDirection: TextDirection.ltr),
|
||||||
|
RenderParagraph(const TextSpan(text: 'c'), textDirection: TextDirection.ltr),
|
||||||
|
];
|
||||||
|
final RenderEditable editable = RenderEditable(
|
||||||
|
text: const TextSpan(
|
||||||
|
style: TextStyle(height: 1.0, fontSize: 10.0, fontFamily: 'Ahem'),
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(text: 'test'),
|
||||||
|
WidgetSpan(child: Text('a')),
|
||||||
|
TextSpan(children: <InlineSpan>[
|
||||||
|
WidgetSpan(child: Text('b')),
|
||||||
|
WidgetSpan(child: Text('c')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
startHandleLayerLink: LayerLink(),
|
||||||
|
endHandleLayerLink: LayerLink(),
|
||||||
|
textDirection: TextDirection.ltr,
|
||||||
|
offset: ViewportOffset.fixed(0.0),
|
||||||
|
textSelectionDelegate: delegate,
|
||||||
|
selection: const TextSelection.collapsed(
|
||||||
|
offset: 0,
|
||||||
|
),
|
||||||
|
children: renderBoxes,
|
||||||
|
);
|
||||||
|
layout(editable, constraints: BoxConstraints.loose(const Size(500.0, 500.0)));
|
||||||
|
// Prepare for painting after layout.
|
||||||
|
pumpFrame(phase: EnginePhase.compositingBits);
|
||||||
|
BoxHitTestResult result = BoxHitTestResult();
|
||||||
|
editable.hitTest(result, position: Offset.zero);
|
||||||
|
// We expect two hit test entries in the path because the RenderEditable
|
||||||
|
// will add itself as well.
|
||||||
|
expect(result.path, hasLength(2));
|
||||||
|
HitTestTarget target = result.path.first.target;
|
||||||
|
expect(target, isA<TextSpan>());
|
||||||
|
expect((target as TextSpan).text, 'test');
|
||||||
|
// Only testing the RenderEditable entry here once, not anymore below.
|
||||||
|
expect(result.path.last.target, isA<RenderEditable>());
|
||||||
|
result = BoxHitTestResult();
|
||||||
|
editable.hitTest(result, position: const Offset(15.0, 0.0));
|
||||||
|
expect(result.path, hasLength(2));
|
||||||
|
target = result.path.first.target;
|
||||||
|
expect(target, isA<TextSpan>());
|
||||||
|
expect((target as TextSpan).text, 'test');
|
||||||
|
|
||||||
|
result = BoxHitTestResult();
|
||||||
|
editable.hitTest(result, position: const Offset(41.0, 0.0));
|
||||||
|
expect(result.path, hasLength(3));
|
||||||
|
target = result.path.first.target;
|
||||||
|
expect(target, isA<TextSpan>());
|
||||||
|
expect((target as TextSpan).text, 'a');
|
||||||
|
|
||||||
|
result = BoxHitTestResult();
|
||||||
|
editable.hitTest(result, position: const Offset(55.0, 0.0));
|
||||||
|
expect(result.path, hasLength(3));
|
||||||
|
target = result.path.first.target;
|
||||||
|
expect(target, isA<TextSpan>());
|
||||||
|
expect((target as TextSpan).text, 'b');
|
||||||
|
|
||||||
|
result = BoxHitTestResult();
|
||||||
|
editable.hitTest(result, position: const Offset(69.0, 5.0));
|
||||||
|
expect(result.path, hasLength(3));
|
||||||
|
target = result.path.first.target;
|
||||||
|
expect(target, isA<TextSpan>());
|
||||||
|
expect((target as TextSpan).text, 'c');
|
||||||
|
|
||||||
|
result = BoxHitTestResult();
|
||||||
|
editable.hitTest(result, position: const Offset(5.0, 15.0));
|
||||||
|
expect(result.path, hasLength(0));
|
||||||
|
}, skip: isBrowser); // https://github.com/flutter/flutter/issues/61020
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
class _TestRenderEditable extends RenderEditable {
|
class _TestRenderEditable extends RenderEditable {
|
||||||
|
@ -2841,7 +2841,7 @@ void main() {
|
|||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
|
||||||
expect(findRenderEditable(tester).text!.text, expectedValue);
|
expect((findRenderEditable(tester).text! as TextSpan).text, expectedValue);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
semantics,
|
semantics,
|
||||||
@ -2906,7 +2906,7 @@ void main() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
final String expectedValue = obscuringCharacter * originalText.length;
|
final String expectedValue = obscuringCharacter * originalText.length;
|
||||||
expect(findRenderEditable(tester).text!.text, expectedValue);
|
expect((findRenderEditable(tester).text! as TextSpan).text, expectedValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
group('a11y copy/cut/paste', () {
|
group('a11y copy/cut/paste', () {
|
||||||
@ -3808,17 +3808,17 @@ void main() {
|
|||||||
|
|
||||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
// The actual text span is split into 3 parts with the middle part underlined.
|
// The actual text span is split into 3 parts with the middle part underlined.
|
||||||
expect(renderEditable.text!.children!.length, 3);
|
expect((renderEditable.text! as TextSpan).children!.length, 3);
|
||||||
final TextSpan textSpan = renderEditable.text!.children![1] as TextSpan;
|
final TextSpan textSpan = (renderEditable.text! as TextSpan).children![1] as TextSpan;
|
||||||
expect(textSpan.text, 'composing');
|
expect(textSpan.text, 'composing');
|
||||||
expect(textSpan.style!.decoration, TextDecoration.underline);
|
expect(textSpan.style!.decoration, TextDecoration.underline);
|
||||||
|
|
||||||
focusNode.unfocus();
|
focusNode.unfocus();
|
||||||
await tester.pump();
|
await tester.pump();
|
||||||
|
|
||||||
expect(renderEditable.text!.children, isNull);
|
expect((renderEditable.text! as TextSpan).children, isNull);
|
||||||
// Everything's just formatted the same way now.
|
// Everything's just formated the same way now.
|
||||||
expect(renderEditable.text!.text, 'text composing text');
|
expect((renderEditable.text! as TextSpan).text, 'text composing text');
|
||||||
expect(renderEditable.text!.style!.decoration, isNull);
|
expect(renderEditable.text!.style!.decoration, isNull);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -6137,7 +6137,7 @@ void main() {
|
|||||||
));
|
));
|
||||||
|
|
||||||
final RenderEditable renderEditable = findRenderEditable(tester);
|
final RenderEditable renderEditable = findRenderEditable(tester);
|
||||||
final TextSpan textSpan = renderEditable.text!;
|
final TextSpan textSpan = renderEditable.text! as TextSpan;
|
||||||
expect(textSpan.style!.color, color);
|
expect(textSpan.style!.color, color);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
Loading…
x
Reference in New Issue
Block a user