Skip to content

Commit a916020

Browse files
Initial release - includes primary focus widget bounds, and focus change history.
0 parents  commit a916020

13 files changed

+410
-0
lines changed

.gitignore

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Miscellaneous
2+
*.class
3+
*.log
4+
*.pyc
5+
*.swp
6+
.DS_Store
7+
.atom/
8+
.buildlog/
9+
.history
10+
.svn/
11+
migrate_working_dir/
12+
13+
# IntelliJ related
14+
*.iml
15+
*.ipr
16+
*.iws
17+
.idea/
18+
19+
# The .vscode folder contains launch configuration and tasks you configure in
20+
# VS Code which you may wish to be included in version control, so this line
21+
# is commented out by default.
22+
#.vscode/
23+
24+
# Flutter/Dart/Pub related
25+
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
26+
/pubspec.lock
27+
**/doc/api/
28+
.dart_tool/
29+
.flutter-plugins
30+
.flutter-plugins-dependencies
31+
build/

.metadata

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# This file tracks properties of this Flutter project.
2+
# Used by Flutter tool to assess capabilities and perform upgrades etc.
3+
#
4+
# This file should be version controlled and should not be manually edited.
5+
6+
version:
7+
revision: "8defaa71a77c16e8547abdbfad2053ce3a6e2d5b"
8+
channel: "stable"
9+
10+
project_type: package

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
## 0.1.0
2+
Initial Release:
3+
* `FocusDebugger`: A widget that surrounds an entire app and paints focus debugging visuals.
4+
* `FocusWidgetPainter`: Paints the global bounds around the widget with primary focus.
5+
* `FocusHistoryPane`: A pane that displays a full history of focus movement during an app run.
6+
* `FfdLogs`: Logger used within `flutter_focus_debugger`.

LICENSE

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Copyright (c) 2025 Declarative, Inc.
2+
3+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4+
5+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6+
7+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Flutter Focus Debugger
2+
Tools for tracing and debugging focus changes in Flutter.

analysis_options.yaml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
include: package:flutter_lints/flutter.yaml
2+
3+
# Additional information about this file can be found at
4+
# https://dart.dev/guides/language/analysis-options
5+
6+
linter:
7+
rules:
8+
always_use_package_imports: true

lib/flutter_focus_debugger.dart

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export 'src/focus_debugger.dart';
2+
export 'src/focus_history_pane.dart';
3+
export 'src/focus_tree_pane.dart';
4+
export 'src/focus_widget_painter.dart';
5+
export 'src/logging.dart';
6+
7+
// Export the logging package so that developers don't have to add it
8+
// for the purpose of specifying a log `Level`.
9+
export 'package:logging/logging.dart';

lib/src/focus_debugger.dart

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import 'package:flutter/material.dart';
2+
import 'package:flutter_focus_debugger/src/focus_history_pane.dart';
3+
import 'package:flutter_focus_debugger/src/focus_widget_painter.dart';
4+
5+
/// A debugger UI to better understand and track the focus structure in a Flutter
6+
/// app.
7+
///
8+
/// The debugger UI is painted on top of the [child]. The UI includes:
9+
///
10+
/// - Visual bounds around the widget with primary focus.
11+
/// - A translucent pane that shows the history of focus movement.
12+
class FocusDebugger extends StatelessWidget {
13+
const FocusDebugger({super.key, this.isEnabled = true, required this.child});
14+
15+
/// Whether to show a focus debugging UI.
16+
///
17+
/// This is an option so that developers can leave this widget in their widget
18+
/// tree across debug and production builds, and simply toggle the behavior on
19+
/// and off.
20+
final bool isEnabled;
21+
22+
/// The app widget tree (this widget is typically placed at the top of the widget
23+
/// tree).
24+
final Widget child;
25+
26+
@override
27+
Widget build(BuildContext context) {
28+
if (!isEnabled) {
29+
return child;
30+
}
31+
32+
return Directionality(
33+
textDirection: TextDirection.ltr,
34+
child: Stack(
35+
children: [
36+
FocusWidgetPainter(child: child), //
37+
Positioned(top: 0, right: 0, bottom: 0, child: FocusHistoryPane()),
38+
],
39+
),
40+
);
41+
}
42+
}

lib/src/focus_history_pane.dart

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import 'package:flutter/material.dart' hide LinearGradient;
2+
import 'package:flutter/rendering.dart';
3+
4+
class FocusHistoryPane extends StatefulWidget {
5+
const FocusHistoryPane({super.key, this.width = 200});
6+
7+
final double width;
8+
9+
@override
10+
State<FocusHistoryPane> createState() => _FocusHistoryPaneState();
11+
}
12+
13+
class _FocusHistoryPaneState extends State<FocusHistoryPane> {
14+
final _history = <String>[];
15+
16+
@override
17+
void initState() {
18+
super.initState();
19+
20+
FocusManager.instance.addListener(_onFocusChange);
21+
}
22+
23+
@override
24+
void dispose() {
25+
FocusManager.instance.removeListener(_onFocusChange);
26+
27+
super.dispose();
28+
}
29+
30+
void _onFocusChange() {
31+
final focusedNode = FocusManager.instance.primaryFocus;
32+
final name = _getFocusName(focusedNode);
33+
if (_history.lastOrNull == name) {
34+
return;
35+
}
36+
37+
setState(() {
38+
_history.add(name);
39+
});
40+
}
41+
42+
String _getFocusName(FocusNode? focusNode) {
43+
if (focusNode == null) {
44+
return "None";
45+
}
46+
47+
return focusNode.debugLabel ?? "${focusNode.hashCode}";
48+
}
49+
50+
@override
51+
Widget build(BuildContext context) {
52+
return SizedBox(
53+
width: widget.width,
54+
child: ShaderMask(
55+
shaderCallback: _fadeOutTopToBottom,
56+
child: ColoredBox(
57+
color: Colors.black.withValues(alpha: 0.8),
58+
child: Column(
59+
crossAxisAlignment: CrossAxisAlignment.stretch,
60+
children: [
61+
Padding(
62+
padding: const EdgeInsets.all(12),
63+
child: Text(
64+
"FOCUS HISTORY",
65+
textAlign: TextAlign.center,
66+
style: TextStyle(color: Colors.white, fontSize: 8, fontWeight: FontWeight.bold),
67+
),
68+
),
69+
Expanded(
70+
child: ListView.builder(
71+
itemCount: _history.length,
72+
itemBuilder: (context, index) {
73+
final historyIndex = _history.length - index - 1;
74+
final name = _history[historyIndex];
75+
76+
return Padding(
77+
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
78+
child: Text("$historyIndex: $name", style: TextStyle(color: Colors.purpleAccent)),
79+
);
80+
},
81+
),
82+
),
83+
],
84+
),
85+
),
86+
),
87+
);
88+
}
89+
90+
Shader _fadeOutTopToBottom(Rect bounds) {
91+
return LinearGradient(
92+
colors: [Colors.white, Colors.transparent],
93+
begin: Alignment.topCenter,
94+
end: Alignment.bottomCenter,
95+
stops: [0.3, 0.6],
96+
).createShader(bounds);
97+
}
98+
}

lib/src/focus_tree_pane.dart

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'package:flutter/material.dart';
2+
3+
class FocusTreePane extends StatefulWidget {
4+
const FocusTreePane({super.key, required this.child});
5+
6+
final Widget child;
7+
8+
@override
9+
State<FocusTreePane> createState() => _FocusTreePaneState();
10+
}
11+
12+
class _FocusTreePaneState extends State<FocusTreePane> {
13+
@override
14+
void initState() {
15+
super.initState();
16+
17+
FocusManager.instance.addListener(_onFocusChange);
18+
}
19+
20+
@override
21+
void dispose() {
22+
FocusManager.instance.removeListener(_onFocusChange);
23+
24+
super.dispose();
25+
}
26+
27+
void _onFocusChange() {
28+
// TODO:
29+
}
30+
31+
@override
32+
Widget build(BuildContext context) {
33+
return widget.child;
34+
}
35+
}

0 commit comments

Comments
 (0)