Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/devtools_app/lib/devtools_app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 7 additions & 0 deletions packages/devtools_app/lib/src/app.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -713,6 +715,11 @@ List<DevToolsScreen> defaultScreens({
LoggingScreen(),
createController: (_) => LoggingController(),
),
if (FeatureFlags.accessibility.isEnabled)
DevToolsScreen<AccessibilityController>(
AccessibilityScreen(),
createController: (_) => AccessibilityController(),
),
DevToolsScreen<DevToolsScreenController>(ProviderScreen()),
DevToolsScreen<AppSizeController>(
AppSizeScreen(),
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String>('System');
final textScale = ValueNotifier<double>(1.0);
final boldText = ValueNotifier<bool>(false);
final screenReader = ValueNotifier<bool>(false);
final highContrast = ValueNotifier<bool>(false);

@override
void dispose() {
brightness.dispose();
textScale.dispose();
boldText.dispose();
screenReader.dispose();
highContrast.dispose();
super.dispose();
}
}
Original file line number Diff line number Diff line change
@@ -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<AccessibilityScreenBody> createState() =>
_AccessibilityScreenBodyState();
}

class _AccessibilityScreenBodyState extends State<AccessibilityScreenBody>
with AutoDisposeMixin {
late AccessibilityController controller;

@override
void initState() {
super.initState();
controller = screenControllers.lookup<AccessibilityController>();
}
Comment thread
hannah-hyj marked this conversation as resolved.

@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).',
),
);
}
}
7 changes: 7 additions & 0 deletions packages/devtools_app/lib/src/shared/feature_flags.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -77,6 +83,7 @@ extension FeatureFlags on Never {
memorySaveLoad,
devToolsExtensions,
dapDebugging,
accessibility,
};

/// A set of all the Flutter channel feature flags for debugging purposes.
Expand Down
6 changes: 6 additions & 0 deletions packages/devtools_app/lib/src/shared/framework/screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,12 @@ enum ScreenMetaData {
supportsWebServerDevice: true,
tutorialVideoTimestamp: '?t=558',
),
accessibility(
'accessibility',
title: 'Accessibility',
icon: Icons.accessibility,
requiresFlutter: true,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does this page work for flutter web apps or should it be disabled for web?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

web apps also have semantics tree and a11y settings. This page should work.

),
Comment thread
hannah-hyj marked this conversation as resolved.
provider(
'provider',
title: 'Provider',
Expand Down
Original file line number Diff line number Diff line change
@@ -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<void> 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<int>(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);
});
});
}
7 changes: 7 additions & 0 deletions packages/devtools_test/lib/src/helpers/wrappers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ Widget wrapWithControllers(
ReleaseNotesController? releaseNotes,
VMDeveloperToolsController? vmDeveloperTools,
DTDToolsController? dtdTools,
AccessibilityController? accessibility,
bool includeRouter = true,
DevToolsQueryParams? queryParams,
}) {
Expand Down Expand Up @@ -173,6 +174,12 @@ Widget wrapWithControllers(
offline: offline,
);
}
if (accessibility != null) {
screenControllers.register<AccessibilityController>(
() => accessibility,
offline: offline,
);
}

var child = wrapWithNotifications(widget);
final providers = [
Expand Down
Loading