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