focus_tree_inspector 0.1.0 copy "focus_tree_inspector: ^0.1.0" to clipboard
focus_tree_inspector: ^0.1.0 copied to clipboard

DevTools extension and bridge for best-effort Flutter focus tree inspection.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:focus_tree_inspector/focus_tree_inspector.dart';
import 'package:focus_tree_example/src/widgets/focus_demo_widgets.dart';

void main() {
  FocusTreeInspector.instance.registerFocusTreeBridge();
  runApp(const MyApp());
}

final class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Focus Tree Example',
      theme: ThemeData(colorScheme: .fromSeed(seedColor: Colors.deepPurple), useMaterial3: true),
      home: const FocusPlaygroundPage(),
    );
  }
}

final class FocusPlaygroundPage extends StatefulWidget {
  const FocusPlaygroundPage({super.key});

  @override
  State<FocusPlaygroundPage> createState() => _FocusPlaygroundPageState();
}

final class _FocusPlaygroundPageState extends State<FocusPlaygroundPage> {
  late final TextEditingController _textController;
  late final FocusNode _textFieldFocusNode;

  @override
  void initState() {
    super.initState();
    _textController = TextEditingController(text: 'Tap tiles in DevTools to focus');
    _textFieldFocusNode = FocusNode(debugLabel: 'TextField');
  }

  @override
  void dispose() {
    _textController.dispose();
    _textFieldFocusNode.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        backgroundColor: Theme.of(context).colorScheme.inversePrimary,
        title: const Text('Focus playground'),
      ),
      body: SafeArea(
        child: SingleChildScrollView(
          padding: const EdgeInsets.all(16),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              const FocusDemoSectionTitle('SingleChildScrollView (this page)'),
              const SizedBox(height: 12),

              FocusDemoSection(
                title: 'TextField (focusNode + keyboard)',
                child: TextField(
                  controller: _textController,
                  focusNode: _textFieldFocusNode,
                  decoration: const InputDecoration(border: OutlineInputBorder(), labelText: 'TextField'),
                ),
              ),
              FocusDemoSection(
                title: 'Horizontal ListView.builder (5 focusable tiles)',
                child: SizedBox(
                  height: 120,
                  child: ListView.builder(
                    scrollDirection: Axis.horizontal,
                    itemCount: 5,
                    itemBuilder: (context, index) {
                      return Padding(
                        padding: const EdgeInsets.only(right: 12),
                        child: SizedBox(
                          width: 96,
                          child: FocusSquareTile(debugLabel: 'HList tile $index', label: '$index', colorSeed: index),
                        ),
                      );
                    },
                  ),
                ),
              ),

              FocusDemoSection(
                title: 'GridView.builder (5 focusable tiles)',
                child: GridView.builder(
                  shrinkWrap: true,
                  physics: const NeverScrollableScrollPhysics(),
                  itemCount: 5,
                  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
                    crossAxisCount: 3,
                    mainAxisSpacing: 12,
                    crossAxisSpacing: 12,
                    childAspectRatio: 1,
                  ),
                  itemBuilder: (context, index) {
                    return FocusSquareTile(debugLabel: 'Grid tile $index', label: '$index', colorSeed: index + 10);
                  },
                ),
              ),

              FocusDemoSection(
                title: 'Buttons in a row',
                child: Row(
                  children: const [
                    Expanded(
                      child: FocusLabeledButton(debugLabel: 'Row button 0', label: 'Row 0'),
                    ),
                    SizedBox(width: 12),
                    Expanded(
                      child: FocusLabeledButton(debugLabel: 'Row button 1', label: 'Row 1'),
                    ),
                    SizedBox(width: 12),
                    Expanded(
                      child: FocusLabeledButton(debugLabel: 'Row button 2', label: 'Row 2'),
                    ),
                  ],
                ),
              ),

              FocusDemoSection(
                title: 'Buttons in a column',
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: const [
                    FocusLabeledButton(debugLabel: 'Column button 0', label: 'Column 0'),
                    SizedBox(height: 12),
                    FocusLabeledButton(debugLabel: 'Column button 1', label: 'Column 1'),
                    SizedBox(height: 12),
                    FocusLabeledButton(debugLabel: 'Column button 2', label: 'Column 2'),
                  ],
                ),
              ),

              const FocusDemoSection(
                title: 'Multiple FocusScopes + FocusTraversalGroups (for indexing)',
                child: MultiScopeTraversalPlayground(),
              ),

              FocusDemoSection(
                title: 'FocusTraversalGroup + custom order',
                child: FocusTraversalGroup(
                  policy: OrderedTraversalPolicy(),
                  child: Row(
                    children: const [
                      Expanded(
                        child: FocusTraversalOrder(
                          order: NumericFocusOrder(3),
                          child: FocusSquareTile(debugLabel: 'Ordered tile 3', label: '3', colorSeed: 30),
                        ),
                      ),
                      SizedBox(width: 12),
                      Expanded(
                        child: FocusTraversalOrder(
                          order: NumericFocusOrder(1),
                          child: FocusSquareTile(debugLabel: 'Ordered tile 1', label: '1', colorSeed: 31),
                        ),
                      ),
                      SizedBox(width: 12),
                      Expanded(
                        child: FocusTraversalOrder(
                          order: NumericFocusOrder(2),
                          child: FocusSquareTile(debugLabel: 'Ordered tile 2', label: '2', colorSeed: 32),
                        ),
                      ),
                    ],
                  ),
                ),
              ),

              FocusDemoSection(
                title: 'Non-focusable / skip traversal variants',
                child: Row(
                  children: const [
                    Expanded(
                      child: FocusSquareTile(
                        debugLabel: 'canRequestFocus=false',
                        label: 'noReq',
                        colorSeed: 40,
                        canRequestFocus: false,
                      ),
                    ),
                    SizedBox(width: 12),
                    Expanded(
                      child: FocusSquareTile(
                        debugLabel: 'skipTraversal=true',
                        label: 'skip',
                        colorSeed: 41,
                        skipTraversal: true,
                      ),
                    ),
                    SizedBox(width: 12),
                    Expanded(
                      child: ExcludeFocus(
                        child: FocusSquareTile(debugLabel: 'ExcludeFocus', label: 'excluded', colorSeed: 42),
                      ),
                    ),
                  ],
                ),
              ),

              FocusDemoSection(
                title: 'Autofocus tile (starts focused)',
                child: const Align(
                  alignment: Alignment.centerLeft,
                  child: SizedBox(
                    width: 120,
                    child: FocusSquareTile(debugLabel: 'autofocus tile', label: 'auto', colorSeed: 50),
                  ),
                ),
              ),

              FocusDemoSection(
                title: 'Navigation',
                child: Align(
                  alignment: Alignment.centerLeft,
                  child: FocusActionButton(
                    debugLabel: 'Open next page',
                    label: 'Open next page',
                    onPressed: () {
                      Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const NextPage()));
                    },
                  ),
                ),
              ),

              const SizedBox(height: 24),
              ElevatedButton(onPressed: debugDumpFocusTree, child: const Text('debugDumpFocusTree()')),
            ],
          ),
        ),
      ),
    );
  }
}

final class MultiScopeTraversalPlayground extends StatelessWidget {
  const MultiScopeTraversalPlayground({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        const Text(
          'Use this section to validate scopeTraversalIndex (local) vs traversalIndex (global). '
          'Each card is an explicit FocusScope with its own traversal groups.',
        ),
        const SizedBox(height: 12),
        Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Expanded(
              child: LabeledFocusScopeCard(
                scopeDebugLabel: 'Scope A',
                title: 'Scope A (ReadingOrder, with nested Ordered group)',
                child: FocusTraversalGroup(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      const SizedBox(
                        height: 92,
                        child: Row(
                          children: [
                            Expanded(
                              child: FocusSquareTile(debugLabel: 'A tile 0', label: 'A0', colorSeed: 70),
                            ),
                            SizedBox(width: 12),
                            Expanded(
                              child: FocusSquareTile(debugLabel: 'A tile 1', label: 'A1', colorSeed: 71),
                            ),
                          ],
                        ),
                      ),
                      const SizedBox(height: 12),
                      FocusTraversalGroup(
                        policy: OrderedTraversalPolicy(),
                        child: Row(
                          children: [
                            Expanded(
                              child: FocusTraversalOrder(
                                order: NumericFocusOrder(20),
                                child: FocusSquareTile(debugLabel: 'A ordered 20', label: '20', colorSeed: 72),
                              ),
                            ),
                            const SizedBox(width: 12),
                            Expanded(
                              child: FocusTraversalOrder(
                                order: NumericFocusOrder(10),
                                child: FocusSquareTile(debugLabel: 'A ordered 10', label: '10', colorSeed: 73),
                              ),
                            ),
                            const SizedBox(width: 12),
                            Expanded(
                              child: FocusTraversalOrder(
                                order: NumericFocusOrder(30),
                                child: FocusSquareTile(debugLabel: 'A ordered 30', label: '30', colorSeed: 74),
                              ),
                            ),
                          ],
                        ),
                      ),
                      const SizedBox(height: 12),
                      FocusLabeledButton(debugLabel: 'A button', label: 'A button'),
                    ],
                  ),
                ),
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: LabeledFocusScopeCard(
                scopeDebugLabel: 'Scope B',
                title: 'Scope B (OrderedTraversalPolicy at scope root)',
                child: FocusTraversalGroup(
                  policy: OrderedTraversalPolicy(),
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.stretch,
                    children: [
                      FocusTraversalOrder(
                        order: NumericFocusOrder(2),
                        child: FocusLabeledButton(debugLabel: 'B button 2', label: 'B 2'),
                      ),
                      const SizedBox(height: 12),
                      FocusTraversalOrder(
                        order: NumericFocusOrder(1),
                        child: FocusLabeledButton(debugLabel: 'B button 1', label: 'B 1'),
                      ),
                      const SizedBox(height: 12),
                      FocusTraversalOrder(
                        order: NumericFocusOrder(3),
                        child: FocusTraversalGroup(
                          child: Row(
                            children: [
                              Expanded(
                                child: FocusSquareTile(debugLabel: 'B tile 0', label: 'B0', colorSeed: 75),
                              ),
                              const SizedBox(width: 12),
                              Expanded(
                                child: FocusSquareTile(debugLabel: 'B tile 1', label: 'B1', colorSeed: 76),
                              ),
                            ],
                          ),
                        ),
                      ),
                      const SizedBox(height: 12),
                      ExcludeFocus(
                        child: FocusSquareTile(debugLabel: 'B excluded', label: 'X', colorSeed: 77),
                      ),
                    ],
                  ),
                ),
              ),
            ),
          ],
        ),
      ],
    );
  }
}

final class NextPage extends StatelessWidget {
  const NextPage({super.key});

  @override
  Widget build(BuildContext context) {
    return DefaultTabController(
      length: 2,
      child: Scaffold(
        appBar: AppBar(
          backgroundColor: Theme.of(context).colorScheme.inversePrimary,
          title: const Text('Next page'),
          automaticallyImplyLeading: false,
          leading: const BackButton(),
          bottom: const TabBar(
            tabs: [
              Tab(text: 'Form'),
              Tab(text: 'Scroll'),
            ],
          ),
        ),
        body: const TabBarView(children: [NextPageFormTab(), NextPageScrollTab()]),
      ),
    );
  }
}

final class NextPageFormTab extends StatelessWidget {
  const NextPageFormTab({super.key});

  @override
  Widget build(BuildContext context) {
    return SafeArea(
      child: SingleChildScrollView(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            const Text(
              'This page is designed to stress focus traversal: nested scopes, nested traversal groups, '
              'and mixed widget types.',
            ),
            const SizedBox(height: 12),
            LabeledFocusScopeCard(
              scopeDebugLabel: 'NextPage Scope 1',
              title: 'Scope 1 (ReadingOrder + nested Ordered group)',
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.stretch,
                children: [
                  const DebugTextField(debugLabel: 'NP field A', labelText: 'Field A'),
                  const SizedBox(height: 12),
                  const DebugTextField(debugLabel: 'NP field B', labelText: 'Field B'),
                  const SizedBox(height: 12),
                  FocusTraversalGroup(
                    policy: OrderedTraversalPolicy(),
                    child: Row(
                      children: [
                        Expanded(
                          child: FocusTraversalOrder(
                            order: NumericFocusOrder(2),
                            child: const FocusSquareTile(debugLabel: 'NP ordered 2', label: '2', colorSeed: 81),
                          ),
                        ),
                        const SizedBox(width: 12),
                        Expanded(
                          child: FocusTraversalOrder(
                            order: NumericFocusOrder(1),
                            child: const FocusSquareTile(debugLabel: 'NP ordered 1', label: '1', colorSeed: 82),
                          ),
                        ),
                        const SizedBox(width: 12),
                        Expanded(
                          child: FocusTraversalOrder(
                            order: NumericFocusOrder(3),
                            child: const FocusSquareTile(debugLabel: 'NP ordered 3', label: '3', colorSeed: 83),
                          ),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
            ),
            const SizedBox(height: 12),
            LabeledFocusScopeCard(
              scopeDebugLabel: 'NextPage Scope 2',
              title: 'Scope 2 (OrderedTraversalPolicy over mixed controls)',
              child: FocusTraversalGroup(
                policy: OrderedTraversalPolicy(),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.stretch,
                  children: [
                    FocusTraversalOrder(
                      order: NumericFocusOrder(10),
                      child: const DebugTextField(debugLabel: 'NP field C', labelText: 'Field C'),
                    ),
                    const SizedBox(height: 12),
                    FocusTraversalOrder(
                      order: NumericFocusOrder(20),
                      child: const DebugSwitchTile(debugLabel: 'NP switch', title: 'Enable option'),
                    ),
                    const SizedBox(height: 12),
                    FocusTraversalOrder(
                      order: NumericFocusOrder(30),
                      child: const DebugSlider(debugLabel: 'NP slider', label: 'Intensity'),
                    ),
                  ],
                ),
              ),
            ),
            const SizedBox(height: 24),
            ElevatedButton(onPressed: debugDumpFocusTree, child: const Text('debugDumpFocusTree()')),
          ],
        ),
      ),
    );
  }
}

final class NextPageScrollTab extends StatelessWidget {
  const NextPageScrollTab({super.key});

  @override
  Widget build(BuildContext context) {
    return CustomScrollView(
      slivers: [
        const SliverToBoxAdapter(
          child: Padding(
            padding: EdgeInsets.all(16),
            child: Text('Nested scroll + traversal groups (try tabbing through list + grid)'),
          ),
        ),
        SliverToBoxAdapter(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16),
            child: FocusTraversalGroup(
              child: SizedBox(
                height: 88,
                child: ListView.separated(
                  scrollDirection: Axis.horizontal,
                  itemCount: 8,
                  separatorBuilder: (context, index) => const SizedBox(width: 12),
                  itemBuilder: (context, index) {
                    return SizedBox(
                      width: 80,
                      child: FocusSquareTile(debugLabel: 'NP HList $index', label: 'H$index', colorSeed: 90 + index),
                    );
                  },
                ),
              ),
            ),
          ),
        ),
        const SliverToBoxAdapter(child: SizedBox(height: 12)),
        SliverPadding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          sliver: SliverList.separated(
            itemCount: 10,
            separatorBuilder: (context, index) => const SizedBox(height: 8),
            itemBuilder: (context, index) {
              return FocusListRow(
                debugLabel: 'NP list row $index',
                title: 'Row $index',
                subtitle: 'List item with its own FocusNode',
              );
            },
          ),
        ),
        const SliverToBoxAdapter(child: SizedBox(height: 12)),
        SliverPadding(
          padding: const EdgeInsets.symmetric(horizontal: 16),
          sliver: SliverToBoxAdapter(
            child: FocusTraversalGroup(
              policy: OrderedTraversalPolicy(),
              child: Row(
                children: const [
                  Expanded(
                    child: FocusTraversalOrder(
                      order: NumericFocusOrder(2),
                      child: FocusSquareTile(debugLabel: 'NP footer ordered 2', label: 'F2', colorSeed: 120),
                    ),
                  ),
                  SizedBox(width: 12),
                  Expanded(
                    child: FocusTraversalOrder(
                      order: NumericFocusOrder(1),
                      child: FocusSquareTile(debugLabel: 'NP footer ordered 1', label: 'F1', colorSeed: 121),
                    ),
                  ),
                  SizedBox(width: 12),
                  Expanded(
                    child: FocusTraversalOrder(
                      order: NumericFocusOrder(3),
                      child: FocusSquareTile(debugLabel: 'NP footer ordered 3', label: 'F3', colorSeed: 122),
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
        const SliverToBoxAdapter(child: SizedBox(height: 24)),
      ],
    );
  }
}