Unverified Commit 1444e772 authored by Michael Goderbauer's avatar Michael Goderbauer Committed by GitHub

Use stable IDs for TextSpan SemanticsNodes (#52769)

parent 542feb47
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
import 'dart:collection';
import 'dart:math' as math; import 'dart:math' as math;
import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment, TextHeightBehavior; import 'dart:ui' as ui show Gradient, Shader, TextBox, PlaceholderAlignment, TextHeightBehavior;
...@@ -844,6 +845,12 @@ class RenderParagraph extends RenderBox ...@@ -844,6 +845,12 @@ class RenderParagraph extends RenderBox
} }
} }
// Caches [SemanticsNode]s created during [assembleSemanticsNode] so they
// can be re-used when [assembleSemanticsNode] is called again. This ensures
// stable ids for the [SemanticsNode]s of [TextSpan]s across
// [assembleSemanticsNode] invocations.
Queue<SemanticsNode> _cachedChildNodes;
@override @override
void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) { void assembleSemanticsNode(SemanticsNode node, SemanticsConfiguration config, Iterable<SemanticsNode> children) {
assert(_semanticsInfo != null && _semanticsInfo.isNotEmpty); assert(_semanticsInfo != null && _semanticsInfo.isNotEmpty);
...@@ -854,6 +861,7 @@ class RenderParagraph extends RenderBox ...@@ -854,6 +861,7 @@ class RenderParagraph extends RenderBox
int start = 0; int start = 0;
int placeholderIndex = 0; int placeholderIndex = 0;
RenderBox child = firstChild; RenderBox child = firstChild;
final Queue<SemanticsNode> newChildCache = Queue<SemanticsNode>();
for (final InlineSpanSemanticsInformation info in _combineSemanticsInfo()) { for (final InlineSpanSemanticsInformation info in _combineSemanticsInfo()) {
final TextDirection initialDirection = currentDirection; final TextDirection initialDirection = currentDirection;
final TextSelection selection = TextSelection( final TextSelection selection = TextSelection(
...@@ -914,17 +922,27 @@ class RenderParagraph extends RenderBox ...@@ -914,17 +922,27 @@ class RenderParagraph extends RenderBox
assert(false); assert(false);
} }
} }
newChildren.add( final SemanticsNode newChild = (_cachedChildNodes?.isNotEmpty == true)
SemanticsNode() ? _cachedChildNodes.removeFirst()
..updateWith(config: configuration) : SemanticsNode();
..rect = currentRect, newChild
); ..updateWith(config: configuration)
..rect = currentRect;
newChildCache.addLast(newChild);
newChildren.add(newChild);
} }
start += info.text.length; start += info.text.length;
} }
_cachedChildNodes = newChildCache;
node.updateWith(config: config, childrenInInversePaintOrder: newChildren); node.updateWith(config: config, childrenInInversePaintOrder: newChildren);
} }
@override
void clearSemantics() {
super.clearSemantics();
_cachedChildNodes = null;
}
@override @override
List<DiagnosticsNode> debugDescribeChildren() { List<DiagnosticsNode> debugDescribeChildren() {
return <DiagnosticsNode>[ return <DiagnosticsNode>[
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:flutter/rendering.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'semantics_tester.dart';
void main() {
testWidgets('SemanticsNode ids are stable', (WidgetTester tester) async {
// Regression test for b/151732341.
final SemanticsTester semantics = SemanticsTester(tester);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Text.rich(
TextSpan(
text: 'Hallo ',
recognizer: TapGestureRecognizer()..onTap = () {},
children: <TextSpan>[
TextSpan(
text: 'Welt ',
recognizer: TapGestureRecognizer()..onTap = () {},
),
TextSpan(
text: '!!!',
recognizer: TapGestureRecognizer()..onTap = () {},
),
],
),
),
));
expect(find.text('Hallo Welt !!!'), findsOneWidget);
final SemanticsNode node = tester.getSemantics(find.text('Hallo Welt !!!'));
final Map<String, int> labelToNodeId = <String, int>{};
node.visitChildren((SemanticsNode node) {
labelToNodeId[node.label] = node.id;
return true;
});
expect(node.id, 1);
expect(labelToNodeId['Hallo '], 2);
expect(labelToNodeId['Welt '], 3);
expect(labelToNodeId['!!!'], 4);
expect(labelToNodeId.length, 3);
// Rebuild semantics.
tester.renderObject(find.text('Hallo Welt !!!')).markNeedsSemanticsUpdate();
await tester.pump();
final SemanticsNode nodeAfterRebuild = tester.getSemantics(find.text('Hallo Welt !!!'));
final Map<String, int> labelToNodeIdAfterRebuild = <String, int>{};
nodeAfterRebuild.visitChildren((SemanticsNode node) {
labelToNodeIdAfterRebuild[node.label] = node.id;
return true;
});
// Node IDs are stable.
expect(nodeAfterRebuild.id, node.id);
expect(labelToNodeIdAfterRebuild['Hallo '], labelToNodeId['Hallo ']);
expect(labelToNodeIdAfterRebuild['Welt '], labelToNodeId['Welt ']);
expect(labelToNodeIdAfterRebuild['!!!'], labelToNodeId['!!!']);
expect(labelToNodeIdAfterRebuild.length, 3);
// Remove one node.
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Text.rich(
TextSpan(
text: 'Hallo ',
recognizer: TapGestureRecognizer()..onTap = () {},
children: <TextSpan>[
TextSpan(
text: 'Welt ',
recognizer: TapGestureRecognizer()..onTap = () {},
),
],
),
),
));
final SemanticsNode nodeAfterRemoval = tester.getSemantics(find.text('Hallo Welt '));
final Map<String, int> labelToNodeIdAfterRemoval = <String, int>{};
nodeAfterRemoval.visitChildren((SemanticsNode node) {
labelToNodeIdAfterRemoval[node.label] = node.id;
return true;
});
// Node IDs are stable.
expect(nodeAfterRemoval.id, node.id);
expect(labelToNodeIdAfterRemoval['Hallo '], labelToNodeId['Hallo ']);
expect(labelToNodeIdAfterRemoval['Welt '], labelToNodeId['Welt ']);
expect(labelToNodeIdAfterRemoval.length, 2);
await tester.pumpWidget(Directionality(
textDirection: TextDirection.ltr,
child: Text.rich(
TextSpan(
text: 'Hallo ',
recognizer: TapGestureRecognizer()..onTap = () {},
children: <TextSpan>[
TextSpan(
text: 'Welt ',
recognizer: TapGestureRecognizer()..onTap = () {},
),
TextSpan(
text: '!!!',
recognizer: TapGestureRecognizer()..onTap = () {},
),
],
),
),
));
expect(find.text('Hallo Welt !!!'), findsOneWidget);
final SemanticsNode nodeAfterAddition = tester.getSemantics(find.text('Hallo Welt !!!'));
final Map<String, int> labelToNodeIdAfterAddition = <String, int>{};
nodeAfterAddition.visitChildren((SemanticsNode node) {
labelToNodeIdAfterAddition[node.label] = node.id;
return true;
});
// New node gets a new ID.
expect(nodeAfterAddition.id, node.id);
expect(labelToNodeIdAfterAddition['Hallo '], labelToNodeId['Hallo ']);
expect(labelToNodeIdAfterAddition['Welt '], labelToNodeId['Welt ']);
expect(labelToNodeIdAfterAddition['!!!'], isNot(labelToNodeId['!!!']));
expect(labelToNodeIdAfterAddition['!!!'], isNotNull);
expect(labelToNodeIdAfterAddition.length, 3);
semantics.dispose();
});
}
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