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

Add `InlineSpan.visitDirectChildren` (#125656)

I'd like to find out the `fontSize` of a `PlaceholderSpan`, and currently there doesn't seem to be a way to do `TextStyle` cascading in the framework:

 `InlineSpan.visitChildren` traverses the entire `InlineSpan` tree using a preorder traversal, and nodes that don't have "content" will be skipped (https://master-api.flutter.dev/flutter/painting/InlineSpan/visitChildren.html): 

> Walks this [InlineSpan](https://master-api.flutter.dev/flutter/painting/InlineSpan-class.html) and any descendants in pre-order and calls visitor for each span that has content.

which makes it impossible to do `TextStyle` cascading in the framework: 
- `InlineSpan`s with a non-null `TextStyle` but has no content will be skipped
- `visitChildren` doesn't directly expose the hierarchy, it only gives information about the flattened tree.

This doesn't look like a breaking change, most internal customers are extending `WidgetSpan` which has a concrete implementation of the new method.

Alternatively I could create a fake `ui.ParagraphBuilder` and record the `ui.TextStyle` at the top of the stack when `addPlaceholder` is called. But `ui.TextStyle` properties are not exposed to the framework.
parent 7815699d
......@@ -225,8 +225,31 @@ abstract class InlineSpan extends DiagnosticableTree {
///
/// When `visitor` returns true, the walk will continue. When `visitor` returns
/// false, then the walk will end.
///
/// See also:
///
/// * [visitDirectChildren], which preforms `build`-order traversal on the
/// immediate children of this [InlineSpan], regardless of whether they
/// have content.
bool visitChildren(InlineSpanVisitor visitor);
/// Calls `visitor` for each immediate child of this [InlineSpan].
///
/// The immediate children are visited in the same order they are added to
/// a [ui.ParagraphBuilder] in the [build] method, which is also the logical
/// order of the child [InlineSpan]s in the text.
///
/// The traversal stops when all immediate children are visited, or when the
/// `visitor` callback returns `false` on an immediate child. This method
/// itself returns a `bool` indicating whether the visitor callback returned
/// `true` on all immediate children.
///
/// See also:
///
/// * [visitChildren], which performs preorder traversal on this [InlineSpan]
/// if it has content, and all its descendants with content.
bool visitDirectChildren(InlineSpanVisitor visitor);
/// Returns the [InlineSpan] that contains the given position in the text.
InlineSpan? getSpanForPosition(TextPosition position) {
assert(debugAssertIsValid());
......
......@@ -274,14 +274,13 @@ class _UntilTextBoundary extends TextBoundary {
/// caret's size and position. This is preferred due to the expensive
/// nature of the calculation.
///
// This should be a sealed class: A _CaretMetrics is either a _LineCaretMetrics
// or an _EmptyLineCaretMetrics.
// A _CaretMetrics is either a _LineCaretMetrics or an _EmptyLineCaretMetrics.
@immutable
abstract class _CaretMetrics { }
sealed class _CaretMetrics { }
/// The _CaretMetrics for carets located in a non-empty line. Carets located in a
/// non-empty line are associated with a glyph within the same line.
class _LineCaretMetrics implements _CaretMetrics {
final class _LineCaretMetrics implements _CaretMetrics {
const _LineCaretMetrics({required this.offset, required this.writingDirection, required this.fullHeight});
/// The offset of the top left corner of the caret from the top left
/// corner of the paragraph.
......@@ -294,7 +293,7 @@ class _LineCaretMetrics implements _CaretMetrics {
/// The _CaretMetrics for carets located in an empty line (when the text is
/// empty, or the caret is between two a newline characters).
class _EmptyLineCaretMetrics implements _CaretMetrics {
final class _EmptyLineCaretMetrics implements _CaretMetrics {
const _EmptyLineCaretMetrics({ required this.lineVerticalOffset });
/// The y offset of the unoccupied line.
......@@ -856,12 +855,10 @@ class TextPainter {
/// Valid only after [layout] has been called.
double computeDistanceToActualBaseline(TextBaseline baseline) {
assert(_debugAssertTextLayoutIsValid);
switch (baseline) {
case TextBaseline.alphabetic:
return _paragraph!.alphabeticBaseline;
case TextBaseline.ideographic:
return _paragraph!.ideographicBaseline;
}
return switch (baseline) {
TextBaseline.alphabetic => _paragraph!.alphabeticBaseline,
TextBaseline.ideographic => _paragraph!.ideographicBaseline,
};
}
/// Whether any text was truncated or ellipsized.
......@@ -1144,29 +1141,17 @@ class TextPainter {
}
static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) {
switch (textAlign) {
case TextAlign.left:
return 0.0;
case TextAlign.right:
return 1.0;
case TextAlign.center:
return 0.5;
case TextAlign.start:
case TextAlign.justify:
switch (textDirection) {
case TextDirection.rtl:
return 1.0;
case TextDirection.ltr:
return 0.0;
}
case TextAlign.end:
switch (textDirection) {
case TextDirection.rtl:
return 0.0;
case TextDirection.ltr:
return 1.0;
}
}
return switch ((textAlign, textDirection)) {
(TextAlign.left, _) => 0.0,
(TextAlign.right, _) => 1.0,
(TextAlign.center, _) => 0.5,
(TextAlign.start, TextDirection.ltr) => 0.0,
(TextAlign.start, TextDirection.rtl) => 1.0,
(TextAlign.justify, TextDirection.ltr) => 0.0,
(TextAlign.justify, TextDirection.rtl) => 1.0,
(TextAlign.end, TextDirection.ltr) => 1.0,
(TextAlign.end, TextDirection.rtl) => 0.0,
};
}
/// Returns the offset at which to paint the caret.
......@@ -1181,29 +1166,27 @@ class TextPainter {
caretMetrics = _computeCaretMetrics(position);
}
if (caretMetrics is _EmptyLineCaretMetrics) {
final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
// The full width is not (width - caretPrototype.width)
// because RenderEditable reserves cursor width on the right. Ideally this
// should be handled by RenderEditable instead.
final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * width;
return Offset(dx, caretMetrics.lineVerticalOffset);
}
final Offset offset;
switch ((caretMetrics as _LineCaretMetrics).writingDirection) {
case TextDirection.rtl:
offset = Offset(caretMetrics.offset.dx - caretPrototype.width, caretMetrics.offset.dy);
case TextDirection.ltr:
offset = caretMetrics.offset;
final Offset rawOffset;
switch (caretMetrics) {
case _EmptyLineCaretMetrics(:final double lineVerticalOffset):
final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
// The full width is not (width - caretPrototype.width)
// because RenderEditable reserves cursor width on the right. Ideally this
// should be handled by RenderEditable instead.
final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * width;
return Offset(dx, lineVerticalOffset);
case _LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset):
rawOffset = offset;
case _LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset):
rawOffset = Offset(offset.dx - caretPrototype.width, offset.dy);
}
// If offset.dx is outside of the advertised content area, then the associated
// glyph cluster belongs to a trailing newline character. Ideally the behavior
// should be handled by higher-level implementations (for instance,
// RenderEditable reserves width for showing the caret, it's best to handle
// the clamping there).
final double adjustedDx = clampDouble(offset.dx, 0, width);
return Offset(adjustedDx, offset.dy);
final double adjustedDx = clampDouble(rawOffset.dx, 0, width);
return Offset(adjustedDx, rawOffset.dy);
}
/// {@template flutter.painting.textPainter.getFullHeightForCaret}
......@@ -1216,8 +1199,10 @@ class TextPainter {
// TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495
return null;
}
final _CaretMetrics caretMetrics = _computeCaretMetrics(position);
return caretMetrics is _LineCaretMetrics ? caretMetrics.fullHeight : null;
return switch(_computeCaretMetrics(position)) {
_LineCaretMetrics(:final double fullHeight) => fullHeight,
_EmptyLineCaretMetrics() => null,
};
}
// Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
......@@ -1238,17 +1223,10 @@ class TextPainter {
return _caretMetrics;
}
final int offset = position.offset;
final _CaretMetrics? metrics;
switch (position.affinity) {
case TextAffinity.upstream: {
metrics = _getMetricsFromUpstream(offset) ?? _getMetricsFromDownstream(offset);
break;
}
case TextAffinity.downstream: {
metrics = _getMetricsFromDownstream(offset) ?? _getMetricsFromUpstream(offset);
break;
}
}
final _CaretMetrics? metrics = switch (position.affinity) {
TextAffinity.upstream => _getMetricsFromUpstream(offset) ?? _getMetricsFromDownstream(offset),
TextAffinity.downstream => _getMetricsFromDownstream(offset) ?? _getMetricsFromUpstream(offset),
};
// Cache the input parameters to prevent repeat work later.
_previousCaretPosition = position;
return _caretMetrics = metrics ?? const _EmptyLineCaretMetrics(lineVerticalOffset: 0);
......
......@@ -289,14 +289,12 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
builder.addText('\uFFFD');
}
}
if (children != null) {
for (final InlineSpan child in children!) {
child.build(
builder,
textScaleFactor: textScaleFactor,
dimensions: dimensions,
);
}
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
child.build(
builder,
textScaleFactor: textScaleFactor,
dimensions: dimensions,
);
}
if (hasStyle) {
builder.pop();
......@@ -310,16 +308,22 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
/// returns false, then the walk will end.
@override
bool visitChildren(InlineSpanVisitor visitor) {
if (text != null) {
if (!visitor(this)) {
if (text != null && !visitor(this)) {
return false;
}
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
if (!child.visitChildren(visitor)) {
return false;
}
}
if (children != null) {
for (final InlineSpan child in children!) {
if (!child.visitChildren(visitor)) {
return false;
}
return true;
}
@override
bool visitDirectChildren(InlineSpanVisitor visitor) {
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
if (!visitor(child)) {
return false;
}
}
return true;
......@@ -389,17 +393,15 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
recognizer: recognizer,
));
}
if (children != null) {
for (final InlineSpan child in children!) {
if (child is TextSpan) {
child.computeSemanticsInformation(
collector,
inheritedLocale: effectiveLocale,
inheritedSpellOut: effectiveSpellOut,
);
} else {
child.computeSemanticsInformation(collector);
}
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
if (child is TextSpan) {
child.computeSemanticsInformation(
collector,
inheritedLocale: effectiveLocale,
inheritedSpellOut: effectiveSpellOut,
);
} else {
child.computeSemanticsInformation(collector);
}
}
}
......@@ -426,10 +428,7 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
/// Any [GestureRecognizer]s are added to `semanticsElements`. Null is added to
/// `semanticsElements` for [PlaceholderSpan]s.
void describeSemantics(Accumulator offset, List<int> semanticsOffsets, List<dynamic> semanticsElements) {
if (
recognizer != null &&
(recognizer is TapGestureRecognizer || recognizer is LongPressGestureRecognizer)
) {
if (recognizer is TapGestureRecognizer || recognizer is LongPressGestureRecognizer) {
final int length = semanticsLabel?.length ?? text!.length;
semanticsOffsets.add(offset.value);
semanticsOffsets.add(offset.value + length);
......@@ -573,11 +572,8 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
@override
List<DiagnosticsNode> debugDescribeChildren() {
if (children == null) {
return const <DiagnosticsNode>[];
}
return children!.map<DiagnosticsNode>((InlineSpan child) {
return children?.map<DiagnosticsNode>((InlineSpan child) {
return child.toDiagnosticsNode();
}).toList();
}).toList() ?? const <DiagnosticsNode>[];
}
}
......@@ -494,19 +494,17 @@ class RenderParagraph extends RenderBox
switch (span.alignment) {
case ui.PlaceholderAlignment.baseline:
case ui.PlaceholderAlignment.aboveBaseline:
case ui.PlaceholderAlignment.belowBaseline: {
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: {
case ui.PlaceholderAlignment.bottom:
continue;
}
}
}
return true;
......
......@@ -121,9 +121,10 @@ class WidgetSpan extends PlaceholderSpan {
/// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk.
@override
bool visitChildren(InlineSpanVisitor visitor) {
return visitor(this);
}
bool visitChildren(InlineSpanVisitor visitor) => visitor(this);
@override
bool visitDirectChildren(InlineSpanVisitor visitor) => true;
@override
InlineSpan? getSpanForPositionVisitor(TextPosition position, Accumulator offset) {
......
......@@ -282,6 +282,51 @@ void main() {
expect(collector[0].semanticsLabel, 'bbb');
});
test('TextSpan visitDirectChildren', () {
List<InlineSpan> directChildrenOf(InlineSpan root) {
final List<InlineSpan> visitOrder = <InlineSpan>[];
root.visitDirectChildren((InlineSpan span) {
visitOrder.add(span);
return true;
});
return visitOrder;
}
const TextSpan leaf1 = TextSpan(text: 'leaf1');
const TextSpan leaf2 = TextSpan(text: 'leaf2');
const TextSpan branch1 = TextSpan(children: <InlineSpan>[leaf1, leaf2]);
const TextSpan branch2 = TextSpan(text: 'branch2');
const TextSpan root = TextSpan(children: <InlineSpan>[branch1, branch2]);
expect(directChildrenOf(root), <TextSpan>[branch1, branch2]);
expect(directChildrenOf(branch1), <TextSpan>[leaf1, leaf2]);
expect(directChildrenOf(branch2), isEmpty);
expect(directChildrenOf(leaf1), isEmpty);
expect(directChildrenOf(leaf2), isEmpty);
int? indexInTree(InlineSpan target) {
int index = 0;
bool findInSubtree(InlineSpan subtreeRoot) {
if (identical(target, subtreeRoot)) {
// return false to stop traversal.
return false;
}
index += 1;
return subtreeRoot.visitDirectChildren(findInSubtree);
}
return findInSubtree(root) ? null : index;
}
expect(indexInTree(root), 0);
expect(indexInTree(branch1), 1);
expect(indexInTree(leaf1), 2);
expect(indexInTree(leaf2), 3);
expect(indexInTree(branch2), 4);
expect(indexInTree(const TextSpan(text: 'foobar')), null);
});
testWidgets('handles mouse cursor', (WidgetTester tester) async {
await tester.pumpWidget(
const Directionality(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment