diff --git a/packages/flutter/lib/src/material/theme.dart b/packages/flutter/lib/src/material/theme.dart index 4667b2680d..436a4dc133 100644 --- a/packages/flutter/lib/src/material/theme.dart +++ b/packages/flutter/lib/src/material/theme.dart @@ -161,6 +161,42 @@ class Theme extends StatelessWidget { .resolveFrom(context); } + /// Retrieves the [Brightness] to use for descendant Material widgets, based + /// on the value of [ThemeData.brightness] in the given [context]. + /// + /// If no [InheritedTheme] can be found in the given [context], or its `brightness` + /// is null, it will fall back to [MediaQueryData.platformBrightness]. + /// + /// See also: + /// + /// * [maybeBrightnessOf], which returns null if no valid [InheritedTheme] or + /// [MediaQuery] exists. + /// * [ThemeData.brightness], the property that takes precedence over + /// [MediaQueryData.platformBrightness] for descendant Material widgets. + static Brightness brightnessOf(BuildContext context) { + final _InheritedTheme? inheritedTheme = + context.dependOnInheritedWidgetOfExactType<_InheritedTheme>(); + return inheritedTheme?.theme.data.brightness ?? MediaQuery.platformBrightnessOf(context); + } + + /// Retrieves the [Brightness] to use for descendant Material widgets, based + /// on the value of [ThemeData.brightness] in the given [context]. + /// + /// If no [InheritedTheme] or [MediaQuery] can be found in the given [context], it will + /// return null. + /// + /// See also: + /// + /// * [ThemeData.brightness], the property that takes precedence over + /// [MediaQueryData.platformBrightness] for descendant Material widgets. + /// * [brightnessOf], which return a default value if no valid [InheritedTheme] or + /// [MediaQuery] exists, instead of returning null. + static Brightness? maybeBrightnessOf(BuildContext context) { + final _InheritedTheme? inheritedTheme = + context.dependOnInheritedWidgetOfExactType<_InheritedTheme>(); + return inheritedTheme?.theme.data.brightness ?? MediaQuery.maybePlatformBrightnessOf(context); + } + @override Widget build(BuildContext context) { return _InheritedTheme( diff --git a/packages/flutter/test/material/theme_test.dart b/packages/flutter/test/material/theme_test.dart index 9d71b8c5dc..ee9b9aeedd 100644 --- a/packages/flutter/test/material/theme_test.dart +++ b/packages/flutter/test/material/theme_test.dart @@ -47,6 +47,92 @@ void main() { expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.dark)); }); + group('Theme.brightnessOf', () { + testWidgets('return correct brightness when just media query is given', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MediaQuery( + data: MediaQueryData(platformBrightness: Brightness.dark), + child: SizedBox(), + ), + ); + + expect(Theme.brightnessOf(tester.element(find.byType(SizedBox))), equals(Brightness.dark)); + }); + + testWidgets('return correct brightness with overriding theme brightness over media query', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(platformBrightness: Brightness.dark), + child: Theme(data: ThemeData(brightness: Brightness.light), child: const SizedBox()), + ), + ); + + expect(Theme.brightnessOf(tester.element(find.byType(SizedBox))), equals(Brightness.light)); + }); + + testWidgets('returns Brightness.light when no theme or media query is present', ( + WidgetTester tester, + ) async { + // Prevent the implicitly added View from adding a MediaQuery + await tester.pumpWidget( + RawView(view: FakeFlutterView(tester.view, viewId: 77), child: const SizedBox()), + wrapWithView: false, + ); + + expect(Theme.brightnessOf(tester.element(find.byType(SizedBox))), equals(Brightness.light)); + }); + }); + + group('Theme.maybeBrightnessOf', () { + testWidgets('return correct brightness when just media query is given', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + const MediaQuery( + data: MediaQueryData(platformBrightness: Brightness.dark), + child: SizedBox(), + ), + ); + + expect( + Theme.maybeBrightnessOf(tester.element(find.byType(SizedBox))), + equals(Brightness.dark), + ); + }); + + testWidgets('return correct brightness with overriding theme brightness over media query', ( + WidgetTester tester, + ) async { + await tester.pumpWidget( + MediaQuery( + data: const MediaQueryData(platformBrightness: Brightness.dark), + child: Theme(data: ThemeData(brightness: Brightness.light), child: const SizedBox()), + ), + ); + + expect( + Theme.maybeBrightnessOf(tester.element(find.byType(SizedBox))), + equals(Brightness.light), + ); + }); + + testWidgets('returns null when no theme or media query is present', ( + WidgetTester tester, + ) async { + // Prevent the implicitly added View from adding a MediaQuery + await tester.pumpWidget( + RawView(view: FakeFlutterView(tester.view, viewId: 77), child: const SizedBox()), + wrapWithView: false, + ); + + expect(Theme.maybeBrightnessOf(tester.element(find.byType(SizedBox))), isNull); + }); + }); + testWidgets('Theme overrides selection style', (WidgetTester tester) async { final Key key = UniqueKey(); const Color defaultSelectionColor = Color(0x11111111); @@ -1242,3 +1328,11 @@ class _TextStyleProxy implements TextStyle { throw UnimplementedError(); } } + +class FakeFlutterView extends TestFlutterView { + FakeFlutterView(TestFlutterView view, {required this.viewId}) + : super(view: view, display: view.display, platformDispatcher: view.platformDispatcher); + + @override + final int viewId; +}