Commit 63eedb76 authored by Adam Barth's avatar Adam Barth Committed by GitHub

Semantics debugger shouldn't crash when reparenting nodes (#4782)

Our previous approach to detecting when we needed to remove semantics nodes
didn't account for reparenting.
parent 05bcbb6c
...@@ -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 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
...@@ -127,17 +128,54 @@ class _SemanticsDebuggerEntry { ...@@ -127,17 +128,54 @@ class _SemanticsDebuggerEntry {
'${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}' '${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}'
')'; ')';
} }
String toStringDeep([ String prefix = '']) { String toStringDeep([ String prefix = '']) {
if (prefix.length > 20) if (prefix.length > 20)
return '$prefix<ABORTED>\n'; return '$prefix<ABORTED>\n';
String result = '$prefix$this\n'; String result = '$prefix$this\n';
for (_SemanticsDebuggerEntry child in children.reversed) { prefix += ' ';
prefix += ' '; for (_SemanticsDebuggerEntry child in children) {
result += '${child.toStringDeep(prefix)}'; result += '${child.toStringDeep(prefix)}';
} }
return result; return result;
} }
void updateWith(mojom.SemanticsNode node) {
if (node.flags != null) {
canBeTapped = node.flags.canBeTapped;
canBeLongPressed = node.flags.canBeLongPressed;
canBeScrolledHorizontally = node.flags.canBeScrolledHorizontally;
canBeScrolledVertically = node.flags.canBeScrolledVertically;
hasCheckedState = node.flags.hasCheckedState;
isChecked = node.flags.isChecked;
}
if (node.strings != null) {
assert(node.strings.label != null);
label = node.strings.label;
} else {
assert(label != null);
}
if (node.geometry != null) {
if (node.geometry.transform != null) {
assert(node.geometry.transform.length == 16);
// TODO(ianh): Replace this with a cleaner call once
// https://github.com/google/vector_math.dart/issues/159
// is fixed.
List<double> array = node.geometry.transform;
transform = new Matrix4(
array[0], array[1], array[2], array[3],
array[4], array[5], array[6], array[7],
array[8], array[9], array[10], array[11],
array[12], array[13], array[14], array[15]
);
} else {
transform = null;
}
rect = new Rect.fromLTWH(node.geometry.left, node.geometry.top, node.geometry.width, node.geometry.height);
}
_updateMessage();
}
int findDepth() { int findDepth() {
if (children == null || children.isEmpty) if (children == null || children.isEmpty)
return 1; return 1;
...@@ -153,7 +191,7 @@ class _SemanticsDebuggerEntry { ...@@ -153,7 +191,7 @@ class _SemanticsDebuggerEntry {
); );
TextPainter textPainter; TextPainter textPainter;
void updateMessage() { void _updateMessage() {
List<String> annotations = <String>[]; List<String> annotations = <String>[];
bool wantsTap = false; bool wantsTap = false;
if (hasCheckedState) { if (hasCheckedState) {
...@@ -251,7 +289,7 @@ class _SemanticsDebuggerEntry { ...@@ -251,7 +289,7 @@ class _SemanticsDebuggerEntry {
} }
} }
class _SemanticsDebuggerListener implements mojom.SemanticsListener { class _SemanticsDebuggerListener extends ChangeNotifier implements mojom.SemanticsListener {
_SemanticsDebuggerListener._() { _SemanticsDebuggerListener._() {
SemanticsNode.addListener(this); SemanticsNode.addListener(this);
} }
...@@ -262,66 +300,40 @@ class _SemanticsDebuggerListener implements mojom.SemanticsListener { ...@@ -262,66 +300,40 @@ class _SemanticsDebuggerListener implements mojom.SemanticsListener {
instance ??= new _SemanticsDebuggerListener._(); instance ??= new _SemanticsDebuggerListener._();
} }
Set<VoidCallback> _listeners = new Set<VoidCallback>(); _SemanticsDebuggerEntry get rootNode => _nodes[0];
void addListener(VoidCallback callback) { final Map<int, _SemanticsDebuggerEntry> _nodes = <int, _SemanticsDebuggerEntry>{};
assert(!_listeners.contains(callback));
_listeners.add(callback);
}
void removeListener(VoidCallback callback) {
_listeners.remove(callback);
}
Map<int, _SemanticsDebuggerEntry> nodes = <int, _SemanticsDebuggerEntry>{};
_SemanticsDebuggerEntry _updateNode(mojom.SemanticsNode node) { _SemanticsDebuggerEntry _updateNode(mojom.SemanticsNode node) {
_SemanticsDebuggerEntry entry = nodes.putIfAbsent(node.id, () => new _SemanticsDebuggerEntry(node.id)); final int id = node.id;
if (node.flags != null) { _SemanticsDebuggerEntry entry = _nodes.putIfAbsent(id, () => new _SemanticsDebuggerEntry(id));
entry.canBeTapped = node.flags.canBeTapped; entry.updateWith(node);
entry.canBeLongPressed = node.flags.canBeLongPressed;
entry.canBeScrolledHorizontally = node.flags.canBeScrolledHorizontally;
entry.canBeScrolledVertically = node.flags.canBeScrolledVertically;
entry.hasCheckedState = node.flags.hasCheckedState;
entry.isChecked = node.flags.isChecked;
}
if (node.strings != null) {
assert(node.strings.label != null);
entry.label = node.strings.label;
} else {
assert(entry.label != null);
}
if (node.geometry != null) {
if (node.geometry.transform != null) {
assert(node.geometry.transform.length == 16);
// TODO(ianh): Replace this with a cleaner call once
// https://github.com/google/vector_math.dart/issues/159
// is fixed.
List<double> array = node.geometry.transform;
entry.transform = new Matrix4(
array[0], array[1], array[2], array[3],
array[4], array[5], array[6], array[7],
array[8], array[9], array[10], array[11],
array[12], array[13], array[14], array[15]
);
} else {
entry.transform = null;
}
entry.rect = new Rect.fromLTWH(node.geometry.left, node.geometry.top, node.geometry.width, node.geometry.height);
}
entry.updateMessage();
if (node.children != null) { if (node.children != null) {
Set<_SemanticsDebuggerEntry> oldChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children ?? const <_SemanticsDebuggerEntry>[]); if (entry.children != null)
entry.children?.clear(); entry.children.clear();
entry.children ??= new List<_SemanticsDebuggerEntry>(); else
entry.children = new List<_SemanticsDebuggerEntry>();
for (mojom.SemanticsNode child in node.children) for (mojom.SemanticsNode child in node.children)
entry.children.add(_updateNode(child)); entry.children.add(_updateNode(child));
Set<_SemanticsDebuggerEntry> newChildren = new Set<_SemanticsDebuggerEntry>.from(entry.children);
Set<_SemanticsDebuggerEntry> removedChildren = oldChildren.difference(newChildren);
for (_SemanticsDebuggerEntry oldChild in removedChildren)
nodes.remove(oldChild.id);
} }
return entry; return entry;
} }
void _removeDetachedNodes() {
// TODO(abarth): We should be able to keep this table updated without
// walking the entire tree.
Set<int> detachedNodes = new Set<int>.from(_nodes.keys);
Queue<_SemanticsDebuggerEntry> unvisited = new Queue<_SemanticsDebuggerEntry>();
unvisited.add(rootNode);
while (unvisited.isNotEmpty) {
_SemanticsDebuggerEntry node = unvisited.removeFirst();
detachedNodes.remove(node.id);
if (node.children != null)
unvisited.addAll(node.children);
}
for (int id in detachedNodes)
_nodes.remove(id);
}
int generation = 0; int generation = 0;
@override @override
...@@ -329,12 +341,12 @@ class _SemanticsDebuggerListener implements mojom.SemanticsListener { ...@@ -329,12 +341,12 @@ class _SemanticsDebuggerListener implements mojom.SemanticsListener {
generation += 1; generation += 1;
for (mojom.SemanticsNode node in nodes) for (mojom.SemanticsNode node in nodes)
_updateNode(node); _updateNode(node);
for (VoidCallback listener in _listeners) _removeDetachedNodes();
listener(); notifyListeners();
} }
_SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) { _SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
return nodes[0]?.hitTest(position, filter); return rootNode?.hitTest(position, filter);
} }
void handleTap(Point position) { void handleTap(Point position) {
...@@ -370,10 +382,8 @@ class _SemanticsDebuggerPainter extends CustomPainter { ...@@ -370,10 +382,8 @@ class _SemanticsDebuggerPainter extends CustomPainter {
@override @override
void paint(Canvas canvas, Size size) { void paint(Canvas canvas, Size size) {
_SemanticsDebuggerListener.instance.nodes[0]?.paint( _SemanticsDebuggerEntry rootNode = _SemanticsDebuggerListener.instance.rootNode;
canvas, rootNode?.paint(canvas, rootNode.findDepth());
_SemanticsDebuggerListener.instance.nodes[0].findDepth()
);
if (pointerPosition != null) { if (pointerPosition != null) {
Paint paint = new Paint(); Paint paint = new Paint();
paint.color = const Color(0x7F0090FF); paint.color = const Color(0x7F0090FF);
......
...@@ -7,10 +7,8 @@ import 'package:flutter/widgets.dart'; ...@@ -7,10 +7,8 @@ import 'package:flutter/widgets.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
void main() { void main() {
testWidgets('Semantics 6 - SemanticsDebugger smoke test', (WidgetTester tester) async { testWidgets('SemanticsDebugger smoke test', (WidgetTester tester) async {
// This is a smoketest to verify that adding a debugger doesn't crash. // This is a smoketest to verify that adding a debugger doesn't crash.
await tester.pumpWidget( await tester.pumpWidget(
new Stack( new Stack(
children: <Widget>[ children: <Widget>[
...@@ -42,6 +40,70 @@ void main() { ...@@ -42,6 +40,70 @@ void main() {
); );
expect(true, isTrue); // expect that we reach here without crashing expect(true, isTrue); // expect that we reach here without crashing
});
testWidgets('SemanticsDebugger reparents subtree', (WidgetTester tester) async {
GlobalKey key = new GlobalKey();
await tester.pumpWidget(
new SemanticsDebugger(
child: new Stack(
children: <Widget>[
new Semantics(label: 'label1'),
new Positioned(
key: key, left: 0.0, top: 0.0, width: 100.0, height: 100.0,
child: new Semantics(label: 'label2')
),
]
)
)
);
await tester.pumpWidget(
new SemanticsDebugger(
child: new Stack(
children: <Widget>[
new Semantics(label: 'label1'),
new Semantics(
container: true,
child: new Stack(
children: <Widget>[
new Positioned(
key: key, left: 0.0, top: 0.0, width: 100.0, height: 100.0,
child: new Semantics(label: 'label2')
),
new Semantics(label: 'label3'),
]
)
)
]
)
)
);
await tester.pumpWidget(
new SemanticsDebugger(
child: new Stack(
children: <Widget>[
new Semantics(label: 'label1'),
new Semantics(
container: true,
child: new Stack(
children: <Widget>[
new Positioned(
key: key, left: 0.0, top: 0.0, width: 100.0, height: 100.0,
child: new Semantics(label: 'label2')
),
new Semantics(label: 'label3'),
new Semantics(label: 'label4'),
]
)
)
]
)
)
);
expect(tester.takeException(), isNull);
}); });
} }
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