Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions lib/src/marks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ abstract final class DpadMarks {
static final Expando<bool> entry = Expando<bool>('dpad.entry');

/// Returns the geometry of [node] in global coordinates, or `null` when
/// the node is not attached to a laid-out render object.
/// the node is not attached to a laid-out render object or its geometry is
/// not finite.
static Rect? rectOf(FocusNode node) {
final BuildContext? context = node.context;
if (context == null || !context.mounted) {
Expand All @@ -24,10 +25,14 @@ abstract final class DpadMarks {
!renderObject.hasSize) {
return null;
}
return MatrixUtils.transformRect(
final rect = MatrixUtils.transformRect(
renderObject.getTransformTo(null),
Offset.zero & renderObject.size,
);
// A non-finite rect is not usable geometry: an ancestor transform can go
// non-finite for a frame (an overscroll stretch driven by a fling is the
// usual case), and every caller already treats `null` as "no rect yet".
return rect.isFinite ? rect : null;
}

/// Whether [node] can be focused right now.
Expand Down
7 changes: 6 additions & 1 deletion lib/src/root.dart
Original file line number Diff line number Diff line change
Expand Up @@ -557,7 +557,12 @@ class _DpadDebugOverlayState extends State<_DpadDebugOverlay>
final Rect? rect = (node == null || node is FocusScopeNode)
? null
: DpadMarks.rectOf(node);
if (node == null || rect == null) {
// The focused rect can be non-finite for a frame (a transform that went
// non-finite mid-animation); rounding its size for the label or
// positioning the outline with it would throw `Infinity or NaN toInt`
// once per frame. Blank this frame instead: a debug tool must never be
// the thing that takes the frame down.
if (node == null || rect == null || !rect.isFinite) {
return const SizedBox.shrink();
}
final DpadRegionState? region = DpadRegion.ofNode(node);
Expand Down
46 changes: 46 additions & 0 deletions test/debug_overlay_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

import 'common.dart';

/// Regression tests for the focus inspector (`debugOverlay: true`).
///
/// They live in their own file because the overlay is a debug tool with a
/// per-frame repaint: the interesting cases are the ones where the rect it
/// reads is not usable geometry yet.
void main() {
testWidgets('a non-finite focused rect blanks the overlay, not the frame',
(tester) async {
// An ancestor transform can go non-finite for a frame — the framework's
// stretch-overscroll effect does exactly that while a fling is in
// flight — and the focused node's global rect then is not finite either.
// The overlay repaints every frame, so rounding or positioning that rect
// used to throw `UnsupportedError: Infinity or NaN toInt` once per frame.
final node = FocusNode(debugLabel: 'hero-button');

await tester.pumpWidget(tvApp(
debugOverlay: true,
// Excluded from semantics on purpose: the non-finite transform makes
// every offset in this subtree non-finite, and the semantics tree
// asserts on those independently of the overlay. This test is about the
// overlay's geometry read, not about the framework's semantics.
home: ExcludeSemantics(
child: Transform(
alignment: Alignment.topLeft,
transform: Matrix4.diagonal3Values(double.infinity, 1.0, 1.0),
child: item('a', node, autofocus: true),
),
),
));
await tester.pump();

expect(node.hasPrimaryFocus, isTrue,
reason: 'the case only means something while a node is focused');
expect(tester.takeException(), isNull,
reason: 'the overlay must not throw for a non-finite rect');

// The ticker repaints the overlay every frame; those must be clean too.
await tester.pump();
expect(tester.takeException(), isNull);
});
}