diff --git a/packages/devtools_app/lib/devtools_app.dart b/packages/devtools_app/lib/devtools_app.dart index 5125d710595..fb2637e7665 100644 --- a/packages/devtools_app/lib/devtools_app.dart +++ b/packages/devtools_app/lib/devtools_app.dart @@ -10,6 +10,8 @@ export 'src/framework/notifications_view.dart'; export 'src/framework/release_notes.dart'; export 'src/framework/scaffold/app_bar.dart'; export 'src/framework/scaffold/status_line.dart'; +export 'src/screens/accessibility/accessibility_controller.dart'; +export 'src/screens/accessibility/accessibility_screen.dart'; export 'src/screens/app_size/app_size_controller.dart'; export 'src/screens/app_size/app_size_screen.dart'; export 'src/screens/debugger/breakpoint_manager.dart'; diff --git a/packages/devtools_app/lib/src/app.dart b/packages/devtools_app/lib/src/app.dart index 272f1fc06cd..e51ec18c84a 100644 --- a/packages/devtools_app/lib/src/app.dart +++ b/packages/devtools_app/lib/src/app.dart @@ -23,6 +23,8 @@ import 'framework/notifications_view.dart'; import 'framework/observer/disconnect_observer.dart'; import 'framework/release_notes.dart'; import 'framework/scaffold/scaffold.dart'; +import 'screens/accessibility/accessibility_controller.dart'; +import 'screens/accessibility/accessibility_screen.dart'; import 'screens/app_size/app_size_controller.dart'; import 'screens/app_size/app_size_screen.dart'; import 'screens/debugger/debugger_controller.dart'; @@ -713,6 +715,11 @@ List defaultScreens({ LoggingScreen(), createController: (_) => LoggingController(), ), + if (FeatureFlags.accessibility.isEnabled) + DevToolsScreen( + AccessibilityScreen(), + createController: (_) => AccessibilityController(), + ), DevToolsScreen(ProviderScreen()), DevToolsScreen( AppSizeScreen(), diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart new file mode 100644 index 00000000000..69f6dd12e0d --- /dev/null +++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart @@ -0,0 +1,67 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/foundation.dart'; + +import '../../shared/framework/screen.dart'; +import '../../shared/framework/screen_controllers.dart'; + +/// Controller for the Accessibility screen. +class AccessibilityController extends DevToolsScreenController + with AutoDisposeControllerMixin { + AccessibilityController() { + _initListeners(); + } + + void _initListeners() { + addAutoDisposeListener(brightness, _onBrightnessChanged); + addAutoDisposeListener(textScale, _onTextScaleChanged); + addAutoDisposeListener(boldText, _onBoldTextChanged); + addAutoDisposeListener(screenReader, _onScreenReaderChanged); + addAutoDisposeListener(highContrast, _onHighContrastChanged); + } + + void _onBrightnessChanged() { + // TODO(hannah-hyj): Implement VM service extension call for brightness override. + // e.g. using 'ext.flutter.brightnessOverride'. + } + + void _onTextScaleChanged() { + // TODO(hannah-hyj): Implement VM service extension call for text scale override. + } + + void _onBoldTextChanged() { + // TODO(hannah-hyj): Implement VM service extension call for bold text override. + } + + void _onScreenReaderChanged() { + // TODO(hannah-hyj): Implement VM service extension call for screen reader / semantics debugger. + // e.g. using 'ext.flutter.showSemanticsDebugger'. + } + + void _onHighContrastChanged() { + // TODO(hannah-hyj): Implement VM service extension call for high contrast override. + } + + @override + final screenId = ScreenMetaData.accessibility.id; + + // --- Accessibility Overrides State --- + final brightness = ValueNotifier('System'); + final textScale = ValueNotifier(1.0); + final boldText = ValueNotifier(false); + final screenReader = ValueNotifier(false); + final highContrast = ValueNotifier(false); + + @override + void dispose() { + brightness.dispose(); + textScale.dispose(); + boldText.dispose(); + screenReader.dispose(); + highContrast.dispose(); + super.dispose(); + } +} diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart new file mode 100644 index 00000000000..6c3efd2a224 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart @@ -0,0 +1,94 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:flutter/material.dart'; + +import '../../shared/framework/screen.dart'; +import '../../shared/globals.dart'; +import '../../shared/ui/common_widgets.dart'; +import 'accessibility_controller.dart'; + +/// A screen that displays accessibility information. +class AccessibilityScreen extends Screen { + AccessibilityScreen() : super.fromMetaData(ScreenMetaData.accessibility); + + static final id = ScreenMetaData.accessibility.id; + + @override + Widget buildScreenBody(BuildContext context) => + const AccessibilityScreenBody(); +} + +class AccessibilityScreenBody extends StatefulWidget { + const AccessibilityScreenBody({super.key}); + + @override + State createState() => + _AccessibilityScreenBodyState(); +} + +class _AccessibilityScreenBodyState extends State + with AutoDisposeMixin { + late AccessibilityController controller; + + @override + void initState() { + super.initState(); + controller = screenControllers.lookup(); + } + + @override + Widget build(BuildContext context) { + final splitAxis = _splitAxisFor(context); + return SplitPane( + axis: splitAxis, + initialFractions: const [0.6, 0.4], + children: const [ + AccessibilitySemanticsTreePane(), + AccessibilityOverridesPane(), + ], + ); + } + + Axis _splitAxisFor(BuildContext context) { + final screenSize = MediaQuery.of(context).size; + return screenSize.width > 1000 ? Axis.horizontal : Axis.vertical; + } +} + +/// A pane that displays the semantics tree of the connected app. +class AccessibilitySemanticsTreePane extends StatelessWidget { + const AccessibilitySemanticsTreePane({super.key}); + + @override + Widget build(BuildContext context) { + return const DevToolsAreaPane( + header: AreaPaneHeader(title: Text('Semantics Tree')), + child: CenteredMessage( + message: + 'Accessibility semantics tree placeholder.\n' + '// TODO(hannah-hyj): Implement semantics tree view and details explorer.', + ), + ); + } +} + +/// A pane that displays the accessibility overrides controls. +class AccessibilityOverridesPane extends StatelessWidget { + const AccessibilityOverridesPane({super.key}); + + @override + Widget build(BuildContext context) { + return const DevToolsAreaPane( + header: AreaPaneHeader(title: Text('Accessibility Overrides')), + child: CenteredMessage( + message: + 'Accessibility overrides placeholder.\n' + '// TODO(hannah-hyj): Implement setting overrides (brightness, text scale, bold text, screen reader, high contrast).', + ), + ); + } +} diff --git a/packages/devtools_app/lib/src/shared/feature_flags.dart b/packages/devtools_app/lib/src/shared/feature_flags.dart index 07882781690..937c75108a6 100644 --- a/packages/devtools_app/lib/src/shared/feature_flags.dart +++ b/packages/devtools_app/lib/src/shared/feature_flags.dart @@ -69,6 +69,12 @@ extension FeatureFlags on Never { enabled: enableExperiments, ); + /// Flag to enable the Accessibility page. + static final accessibility = BooleanFeatureFlag( + name: 'accessibility', + enabled: enableExperiments, + ); + /// A set of all the boolean feature flags for debugging purposes. /// /// When adding a new boolean flag, you are responsible for adding it to this @@ -77,6 +83,7 @@ extension FeatureFlags on Never { memorySaveLoad, devToolsExtensions, dapDebugging, + accessibility, }; /// A set of all the Flutter channel feature flags for debugging purposes. diff --git a/packages/devtools_app/lib/src/shared/framework/screen.dart b/packages/devtools_app/lib/src/shared/framework/screen.dart index a2544f23377..2bfe341a5fd 100644 --- a/packages/devtools_app/lib/src/shared/framework/screen.dart +++ b/packages/devtools_app/lib/src/shared/framework/screen.dart @@ -94,6 +94,12 @@ enum ScreenMetaData { supportsWebServerDevice: true, tutorialVideoTimestamp: '?t=558', ), + accessibility( + 'accessibility', + title: 'Accessibility', + icon: Icons.accessibility, + requiresFlutter: true, + ), provider( 'provider', title: 'Provider', diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart new file mode 100644 index 00000000000..595cf4a4a7b --- /dev/null +++ b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart @@ -0,0 +1,80 @@ +// Copyright 2026 The Flutter Authors +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file or at https://developers.google.com/open-source/licenses/bsd. + +@TestOn('vm') +library; + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_app_shared/ui.dart'; +import 'package:devtools_app_shared/utils.dart'; +import 'package:devtools_test/devtools_test.dart'; +import 'package:devtools_test/helpers.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; + +void main() { + late AccessibilityScreen screen; + late AccessibilityController controller; + const windowSize = Size(1000.0, 1000.0); + + group('Accessibility Screen', () { + Future pumpAccessibilityScreen(WidgetTester tester) async { + await tester.pumpWidget( + wrapWithControllers( + const AccessibilityScreenBody(), + accessibility: controller, + ), + ); + } + + setUp(() { + final fakeServiceConnection = FakeServiceConnectionManager(); + when( + fakeServiceConnection.serviceManager.connectedApp!.isFlutterWebAppNow, + ).thenReturn(false); + when( + fakeServiceConnection.serviceManager.connectedApp!.isProfileBuildNow, + ).thenReturn(false); + when( + fakeServiceConnection.errorBadgeManager.errorCountNotifier( + 'accessibility', + ), + ).thenReturn(ValueNotifier(0)); + + setGlobal(NotificationService, NotificationService()); + setGlobal( + DevToolsEnvironmentParameters, + ExternalDevToolsEnvironmentParameters(), + ); + setGlobal(PreferencesController, PreferencesController()); + setGlobal(ServiceConnectionManager, fakeServiceConnection); + setGlobal(IdeTheme, IdeTheme()); + + controller = AccessibilityController(); + screen = AccessibilityScreen(); + }); + + testWidgets('builds its tab', (WidgetTester tester) async { + await tester.pumpWidget(wrap(Builder(builder: screen.buildTab))); + expect(find.text('Accessibility'), findsOneWidget); + }); + + testWidgetsWithWindowSize('builds split view with panes', windowSize, ( + WidgetTester tester, + ) async { + await pumpAccessibilityScreen(tester); + await tester.pumpAndSettle(); + + expect(find.byType(AccessibilityScreenBody), findsOneWidget); + expect(find.byType(SplitPane), findsAtLeastNWidgets(1)); + + // Overrides pane should be visible + expect(find.byType(AccessibilityOverridesPane), findsOneWidget); + + // Semantics Tree pane should be visible + expect(find.byType(AccessibilitySemanticsTreePane), findsOneWidget); + }); + }); +} diff --git a/packages/devtools_test/lib/src/helpers/wrappers.dart b/packages/devtools_test/lib/src/helpers/wrappers.dart index 20f16ad8817..a782b26d6dc 100644 --- a/packages/devtools_test/lib/src/helpers/wrappers.dart +++ b/packages/devtools_test/lib/src/helpers/wrappers.dart @@ -96,6 +96,7 @@ Widget wrapWithControllers( ReleaseNotesController? releaseNotes, VMDeveloperToolsController? vmDeveloperTools, DTDToolsController? dtdTools, + AccessibilityController? accessibility, bool includeRouter = true, DevToolsQueryParams? queryParams, }) { @@ -173,6 +174,12 @@ Widget wrapWithControllers( offline: offline, ); } + if (accessibility != null) { + screenControllers.register( + () => accessibility, + offline: offline, + ); + } var child = wrapWithNotifications(widget); final providers = [