diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart index 69f6dd12e0d..887ddf5b9fb 100644 --- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart +++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_controller.dart @@ -8,6 +8,17 @@ import 'package:flutter/foundation.dart'; import '../../shared/framework/screen.dart'; import '../../shared/framework/screen_controllers.dart'; +/// Modes for brightness override in the accessibility controls. +enum BrightnessOverride { + system('System Default'), + light('Light Mode'), + dark('Dark Mode'); + + const BrightnessOverride(this.display); + + final String display; +} + /// Controller for the Accessibility screen. class AccessibilityController extends DevToolsScreenController with AutoDisposeControllerMixin { @@ -49,7 +60,9 @@ class AccessibilityController extends DevToolsScreenController final screenId = ScreenMetaData.accessibility.id; // --- Accessibility Overrides State --- - final brightness = ValueNotifier('System'); + final brightness = ValueNotifier( + BrightnessOverride.system, + ); final textScale = ValueNotifier(1.0); final boldText = ValueNotifier(false); final screenReader = ValueNotifier(false); diff --git a/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart index 6c3efd2a224..fc387547b58 100644 --- a/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart +++ b/packages/devtools_app/lib/src/screens/accessibility/accessibility_screen.dart @@ -8,8 +8,12 @@ import 'package:flutter/material.dart'; import '../../shared/framework/screen.dart'; import '../../shared/globals.dart'; -import '../../shared/ui/common_widgets.dart'; import 'accessibility_controller.dart'; +import 'overrides_pane.dart'; +import 'semantics_tree_pane.dart'; + +export 'overrides_pane.dart'; +export 'semantics_tree_pane.dart'; /// A screen that displays accessibility information. class AccessibilityScreen extends Screen { @@ -58,37 +62,3 @@ class _AccessibilityScreenBodyState extends State 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/screens/accessibility/overrides_pane.dart b/packages/devtools_app/lib/src/screens/accessibility/overrides_pane.dart new file mode 100644 index 00000000000..f50ee3ab5f5 --- /dev/null +++ b/packages/devtools_app/lib/src/screens/accessibility/overrides_pane.dart @@ -0,0 +1,204 @@ +// 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:flutter/material.dart'; + +import '../../shared/globals.dart'; +import '../../shared/ui/common_widgets.dart'; +import 'accessibility_controller.dart'; + +/// A pane that displays the accessibility overrides controls. +class AccessibilityOverridesPane extends StatelessWidget { + const AccessibilityOverridesPane({super.key}); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final controller = screenControllers.lookup(); + return DevToolsAreaPane( + header: const AreaPaneHeader( + title: Text('Accessibility Overrides'), + roundedTopBorder: false, + includeTopBorder: false, + ), + child: Scrollbar( + child: SingleChildScrollView( + padding: const EdgeInsets.all(defaultSpacing), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Simulate and test accessibility settings on the connected device in real-time.', + style: theme.subtleTextStyle, + ), + const SizedBox(height: defaultSpacing), + const Divider(), + const SizedBox(height: denseSpacing), + _BrightnessOverride(controller: controller), + const SizedBox(height: defaultSpacing), + _TextScaleOverride(controller: controller), + const SizedBox(height: defaultSpacing), + _SwitchOverride( + label: 'Bold Text', + description: 'Forces all text in the application to be bold.', + notifier: controller.boldText, + ), + const SizedBox(height: defaultSpacing), + _SwitchOverride( + label: 'Screen Reader Debugger', + description: 'Debug and test screen reader layouts.', + notifier: controller.screenReader, + ), + const SizedBox(height: defaultSpacing), + _SwitchOverride( + label: 'High Contrast', + description: 'Increases the contrast of text and icons.', + notifier: controller.highContrast, + ), + ], + ), + ), + ), + ); + } +} + +class _AccessibilityPanelLabel extends StatelessWidget { + const _AccessibilityPanelLabel({ + required this.label, + required this.description, + }); + + final String label; + final String description; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: theme.boldTextStyle), + const SizedBox(height: densePadding), + Text(description, style: theme.subtleTextStyle), + ], + ); + } +} + +class _BrightnessOverride extends StatelessWidget { + const _BrightnessOverride({required this.controller}); + + final AccessibilityController controller; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const _AccessibilityPanelLabel( + label: 'Brightness', + description: 'Override the color scheme mode of the app.', + ), + const SizedBox(height: denseSpacing), + ValueListenableBuilder( + valueListenable: controller.brightness, + builder: (context, value, _) { + return RoundedDropDownButton( + isExpanded: true, + value: value, + items: BrightnessOverride.values.map((option) { + return DropdownMenuItem( + value: option, + child: Text(option.display), + ); + }).toList(), + onChanged: (newValue) { + if (newValue != null) { + controller.brightness.value = newValue; + } + }, + ); + }, + ), + ], + ); + } +} + +class _TextScaleOverride extends StatelessWidget { + const _TextScaleOverride({required this.controller}); + + final AccessibilityController controller; + + static const _minTextScale = 0.5; + static const _maxTextScale = 3.0; + static const _textScaleDivisions = 25; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + return ValueListenableBuilder( + valueListenable: controller.textScale, + builder: (context, value, _) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const _AccessibilityPanelLabel( + label: 'Text Scale', + description: 'Scale the system font size.', + ), + Text( + '${value.toStringAsFixed(2)}x', + style: theme.boldTextStyle, + ), + ], + ), + const SizedBox(height: densePadding), + Slider( + value: value, + min: _minTextScale, + max: _maxTextScale, + divisions: _textScaleDivisions, + onChanged: (newValue) { + controller.textScale.value = newValue; + }, + ), + ], + ); + }, + ); + } +} + +class _SwitchOverride extends StatelessWidget { + const _SwitchOverride({ + required this.label, + required this.description, + required this.notifier, + }); + + final String label; + final String description; + final ValueNotifier notifier; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: _AccessibilityPanelLabel( + label: label, + description: description, + ), + ), + NotifierSwitch(notifier: notifier), + ], + ); + } +} diff --git a/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart b/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart new file mode 100644 index 00000000000..8081874954c --- /dev/null +++ b/packages/devtools_app/lib/src/screens/accessibility/semantics_tree_pane.dart @@ -0,0 +1,29 @@ +// 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:flutter/material.dart'; + +import '../../shared/ui/common_widgets.dart'; + +/// 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'), + includeTopBorder: false, + roundedTopBorder: false, + ), + child: CenteredMessage( + message: + 'Accessibility semantics tree placeholder.\n' + '// TODO(hannah-hyj): Implement semantics tree view and details explorer.', + ), + ); + } +} diff --git a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart index 595cf4a4a7b..ba5e53e637b 100644 --- a/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart +++ b/packages/devtools_app/test/screens/accessibility/accessibility_screen_test.dart @@ -76,5 +76,110 @@ void main() { // Semantics Tree pane should be visible expect(find.byType(AccessibilitySemanticsTreePane), findsOneWidget); }); + + testWidgetsWithWindowSize( + 'renders all override controls in AccessibilityOverridesPane', + windowSize, + (WidgetTester tester) async { + await pumpAccessibilityScreen(tester); + await tester.pumpAndSettle(); + + expect(find.text('Accessibility Overrides'), findsOneWidget); + expect( + find.text( + 'Simulate and test accessibility settings on the connected device in real-time.', + ), + findsOneWidget, + ); + + // Brightness controls + expect(find.text('Brightness'), findsOneWidget); + expect( + find.text('Override the color scheme mode of the app.'), + findsOneWidget, + ); + expect( + find.byType(RoundedDropDownButton), + findsOneWidget, + ); + + // Text Scale controls + expect(find.text('Text Scale'), findsOneWidget); + expect(find.text('Scale the system font size.'), findsOneWidget); + expect(find.text('1.00x'), findsOneWidget); + expect(find.byType(Slider), findsOneWidget); + + // Switches + expect(find.text('Bold Text'), findsOneWidget); + expect( + find.text('Forces all text in the application to be bold.'), + findsOneWidget, + ); + expect(find.text('Screen Reader Debugger'), findsOneWidget); + expect( + find.text('Debug and test screen reader layouts.'), + findsOneWidget, + ); + expect(find.text('High Contrast'), findsOneWidget); + expect( + find.text('Increases the contrast of text and icons.'), + findsOneWidget, + ); + expect(find.byType(Switch), findsNWidgets(3)); + }, + ); + + testWidgetsWithWindowSize( + 'interacting with override controls updates controller state', + windowSize, + (WidgetTester tester) async { + await pumpAccessibilityScreen(tester); + await tester.pumpAndSettle(); + + Finder findSwitchFor(String label) { + return find.descendant( + of: find.ancestor(of: find.text(label), matching: find.byType(Row)), + matching: find.byType(Switch), + ); + } + + // 1. Test Brightness Dropdown + expect(controller.brightness.value, BrightnessOverride.system); + await tester.tap( + find.byType(RoundedDropDownButton), + ); + await tester.pumpAndSettle(); + await tester.tap(find.text('Light Mode').last); + await tester.pumpAndSettle(); + expect(controller.brightness.value, BrightnessOverride.light); + + // 2. Test Bold Text Switch + expect(controller.boldText.value, isFalse); + final boldTextSwitch = findSwitchFor('Bold Text'); + await tester.ensureVisible(boldTextSwitch); + await tester.pumpAndSettle(); + await tester.tap(boldTextSwitch); + await tester.pumpAndSettle(); + expect(controller.boldText.value, isTrue); + + // 3. Test Screen Reader Debugger Switch + expect(controller.screenReader.value, isFalse); + final screenReaderSwitch = findSwitchFor('Screen Reader Debugger'); + await tester.ensureVisible(screenReaderSwitch); + await tester.pumpAndSettle(); + await tester.tap(screenReaderSwitch); + await tester.pumpAndSettle(); + expect(controller.screenReader.value, isTrue); + + // 4. Test High Contrast Switch + expect(controller.highContrast.value, isFalse); + final highContrastSwitch = findSwitchFor('High Contrast'); + await tester.ensureVisible(highContrastSwitch); + await tester.pumpAndSettle(); + await tester.tap(highContrastSwitch); + await tester.pumpAndSettle(); + expect(controller.highContrast.value, isTrue); + }, + ); }); }