Skip to content

Commit f704c68

Browse files
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.
1 parent 7815699 commit f704c68

File tree

6 files changed

+147
-106
lines changed

6 files changed

+147
-106
lines changed

packages/flutter/lib/src/painting/inline_span.dart

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,31 @@ abstract class InlineSpan extends DiagnosticableTree {
225225
///
226226
/// When `visitor` returns true, the walk will continue. When `visitor` returns
227227
/// false, then the walk will end.
228+
///
229+
/// See also:
230+
///
231+
/// * [visitDirectChildren], which preforms `build`-order traversal on the
232+
/// immediate children of this [InlineSpan], regardless of whether they
233+
/// have content.
228234
bool visitChildren(InlineSpanVisitor visitor);
229235

236+
/// Calls `visitor` for each immediate child of this [InlineSpan].
237+
///
238+
/// The immediate children are visited in the same order they are added to
239+
/// a [ui.ParagraphBuilder] in the [build] method, which is also the logical
240+
/// order of the child [InlineSpan]s in the text.
241+
///
242+
/// The traversal stops when all immediate children are visited, or when the
243+
/// `visitor` callback returns `false` on an immediate child. This method
244+
/// itself returns a `bool` indicating whether the visitor callback returned
245+
/// `true` on all immediate children.
246+
///
247+
/// See also:
248+
///
249+
/// * [visitChildren], which performs preorder traversal on this [InlineSpan]
250+
/// if it has content, and all its descendants with content.
251+
bool visitDirectChildren(InlineSpanVisitor visitor);
252+
230253
/// Returns the [InlineSpan] that contains the given position in the text.
231254
InlineSpan? getSpanForPosition(TextPosition position) {
232255
assert(debugAssertIsValid());

packages/flutter/lib/src/painting/text_painter.dart

Lines changed: 42 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -274,14 +274,13 @@ class _UntilTextBoundary extends TextBoundary {
274274
/// caret's size and position. This is preferred due to the expensive
275275
/// nature of the calculation.
276276
///
277-
// This should be a sealed class: A _CaretMetrics is either a _LineCaretMetrics
278-
// or an _EmptyLineCaretMetrics.
277+
// A _CaretMetrics is either a _LineCaretMetrics or an _EmptyLineCaretMetrics.
279278
@immutable
280-
abstract class _CaretMetrics { }
279+
sealed class _CaretMetrics { }
281280

282281
/// The _CaretMetrics for carets located in a non-empty line. Carets located in a
283282
/// non-empty line are associated with a glyph within the same line.
284-
class _LineCaretMetrics implements _CaretMetrics {
283+
final class _LineCaretMetrics implements _CaretMetrics {
285284
const _LineCaretMetrics({required this.offset, required this.writingDirection, required this.fullHeight});
286285
/// The offset of the top left corner of the caret from the top left
287286
/// corner of the paragraph.
@@ -294,7 +293,7 @@ class _LineCaretMetrics implements _CaretMetrics {
294293

295294
/// The _CaretMetrics for carets located in an empty line (when the text is
296295
/// empty, or the caret is between two a newline characters).
297-
class _EmptyLineCaretMetrics implements _CaretMetrics {
296+
final class _EmptyLineCaretMetrics implements _CaretMetrics {
298297
const _EmptyLineCaretMetrics({ required this.lineVerticalOffset });
299298

300299
/// The y offset of the unoccupied line.
@@ -856,12 +855,10 @@ class TextPainter {
856855
/// Valid only after [layout] has been called.
857856
double computeDistanceToActualBaseline(TextBaseline baseline) {
858857
assert(_debugAssertTextLayoutIsValid);
859-
switch (baseline) {
860-
case TextBaseline.alphabetic:
861-
return _paragraph!.alphabeticBaseline;
862-
case TextBaseline.ideographic:
863-
return _paragraph!.ideographicBaseline;
864-
}
858+
return switch (baseline) {
859+
TextBaseline.alphabetic => _paragraph!.alphabeticBaseline,
860+
TextBaseline.ideographic => _paragraph!.ideographicBaseline,
861+
};
865862
}
866863

867864
/// Whether any text was truncated or ellipsized.
@@ -1144,29 +1141,17 @@ class TextPainter {
11441141
}
11451142

11461143
static double _computePaintOffsetFraction(TextAlign textAlign, TextDirection textDirection) {
1147-
switch (textAlign) {
1148-
case TextAlign.left:
1149-
return 0.0;
1150-
case TextAlign.right:
1151-
return 1.0;
1152-
case TextAlign.center:
1153-
return 0.5;
1154-
case TextAlign.start:
1155-
case TextAlign.justify:
1156-
switch (textDirection) {
1157-
case TextDirection.rtl:
1158-
return 1.0;
1159-
case TextDirection.ltr:
1160-
return 0.0;
1161-
}
1162-
case TextAlign.end:
1163-
switch (textDirection) {
1164-
case TextDirection.rtl:
1165-
return 0.0;
1166-
case TextDirection.ltr:
1167-
return 1.0;
1168-
}
1169-
}
1144+
return switch ((textAlign, textDirection)) {
1145+
(TextAlign.left, _) => 0.0,
1146+
(TextAlign.right, _) => 1.0,
1147+
(TextAlign.center, _) => 0.5,
1148+
(TextAlign.start, TextDirection.ltr) => 0.0,
1149+
(TextAlign.start, TextDirection.rtl) => 1.0,
1150+
(TextAlign.justify, TextDirection.ltr) => 0.0,
1151+
(TextAlign.justify, TextDirection.rtl) => 1.0,
1152+
(TextAlign.end, TextDirection.ltr) => 1.0,
1153+
(TextAlign.end, TextDirection.rtl) => 0.0,
1154+
};
11701155
}
11711156

11721157
/// Returns the offset at which to paint the caret.
@@ -1181,29 +1166,27 @@ class TextPainter {
11811166
caretMetrics = _computeCaretMetrics(position);
11821167
}
11831168

1184-
if (caretMetrics is _EmptyLineCaretMetrics) {
1185-
final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
1186-
// The full width is not (width - caretPrototype.width)
1187-
// because RenderEditable reserves cursor width on the right. Ideally this
1188-
// should be handled by RenderEditable instead.
1189-
final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * width;
1190-
return Offset(dx, caretMetrics.lineVerticalOffset);
1191-
}
1192-
1193-
final Offset offset;
1194-
switch ((caretMetrics as _LineCaretMetrics).writingDirection) {
1195-
case TextDirection.rtl:
1196-
offset = Offset(caretMetrics.offset.dx - caretPrototype.width, caretMetrics.offset.dy);
1197-
case TextDirection.ltr:
1198-
offset = caretMetrics.offset;
1169+
final Offset rawOffset;
1170+
switch (caretMetrics) {
1171+
case _EmptyLineCaretMetrics(:final double lineVerticalOffset):
1172+
final double paintOffsetAlignment = _computePaintOffsetFraction(textAlign, textDirection!);
1173+
// The full width is not (width - caretPrototype.width)
1174+
// because RenderEditable reserves cursor width on the right. Ideally this
1175+
// should be handled by RenderEditable instead.
1176+
final double dx = paintOffsetAlignment == 0 ? 0 : paintOffsetAlignment * width;
1177+
return Offset(dx, lineVerticalOffset);
1178+
case _LineCaretMetrics(writingDirection: TextDirection.ltr, :final Offset offset):
1179+
rawOffset = offset;
1180+
case _LineCaretMetrics(writingDirection: TextDirection.rtl, :final Offset offset):
1181+
rawOffset = Offset(offset.dx - caretPrototype.width, offset.dy);
11991182
}
12001183
// If offset.dx is outside of the advertised content area, then the associated
12011184
// glyph cluster belongs to a trailing newline character. Ideally the behavior
12021185
// should be handled by higher-level implementations (for instance,
12031186
// RenderEditable reserves width for showing the caret, it's best to handle
12041187
// the clamping there).
1205-
final double adjustedDx = clampDouble(offset.dx, 0, width);
1206-
return Offset(adjustedDx, offset.dy);
1188+
final double adjustedDx = clampDouble(rawOffset.dx, 0, width);
1189+
return Offset(adjustedDx, rawOffset.dy);
12071190
}
12081191

12091192
/// {@template flutter.painting.textPainter.getFullHeightForCaret}
@@ -1216,8 +1199,10 @@ class TextPainter {
12161199
// TODO(LongCatIsLooong): make this case impossible; see https://github.com/flutter/flutter/issues/79495
12171200
return null;
12181201
}
1219-
final _CaretMetrics caretMetrics = _computeCaretMetrics(position);
1220-
return caretMetrics is _LineCaretMetrics ? caretMetrics.fullHeight : null;
1202+
return switch(_computeCaretMetrics(position)) {
1203+
_LineCaretMetrics(:final double fullHeight) => fullHeight,
1204+
_EmptyLineCaretMetrics() => null,
1205+
};
12211206
}
12221207

12231208
// Cached caret metrics. This allows multiple invokes of [getOffsetForCaret] and
@@ -1238,17 +1223,10 @@ class TextPainter {
12381223
return _caretMetrics;
12391224
}
12401225
final int offset = position.offset;
1241-
final _CaretMetrics? metrics;
1242-
switch (position.affinity) {
1243-
case TextAffinity.upstream: {
1244-
metrics = _getMetricsFromUpstream(offset) ?? _getMetricsFromDownstream(offset);
1245-
break;
1246-
}
1247-
case TextAffinity.downstream: {
1248-
metrics = _getMetricsFromDownstream(offset) ?? _getMetricsFromUpstream(offset);
1249-
break;
1250-
}
1251-
}
1226+
final _CaretMetrics? metrics = switch (position.affinity) {
1227+
TextAffinity.upstream => _getMetricsFromUpstream(offset) ?? _getMetricsFromDownstream(offset),
1228+
TextAffinity.downstream => _getMetricsFromDownstream(offset) ?? _getMetricsFromUpstream(offset),
1229+
};
12521230
// Cache the input parameters to prevent repeat work later.
12531231
_previousCaretPosition = position;
12541232
return _caretMetrics = metrics ?? const _EmptyLineCaretMetrics(lineVerticalOffset: 0);

packages/flutter/lib/src/painting/text_span.dart

Lines changed: 31 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -289,14 +289,12 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
289289
builder.addText('\uFFFD');
290290
}
291291
}
292-
if (children != null) {
293-
for (final InlineSpan child in children!) {
294-
child.build(
295-
builder,
296-
textScaleFactor: textScaleFactor,
297-
dimensions: dimensions,
298-
);
299-
}
292+
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
293+
child.build(
294+
builder,
295+
textScaleFactor: textScaleFactor,
296+
dimensions: dimensions,
297+
);
300298
}
301299
if (hasStyle) {
302300
builder.pop();
@@ -310,16 +308,22 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
310308
/// returns false, then the walk will end.
311309
@override
312310
bool visitChildren(InlineSpanVisitor visitor) {
313-
if (text != null) {
314-
if (!visitor(this)) {
311+
if (text != null && !visitor(this)) {
312+
return false;
313+
}
314+
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
315+
if (!child.visitChildren(visitor)) {
315316
return false;
316317
}
317318
}
318-
if (children != null) {
319-
for (final InlineSpan child in children!) {
320-
if (!child.visitChildren(visitor)) {
321-
return false;
322-
}
319+
return true;
320+
}
321+
322+
@override
323+
bool visitDirectChildren(InlineSpanVisitor visitor) {
324+
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
325+
if (!visitor(child)) {
326+
return false;
323327
}
324328
}
325329
return true;
@@ -389,17 +393,15 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
389393
recognizer: recognizer,
390394
));
391395
}
392-
if (children != null) {
393-
for (final InlineSpan child in children!) {
394-
if (child is TextSpan) {
395-
child.computeSemanticsInformation(
396-
collector,
397-
inheritedLocale: effectiveLocale,
398-
inheritedSpellOut: effectiveSpellOut,
399-
);
400-
} else {
401-
child.computeSemanticsInformation(collector);
402-
}
396+
for (final InlineSpan child in children ?? const <InlineSpan>[]) {
397+
if (child is TextSpan) {
398+
child.computeSemanticsInformation(
399+
collector,
400+
inheritedLocale: effectiveLocale,
401+
inheritedSpellOut: effectiveSpellOut,
402+
);
403+
} else {
404+
child.computeSemanticsInformation(collector);
403405
}
404406
}
405407
}
@@ -426,10 +428,7 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
426428
/// Any [GestureRecognizer]s are added to `semanticsElements`. Null is added to
427429
/// `semanticsElements` for [PlaceholderSpan]s.
428430
void describeSemantics(Accumulator offset, List<int> semanticsOffsets, List<dynamic> semanticsElements) {
429-
if (
430-
recognizer != null &&
431-
(recognizer is TapGestureRecognizer || recognizer is LongPressGestureRecognizer)
432-
) {
431+
if (recognizer is TapGestureRecognizer || recognizer is LongPressGestureRecognizer) {
433432
final int length = semanticsLabel?.length ?? text!.length;
434433
semanticsOffsets.add(offset.value);
435434
semanticsOffsets.add(offset.value + length);
@@ -573,11 +572,8 @@ class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotati
573572

574573
@override
575574
List<DiagnosticsNode> debugDescribeChildren() {
576-
if (children == null) {
577-
return const <DiagnosticsNode>[];
578-
}
579-
return children!.map<DiagnosticsNode>((InlineSpan child) {
575+
return children?.map<DiagnosticsNode>((InlineSpan child) {
580576
return child.toDiagnosticsNode();
581-
}).toList();
577+
}).toList() ?? const <DiagnosticsNode>[];
582578
}
583579
}

packages/flutter/lib/src/rendering/paragraph.dart

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -494,19 +494,17 @@ class RenderParagraph extends RenderBox
494494
switch (span.alignment) {
495495
case ui.PlaceholderAlignment.baseline:
496496
case ui.PlaceholderAlignment.aboveBaseline:
497-
case ui.PlaceholderAlignment.belowBaseline: {
497+
case ui.PlaceholderAlignment.belowBaseline:
498498
assert(
499499
RenderObject.debugCheckingIntrinsics,
500500
'Intrinsics are not available for PlaceholderAlignment.baseline, '
501501
'PlaceholderAlignment.aboveBaseline, or PlaceholderAlignment.belowBaseline.',
502502
);
503503
return false;
504-
}
505504
case ui.PlaceholderAlignment.top:
506505
case ui.PlaceholderAlignment.middle:
507-
case ui.PlaceholderAlignment.bottom: {
506+
case ui.PlaceholderAlignment.bottom:
508507
continue;
509-
}
510508
}
511509
}
512510
return true;

packages/flutter/lib/src/widgets/widget_span.dart

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,10 @@ class WidgetSpan extends PlaceholderSpan {
121121

122122
/// Calls `visitor` on this [WidgetSpan]. There are no children spans to walk.
123123
@override
124-
bool visitChildren(InlineSpanVisitor visitor) {
125-
return visitor(this);
126-
}
124+
bool visitChildren(InlineSpanVisitor visitor) => visitor(this);
125+
126+
@override
127+
bool visitDirectChildren(InlineSpanVisitor visitor) => true;
127128

128129
@override
129130
InlineSpan? getSpanForPositionVisitor(TextPosition position, Accumulator offset) {

packages/flutter/test/painting/text_span_test.dart

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,51 @@ void main() {
282282
expect(collector[0].semanticsLabel, 'bbb');
283283
});
284284

285+
test('TextSpan visitDirectChildren', () {
286+
List<InlineSpan> directChildrenOf(InlineSpan root) {
287+
final List<InlineSpan> visitOrder = <InlineSpan>[];
288+
root.visitDirectChildren((InlineSpan span) {
289+
visitOrder.add(span);
290+
return true;
291+
});
292+
return visitOrder;
293+
}
294+
295+
const TextSpan leaf1 = TextSpan(text: 'leaf1');
296+
const TextSpan leaf2 = TextSpan(text: 'leaf2');
297+
298+
const TextSpan branch1 = TextSpan(children: <InlineSpan>[leaf1, leaf2]);
299+
const TextSpan branch2 = TextSpan(text: 'branch2');
300+
301+
const TextSpan root = TextSpan(children: <InlineSpan>[branch1, branch2]);
302+
303+
expect(directChildrenOf(root), <TextSpan>[branch1, branch2]);
304+
expect(directChildrenOf(branch1), <TextSpan>[leaf1, leaf2]);
305+
expect(directChildrenOf(branch2), isEmpty);
306+
expect(directChildrenOf(leaf1), isEmpty);
307+
expect(directChildrenOf(leaf2), isEmpty);
308+
309+
int? indexInTree(InlineSpan target) {
310+
int index = 0;
311+
bool findInSubtree(InlineSpan subtreeRoot) {
312+
if (identical(target, subtreeRoot)) {
313+
// return false to stop traversal.
314+
return false;
315+
}
316+
index += 1;
317+
return subtreeRoot.visitDirectChildren(findInSubtree);
318+
}
319+
return findInSubtree(root) ? null : index;
320+
}
321+
322+
expect(indexInTree(root), 0);
323+
expect(indexInTree(branch1), 1);
324+
expect(indexInTree(leaf1), 2);
325+
expect(indexInTree(leaf2), 3);
326+
expect(indexInTree(branch2), 4);
327+
expect(indexInTree(const TextSpan(text: 'foobar')), null);
328+
});
329+
285330
testWidgets('handles mouse cursor', (WidgetTester tester) async {
286331
await tester.pumpWidget(
287332
const Directionality(

0 commit comments

Comments
 (0)