diff --git a/packages/flutter/lib/src/semantics/semantics.dart b/packages/flutter/lib/src/semantics/semantics.dart index 1934aa2b80..2df357ee82 100644 --- a/packages/flutter/lib/src/semantics/semantics.dart +++ b/packages/flutter/lib/src/semantics/semantics.dart @@ -2092,8 +2092,7 @@ class SemanticsNode with DiagnosticableTreeMixin { /// Visits the immediate children of this node. /// /// This function calls visitor for each immediate child until visitor returns - /// false. Returns true if all the visitor calls returned true, otherwise - /// returns false. + /// false. void visitChildren(SemanticsNodeVisitor visitor) { if (_children != null) { for (final SemanticsNode child in _children!) { diff --git a/packages/flutter/lib/src/widgets/semantics_debugger.dart b/packages/flutter/lib/src/widgets/semantics_debugger.dart index 937e89d001..97d73e3967 100644 --- a/packages/flutter/lib/src/widgets/semantics_debugger.dart +++ b/packages/flutter/lib/src/widgets/semantics_debugger.dart @@ -202,7 +202,7 @@ class _SemanticsDebuggerPainter extends CustomPainter { canvas.save(); canvas.scale(1.0 / devicePixelRatio, 1.0 / devicePixelRatio); if (rootNode != null) { - _paint(canvas, rootNode, _findDepth(rootNode)); + _paint(canvas, rootNode, _findDepth(rootNode), 0, 0); } if (pointerPosition != null) { final Paint paint = Paint(); @@ -332,14 +332,14 @@ class _SemanticsDebuggerPainter extends CustomPainter { return childrenDepth + 1; } - void _paint(Canvas canvas, SemanticsNode node, int rank) { + void _paint(Canvas canvas, SemanticsNode node, int rank, int indexInParent, int level) { canvas.save(); if (node.transform != null) { canvas.transform(node.transform!.storage); } final Rect rect = node.rect; if (!rect.isEmpty) { - final Color lineColor = Color(0xFF000000 + math.Random(node.id).nextInt(0xFFFFFF)); + final Color lineColor = _colorForNode(indexInParent, level); final Rect innerRect = rect.deflate(rank * 1.0); if (innerRect.isEmpty) { final Paint fill = Paint() @@ -361,13 +361,31 @@ class _SemanticsDebuggerPainter extends CustomPainter { } if (!node.mergeAllDescendantsIntoThisNode) { final int childRank = rank - 1; + final int childLevel = level + 1; + int childIndex = 0; node.visitChildren((SemanticsNode child) { - _paint(canvas, child, childRank); + _paint(canvas, child, childRank, childIndex, childLevel); + childIndex += 1; return true; }); } canvas.restore(); } + + static Color _colorForNode(int index, int level) { + return HSLColor.fromAHSL( + 1.0, + // Use custom hash to ensure stable value regardless of Dart changes + 360.0 * math.Random(_getColorSeed(index, level)).nextDouble(), + 1.0, + 0.7, + ).toColor(); + } + + static int _getColorSeed(int level, int index) { + // Should be no collision as long as children number < 10000. + return level * 10000 + index; + } } /// A widget ignores pointer event but still keeps semantics actions. diff --git a/packages/flutter/test/widgets/semantics_debugger_test.dart b/packages/flutter/test/widgets/semantics_debugger_test.dart index 47b8243300..7edcffd50d 100644 --- a/packages/flutter/test/widgets/semantics_debugger_test.dart +++ b/packages/flutter/test/widgets/semantics_debugger_test.dart @@ -61,6 +61,26 @@ void main() { expect(true, isTrue); // expect that we reach here without crashing }); + testWidgets('SemanticsDebugger draw persistent color based on structure', (WidgetTester tester) async { + await tester.pumpWidget( + Directionality( + textDirection: TextDirection.ltr, + child: SemanticsDebugger( + child: Stack( + children: [ + Semantics( + container: true, + child: Semantics(container: true), + ), + ], + ), + ), + ), + ); + + expect(find.byType(SemanticsDebugger), paints..rect()..rect(color: const Color(0xFFF866FF))); + }); + testWidgets('SemanticsDebugger reparents subtree', (WidgetTester tester) async { final GlobalKey key = GlobalKey();