Commit f11bb25b authored by Adam Barth's avatar Adam Barth Committed by GitHub

SemanticsDebugger should use SemanticsNodes directly (#6252)

Instead of reading the mojom serialization and re-inflating it, the
SemanticsDebugger now shows the SemanticsNode objects directly.
parent bed0300b
......@@ -46,6 +46,52 @@ typedef void SemanticsAnnotator(SemanticsNode semantics);
/// Return false to stop visiting nodes.
typedef bool SemanticsNodeVisitor(SemanticsNode node);
/// Summary information about a [SemanticsNode] object.
///
/// A semantics node might [SemanticsNode.mergeAllDescendantsIntoThisNode],
/// which means the individual fields on the semantics node don't fully describe
/// the semantics at that node. This data structure contains the full semantics
/// for the node.
///
/// Typically obtained from [SemanticsNode.getSemanticsData].
class SemanticsData {
/// Creates a semantics data object.
///
/// The [flags], [actions], [label], and [rect] arguments must not be null.
const SemanticsData({
@required this.flags,
@required this.actions,
@required this.label,
@required this.rect,
this.transform
});
/// A bit field of [SemanticsFlags] that apply to this node.
final int flags;
/// A bit field of [SemanticsActions] that apply to this node.
final int actions;
/// A textual description of this node.
final String label;
/// The bounding box for this node in its coordinate system.
final Rect rect;
/// The transform from this node's coordinate system to its parent's coordinate system.
///
/// By default, the transform is null, which represents the identity
/// transformation (i.e., that this node has the same coorinate system as its
/// parent).
final Matrix4 transform;
/// Whether [flags] contains the given flag.
bool hasFlag(SemanticsFlags flag) => (flags & flag.index) != 0;
/// Whether [actions] contains the given action.
bool hasAction(SemanticsAction action) => (actions & action.index) != 0;
}
/// A node that represents some semantic data.
///
/// The semantics tree is maintained during the semantics phase of the pipeline
......@@ -59,7 +105,7 @@ class SemanticsNode extends AbstractNode {
/// is created.
SemanticsNode({
SemanticsActionHandler handler
}) : _id = _generateNewId(),
}) : id = _generateNewId(),
_actionHandler = handler;
/// Creates a semantic node to represent the root of the semantics tree.
......@@ -68,7 +114,7 @@ class SemanticsNode extends AbstractNode {
SemanticsNode.root({
SemanticsActionHandler handler,
SemanticsOwner owner
}) : _id = 0,
}) : id = 0,
_actionHandler = handler {
attach(owner);
}
......@@ -79,9 +125,13 @@ class SemanticsNode extends AbstractNode {
return _lastIdentifier;
}
final int _id;
final SemanticsActionHandler _actionHandler;
/// The unique identifier for this node.
///
/// The root node has an id of zero. Other nodes are given a unique id when
/// they are created.
final int id;
final SemanticsActionHandler _actionHandler;
// GEOMETRY
// These are automatically handled by RenderObject's own logic
......@@ -151,7 +201,7 @@ class SemanticsNode extends AbstractNode {
addAction(SemanticsAction.decrease);
}
bool _hasAction(SemanticsAction action) {
bool _canPerformAction(SemanticsAction action) {
return _actionHandler != null && (_actions & action.index) != 0;
}
......@@ -257,6 +307,20 @@ class SemanticsNode extends AbstractNode {
bool get hasChildren => _children?.isNotEmpty ?? false;
bool _dead = false;
/// Visits the immediate children of this node.
///
/// This function calls visitor for each child in a pre-order travseral
/// until visitor returns false. Returns true if all the visitor calls
/// returned true, otherwise returns false.
void visitChildren(SemanticsNodeVisitor visitor) {
if (_children != null) {
for (SemanticsNode child in _children) {
if (!visitor(child))
return;
}
}
}
/// Called during the compilation phase after all the children of this node have been compiled.
///
/// This function lets the semantic node respond to all the changes to its
......@@ -323,9 +387,11 @@ class SemanticsNode extends AbstractNode {
}
}
// Visits all the descendants of this node, calling visitor for each one, until
// visitor returns false. Returns true if all the visitor calls returned true,
// otherwise returns false.
/// Visit all the descendants of this node.
///
/// This function calls visitor for each descendant in a pre-order travseral
/// until visitor returns false. Returns true if all the visitor calls
/// returned true, otherwise returns false.
bool _visitDescendants(SemanticsNodeVisitor visitor) {
if (_children != null) {
for (SemanticsNode child in _children) {
......@@ -339,8 +405,8 @@ class SemanticsNode extends AbstractNode {
@override
void attach(SemanticsOwner owner) {
super.attach(owner);
assert(!owner._nodes.containsKey(_id));
owner._nodes[_id] = this;
assert(!owner._nodes.containsKey(id));
owner._nodes[id] = this;
owner._detachedNodes.remove(this);
if (_dirty) {
_dirty = false;
......@@ -356,9 +422,9 @@ class SemanticsNode extends AbstractNode {
@override
void detach() {
assert(owner._nodes.containsKey(_id));
assert(owner._nodes.containsKey(id));
assert(!owner._detachedNodes.contains(this));
owner._nodes.remove(_id);
owner._nodes.remove(id);
owner._detachedNodes.add(this);
super.detach();
if (_children != null) {
......@@ -378,9 +444,42 @@ class SemanticsNode extends AbstractNode {
}
}
/// Returns a summary of the semantics for this node.
///
/// If this node has [mergeAllDescendantsIntoThisNode], then the returned data
/// includes the information from this node's descendants. Otherwise, the
/// returned data matches the data on this node.
SemanticsData getSemanticsData() {
int flags = _flags;
int actions = _actions;
String label = _label;
if (mergeAllDescendantsIntoThisNode) {
_visitDescendants((SemanticsNode node) {
flags |= node._flags;
actions |= node._actions;
if (node.label.isNotEmpty) {
if (label.isEmpty)
label = node.label;
else
label = '$label\n${node.label}';
}
return true;
});
}
return new SemanticsData(
flags: flags,
actions: actions,
label: label,
rect: rect,
transform: transform
);
}
mojom.SemanticsNode _serialize() {
mojom.SemanticsNode result = new mojom.SemanticsNode();
result.id = _id;
result.id = id;
if (_dirty) {
// We could be even more efficient about not sending data here, by only
// sending the bits that are dirty (tracking the geometry, flags, strings,
......@@ -430,7 +529,7 @@ class SemanticsNode extends AbstractNode {
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.write('$runtimeType($_id');
buffer.write('$runtimeType($id');
if (_dirty)
buffer.write(" (${ owner != null && owner._dirtyNodes.contains(this) ? 'dirty' : 'STALE' })");
if (_shouldMergeAllDescendantsIntoThisNode)
......@@ -497,6 +596,11 @@ class SemanticsOwner {
final List<SemanticsListener> _listeners = <SemanticsListener>[];
/// The root node of the semantics tree, if any.
///
/// If the semantics tree is empty, returns null.
SemanticsNode get rootSemanticsNode => _nodes[0];
/// Releases any resources retained by this object.
///
/// Requires that there are no listeners registered with [addListener].
......@@ -597,19 +701,18 @@ class SemanticsOwner {
_dirtyNodes.clear();
}
SemanticsActionHandler _getSemanticsActionHandlerForId(int id, { @required SemanticsAction action }) {
assert(action != null);
SemanticsActionHandler _getSemanticsActionHandlerForId(int id, SemanticsAction action) {
SemanticsNode result = _nodes[id];
if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._hasAction(action)) {
if (result != null && result._shouldMergeAllDescendantsIntoThisNode && !result._canPerformAction(action)) {
result._visitDescendants((SemanticsNode node) {
if (node._actionHandler != null && node._hasAction(action)) {
if (node._canPerformAction(action)) {
result = node;
return false; // found node, abort walk
}
return true; // continue walk
});
}
if (result == null || !result._hasAction(action))
if (result == null || !result._canPerformAction(action))
return null;
return result._actionHandler;
}
......@@ -619,7 +722,51 @@ class SemanticsOwner {
/// If the [SemanticsNode] has not indicated that it can perform the action,
/// this function does nothing.
void performAction(int id, SemanticsAction action) {
SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action: action);
assert(action != null);
SemanticsActionHandler handler = _getSemanticsActionHandlerForId(id, action);
handler?.performAction(action);
}
SemanticsActionHandler _getSemanticsActionHandlerForPosition(SemanticsNode node, Point position, SemanticsAction action) {
if (node.transform != null) {
Matrix4 inverse = new Matrix4.identity();
if (inverse.copyInverse(node.transform) == 0.0)
return null;
position = MatrixUtils.transformPoint(inverse, position);
}
if (!node.rect.contains(position))
return null;
if (node.mergeAllDescendantsIntoThisNode) {
SemanticsNode result;
node._visitDescendants((SemanticsNode child) {
if (child._canPerformAction(action)) {
result = child;
return false;
}
return true;
});
return result?._actionHandler;
}
if (node.hasChildren) {
for (SemanticsNode child in node._children.reversed) {
SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(child, position, action);
if (handler != null)
return handler;
}
}
return node._canPerformAction(action) ? node._actionHandler : null;
}
/// Asks the [SemanticsNode] with at the given position to perform the given action.
///
/// If the [SemanticsNode] has not indicated that it can perform the action,
/// this function does nothing.
void performActionAt(Point position, SemanticsAction action) {
assert(action != null);
final SemanticsNode node = rootSemanticsNode;
if (node == null)
return;
SemanticsActionHandler handler = _getSemanticsActionHandlerForPosition(node, position, action);
handler?.performAction(action);
}
}
......@@ -2,8 +2,8 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:collection';
import 'dart:math' as math;
import 'dart:ui' show SemanticsFlags;
import 'package:flutter/foundation.dart';
import 'package:flutter/scheduler.dart';
......@@ -78,30 +78,60 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
void _handleTap() {
assert(_lastPointerDownLocation != null);
_client._performAction(_lastPointerDownLocation, SemanticsAction.tap);
_performAction(_lastPointerDownLocation, SemanticsAction.tap);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handleLongPress() {
assert(_lastPointerDownLocation != null);
_client._performAction(_lastPointerDownLocation, SemanticsAction.longPress);
_performAction(_lastPointerDownLocation, SemanticsAction.longPress);
setState(() {
_lastPointerDownLocation = null;
});
}
void _handlePanEnd(DragEndDetails details) {
assert(_lastPointerDownLocation != null);
_client.handlePanEnd(_lastPointerDownLocation, details.velocity);
double vx = details.velocity.pixelsPerSecond.dx;
double vy = details.velocity.pixelsPerSecond.dy;
if (vx.abs() == vy.abs())
return;
if (vx.abs() > vy.abs()) {
if (vx.sign < 0) {
_performAction(_lastPointerDownLocation, SemanticsAction.decrease);
_performAction(_lastPointerDownLocation, SemanticsAction.scrollLeft);
} else {
_performAction(_lastPointerDownLocation, SemanticsAction.increase);
_performAction(_lastPointerDownLocation, SemanticsAction.scrollRight);
}
} else {
if (vy.sign < 0)
_performAction(_lastPointerDownLocation, SemanticsAction.scrollUp);
else
_performAction(_lastPointerDownLocation, SemanticsAction.scrollDown);
}
setState(() {
_lastPointerDownLocation = null;
});
}
void _performAction(Point position, SemanticsAction action) {
_pipelineOwner.semanticsOwner?.performActionAt(position, action);
}
// TODO(abarth): This shouldn't be a static. We should get the pipeline owner
// from [context] somehow.
PipelineOwner get _pipelineOwner => WidgetsBinding.instance.pipelineOwner;
@override
Widget build(BuildContext context) {
return new CustomPaint(
foregroundPainter: new _SemanticsDebuggerPainter(_client.generation, _client, _lastPointerDownLocation),
foregroundPainter: new _SemanticsDebuggerPainter(
_pipelineOwner,
_client.generation,
_lastPointerDownLocation
),
child: new GestureDetector(
behavior: HitTestBehavior.opaque,
onTap: _handleTap,
......@@ -121,207 +151,6 @@ class _SemanticsDebuggerState extends State<SemanticsDebugger> {
}
}
typedef bool _SemanticsDebuggerEntryFilter(_SemanticsDebuggerEntry entry);
class _SemanticsDebuggerEntry {
_SemanticsDebuggerEntry(this.id);
final int id;
final Set<SemanticsAction> actions = new Set<SemanticsAction>();
bool hasCheckedState = false;
bool isChecked = false;
String label;
Matrix4 transform;
Rect rect;
List<_SemanticsDebuggerEntry> children;
@override
String toString() {
StringBuffer buffer = new StringBuffer();
buffer.write('_SemanticsDebuggerEntry($id; $rect; "$label"');
for (SemanticsAction action in actions)
buffer.write('; $action');
buffer
..write('${hasCheckedState ? isChecked ? "; checked" : "; unchecked" : ""}')
..write(')');
return buffer.toString();
}
String toStringDeep([ String prefix = '']) {
if (prefix.length > 20)
return '$prefix<ABORTED>\n';
String result = '$prefix$this\n';
prefix += ' ';
for (_SemanticsDebuggerEntry child in children) {
result += '${child.toStringDeep(prefix)}';
}
return result;
}
void updateWith(mojom.SemanticsNode node) {
if (node.flags != null) {
hasCheckedState = node.flags.hasCheckedState;
isChecked = node.flags.isChecked;
}
if (node.actions != null) {
actions.clear();
for (int encodedAction in node.actions)
actions.add(SemanticsAction.values[encodedAction]);
}
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() {
if (children == null || children.isEmpty)
return 1;
return children.map((_SemanticsDebuggerEntry e) => e.findDepth()).reduce((int runningDepth, int nextDepth) {
return math.max(runningDepth, nextDepth);
}) + 1;
}
static const TextStyle textStyles = const TextStyle(
color: const Color(0xFF000000),
fontSize: 10.0,
height: 0.8
);
bool get _isScrollable {
return actions.contains(SemanticsAction.scrollLeft)
|| actions.contains(SemanticsAction.scrollRight)
|| actions.contains(SemanticsAction.scrollUp)
|| actions.contains(SemanticsAction.scrollDown);
}
bool get _isAdjustable {
return actions.contains(SemanticsAction.increase)
|| actions.contains(SemanticsAction.decrease);
}
TextPainter textPainter;
void _updateMessage() {
List<String> annotations = <String>[];
bool wantsTap = false;
if (hasCheckedState) {
annotations.add(isChecked ? 'checked' : 'unchecked');
wantsTap = true;
}
if (actions.contains(SemanticsAction.tap)) {
if (!wantsTap)
annotations.add('button');
} else {
if (wantsTap)
annotations.add('disabled');
}
if (actions.contains(SemanticsAction.longPress))
annotations.add('long-pressable');
if (_isScrollable)
annotations.add('scrollable');
if (_isAdjustable)
annotations.add('adjustable');
String message;
if (annotations.isEmpty) {
assert(label != null);
message = label;
} else {
if (label == '') {
message = annotations.join('; ');
} else {
message = '$label (${annotations.join('; ')})';
}
}
message = message.trim();
if (message != '') {
textPainter ??= new TextPainter();
textPainter
..text = new TextSpan(style: textStyles, text: message)
..textAlign = TextAlign.center
..layout(maxWidth: rect.width);
} else {
textPainter = null;
}
}
void paint(Canvas canvas, int rank) {
canvas.save();
if (transform != null)
canvas.transform(transform.storage);
if (!rect.isEmpty) {
Color lineColor = new Color(0xFF000000 + new math.Random(id).nextInt(0xFFFFFF));
Rect innerRect = rect.deflate(rank * 1.0);
if (innerRect.isEmpty) {
Paint fill = new Paint()
..color = lineColor
..style = PaintingStyle.fill;
canvas.drawRect(rect, fill);
} else {
Paint fill = new Paint()
..color = const Color(0xFFFFFFFF)
..style = PaintingStyle.fill;
canvas.drawRect(rect, fill);
Paint line = new Paint()
..strokeWidth = rank * 2.0
..color = lineColor
..style = PaintingStyle.stroke;
canvas.drawRect(innerRect, line);
}
if (textPainter != null) {
canvas.save();
canvas.clipRect(rect);
textPainter.paint(canvas, rect.topLeft.toOffset());
canvas.restore();
}
}
for (_SemanticsDebuggerEntry child in children)
child.paint(canvas, rank - 1);
canvas.restore();
}
_SemanticsDebuggerEntry hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
if (transform != null) {
Matrix4 invertedTransform = new Matrix4.identity();
double determinant = invertedTransform.copyInverse(transform);
if (determinant == 0.0)
return null;
position = MatrixUtils.transformPoint(invertedTransform, position);
}
if (!rect.contains(position))
return null;
_SemanticsDebuggerEntry result;
for (_SemanticsDebuggerEntry child in children.reversed) {
result = child.hitTest(position, filter);
if (result != null)
break;
}
if (result == null || !filter(result))
result = this;
return result;
}
}
class _SemanticsClient extends ChangeNotifier {
_SemanticsClient(PipelineOwner pipelineOwner) {
_semanticsOwner = pipelineOwner.addSemanticsListener(_updateSemanticsTree);
......@@ -336,92 +165,148 @@ class _SemanticsClient extends ChangeNotifier {
super.dispose();
}
_SemanticsDebuggerEntry get rootNode => _nodes[0];
final Map<int, _SemanticsDebuggerEntry> _nodes = <int, _SemanticsDebuggerEntry>{};
_SemanticsDebuggerEntry _updateNode(mojom.SemanticsNode node) {
final int id = node.id;
_SemanticsDebuggerEntry entry = _nodes.putIfAbsent(id, () => new _SemanticsDebuggerEntry(id));
entry.updateWith(node);
if (node.children != null) {
if (entry.children != null)
entry.children.clear();
else
entry.children = new List<_SemanticsDebuggerEntry>();
for (mojom.SemanticsNode child in node.children)
entry.children.add(_updateNode(child));
}
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;
void _updateSemanticsTree(List<mojom.SemanticsNode> nodes) {
generation += 1;
for (mojom.SemanticsNode node in nodes)
_updateNode(node);
_removeDetachedNodes();
notifyListeners();
}
}
String _getMessage(SemanticsNode node) {
SemanticsData data = node.getSemanticsData();
List<String> annotations = <String>[];
_SemanticsDebuggerEntry _hitTest(Point position, _SemanticsDebuggerEntryFilter filter) {
return rootNode?.hitTest(position, filter);
bool wantsTap = false;
if (data.hasFlag(SemanticsFlags.hasCheckedState)) {
annotations.add(data.hasFlag(SemanticsFlags.isChecked) ? 'checked' : 'unchecked');
wantsTap = true;
}
void _performAction(Point position, SemanticsAction action) {
_SemanticsDebuggerEntry entry = _hitTest(position, (_SemanticsDebuggerEntry entry) => entry.actions.contains(action));
_semanticsOwner.performAction(entry?.id ?? 0, action);
if (data.hasAction(SemanticsAction.tap)) {
if (!wantsTap)
annotations.add('button');
} else {
if (wantsTap)
annotations.add('disabled');
}
void handlePanEnd(Point position, Velocity velocity) {
double vx = velocity.pixelsPerSecond.dx;
double vy = velocity.pixelsPerSecond.dy;
if (vx.abs() == vy.abs())
return;
if (vx.abs() > vy.abs()) {
if (vx.sign < 0) {
_performAction(position, SemanticsAction.decrease);
_performAction(position, SemanticsAction.scrollLeft);
} else {
_performAction(position, SemanticsAction.increase);
_performAction(position, SemanticsAction.scrollRight);
}
if (data.hasAction(SemanticsAction.longPress))
annotations.add('long-pressable');
final bool isScrollable = data.hasAction(SemanticsAction.scrollLeft)
|| data.hasAction(SemanticsAction.scrollRight)
|| data.hasAction(SemanticsAction.scrollUp)
|| data.hasAction(SemanticsAction.scrollDown);
final bool isAdjustable = data.hasAction(SemanticsAction.increase)
|| data.hasAction(SemanticsAction.decrease);
if (isScrollable)
annotations.add('scrollable');
if (isAdjustable)
annotations.add('adjustable');
String message;
if (annotations.isEmpty) {
assert(data.label != null);
message = data.label;
} else {
if (data.label.isEmpty) {
message = annotations.join('; ');
} else {
if (vy.sign < 0)
_performAction(position, SemanticsAction.scrollUp);
else
_performAction(position, SemanticsAction.scrollDown);
message = '${data.label} (${annotations.join('; ')})';
}
}
return message.trim();
}
const TextStyle _messageStyle = const TextStyle(
color: const Color(0xFF000000),
fontSize: 10.0,
height: 0.8
);
void _paintMessage(Canvas canvas, SemanticsNode node) {
String message = _getMessage(node);
if (message.isEmpty)
return;
final Rect rect = node.rect;
canvas.save();
canvas.clipRect(rect);
TextPainter textPainter = new TextPainter()
..text = new TextSpan(style: _messageStyle, text: message)
..layout(maxWidth: rect.width);
textPainter.paint(canvas, FractionalOffset.center.inscribe(textPainter.size, rect).topLeft.toOffset());
canvas.restore();
}
int _findDepth(SemanticsNode node) {
if (!node.hasChildren || node.mergeAllDescendantsIntoThisNode)
return 1;
int childrenDepth = 0;
node.visitChildren((SemanticsNode child) {
childrenDepth = math.max(childrenDepth, _findDepth(child));
return true;
});
return childrenDepth + 1;
}
void _paint(Canvas canvas, SemanticsNode node, int rank) {
canvas.save();
if (node.transform != null)
canvas.transform(node.transform.storage);
Rect rect = node.rect;
if (!rect.isEmpty) {
Color lineColor = new Color(0xFF000000 + new math.Random(node.id).nextInt(0xFFFFFF));
Rect innerRect = rect.deflate(rank * 1.0);
if (innerRect.isEmpty) {
Paint fill = new Paint()
..color = lineColor
..style = PaintingStyle.fill;
canvas.drawRect(rect, fill);
} else {
Paint fill = new Paint()
..color = const Color(0xFFFFFFFF)
..style = PaintingStyle.fill;
canvas.drawRect(rect, fill);
Paint line = new Paint()
..strokeWidth = rank * 2.0
..color = lineColor
..style = PaintingStyle.stroke;
canvas.drawRect(innerRect, line);
}
_paintMessage(canvas, node);
}
if (!node.mergeAllDescendantsIntoThisNode) {
final int childRank = rank - 1;
node.visitChildren((SemanticsNode child) {
_paint(canvas, child, childRank);
return true;
});
}
canvas.restore();
}
class _SemanticsDebuggerPainter extends CustomPainter {
const _SemanticsDebuggerPainter(this.generation, this.client, this.pointerPosition);
const _SemanticsDebuggerPainter(this.owner, this.generation, this.pointerPosition);
final PipelineOwner owner;
final int generation;
final _SemanticsClient client;
final Point pointerPosition;
SemanticsNode get _rootSemanticsNode {
return owner.semanticsOwner?.rootSemanticsNode;
}
@override
void paint(Canvas canvas, Size size) {
_SemanticsDebuggerEntry rootNode = client.rootNode;
rootNode?.paint(canvas, rootNode.findDepth());
final SemanticsNode rootNode = _rootSemanticsNode;
if (rootNode != null)
_paint(canvas, rootNode, _findDepth(rootNode));
if (pointerPosition != null) {
Paint paint = new Paint();
paint.color = const Color(0x7F0090FF);
......@@ -431,8 +316,8 @@ class _SemanticsDebuggerPainter extends CustomPainter {
@override
bool shouldRepaint(_SemanticsDebuggerPainter oldDelegate) {
return generation != oldDelegate.generation
|| client != oldDelegate.client
return owner != oldDelegate.owner
|| generation != oldDelegate.generation
|| pointerPosition != oldDelegate.pointerPosition;
}
}
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