From 96f379e85b1ea7430168fb8f9d839130632ffeb8 Mon Sep 17 00:00:00 2001 From: fkrhzbllh Date: Fri, 9 Oct 2026 09:58:43 +0700 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix(dpad):=20blank=20the=20debug?= =?UTF-8?q?=20overlay=20on=20a=20non-finite=20focus=20rect?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `_DpadDebugOverlayState.build` rounds the focused node's rect for its size label and positions the outline with it. That rect comes from `DpadMarks.rectOf`, which returns whatever the node's global transform maps to — and 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, so with `debugOverlay: true` the overlay then threw `UnsupportedError: Infinity or NaN toInt` once per frame. `rectOf` now returns null for non-finite geometry (every caller already treats null as "no rect yet"), and the overlay blanks the frame instead of rounding or positioning a non-finite rect: the inspector must never be what takes a frame down. `test/debug_overlay_test.dart` pins the regression — a focused node under a non-finite transform, asserting no exception across the per-frame repaints. It failed with the exact `Infinity or NaN toInt` before these changes. 🤖 Generated with Codebuff Co-Authored-By: Codebuff --- lib/src/marks.dart | 9 +++++-- lib/src/root.dart | 7 +++++- test/debug_overlay_test.dart | 46 ++++++++++++++++++++++++++++++++++++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 test/debug_overlay_test.dart diff --git a/lib/src/marks.dart b/lib/src/marks.dart index 06fb775..a663fb4 100644 --- a/lib/src/marks.dart +++ b/lib/src/marks.dart @@ -12,7 +12,8 @@ abstract final class DpadMarks { static final Expando entry = Expando('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) { @@ -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. diff --git a/lib/src/root.dart b/lib/src/root.dart index 16a4503..0519698 100644 --- a/lib/src/root.dart +++ b/lib/src/root.dart @@ -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); diff --git a/test/debug_overlay_test.dart b/test/debug_overlay_test.dart new file mode 100644 index 0000000..22f7d35 --- /dev/null +++ b/test/debug_overlay_test.dart @@ -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); + }); +}