diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index 141eea923c..e11e1eebd6 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -5,7 +5,7 @@ /// @docImport 'package:flutter/material.dart'; library; -import 'dart:ui' show Color, lerpDouble; +import 'dart:ui' show Color, SystemColor, SystemColorPalette, lerpDouble; import 'package:flutter/cupertino.dart'; import 'package:flutter/foundation.dart'; @@ -32,8 +32,10 @@ import 'dialog_theme.dart'; import 'divider_theme.dart'; import 'drawer_theme.dart'; import 'dropdown_menu_theme.dart'; +import 'elevated_button.dart'; import 'elevated_button_theme.dart'; import 'expansion_tile_theme.dart'; +import 'filled_button.dart'; import 'filled_button_theme.dart'; import 'floating_action_button_theme.dart'; import 'icon_button_theme.dart'; @@ -50,6 +52,7 @@ import 'menu_theme.dart'; import 'navigation_bar_theme.dart'; import 'navigation_drawer_theme.dart'; import 'navigation_rail_theme.dart'; +import 'outlined_button.dart'; import 'outlined_button_theme.dart'; import 'page_transitions_theme.dart'; import 'popup_menu_theme.dart'; @@ -63,6 +66,7 @@ import 'slider_theme.dart'; import 'snack_bar_theme.dart'; import 'switch_theme.dart'; import 'tab_bar_theme.dart'; +import 'text_button.dart'; import 'text_button_theme.dart'; import 'text_selection_theme.dart'; import 'text_theme.dart'; @@ -246,6 +250,11 @@ class ThemeData with Diagnosticable { /// a component theme parameter like [sliderTheme], [toggleButtonsTheme], /// or [bottomNavigationBarTheme]. /// + /// When [useSystemColors] is true and the platform supports system colors, then the system colors + /// will be used to override certain theme colors. The [colorScheme], [textTheme], + /// [elevatedButtonTheme], [outlinedButtonTheme], [textButtonTheme], [filledButtonTheme], and + /// [floatingActionButtonTheme] are overriden by the system colors. + /// /// See also: /// /// * [ThemeData.from], which creates a ThemeData from a [ColorScheme]. @@ -270,6 +279,7 @@ class ThemeData with Diagnosticable { ScrollbarThemeData? scrollbarTheme, InteractiveInkFeatureFactory? splashFactory, bool? useMaterial3, + bool? useSystemColors, VisualDensity? visualDensity, // COLOR ColorScheme? colorScheme, @@ -387,6 +397,7 @@ class ThemeData with Diagnosticable { scrollbarTheme ??= const ScrollbarThemeData(); visualDensity ??= VisualDensity.defaultDensityForPlatform(platform); useMaterial3 ??= true; + useSystemColors ??= false; final bool useInkSparkle = platform == TargetPlatform.android && !kIsWeb; splashFactory ??= useMaterial3 @@ -557,7 +568,8 @@ class ThemeData with Diagnosticable { buttonBarTheme ??= const ButtonBarThemeData(); dialogBackgroundColor ??= isDark ? Colors.grey[800]! : Colors.white; indicatorColor ??= colorScheme.secondary == primaryColor ? Colors.white : colorScheme.secondary; - return ThemeData.raw( + + ThemeData theme = ThemeData.raw( // For the sanity of the reader, make sure these properties are in the same // order in every place that they are separated by section comments (e.g. // GENERAL CONFIGURATION). Each section except for deprecations should be @@ -651,6 +663,11 @@ class ThemeData with Diagnosticable { dialogBackgroundColor: dialogBackgroundColor, indicatorColor: indicatorColor, ); + + if (useSystemColors) { + theme = theme._overrideWithSystemColors(); + } + return theme; } /// Create a [ThemeData] given a set of exact values. Most values must be @@ -1763,6 +1780,115 @@ class ThemeData with Diagnosticable { }); } + ThemeData _overrideWithSystemColors() { + if (!SystemColor.platformProvidesSystemColors) { + return this; + } + + final SystemColorPalette systemColors = + brightness == Brightness.dark ? SystemColor.dark : SystemColor.light; + + ThemeData theme = this; + + theme = theme.copyWith( + colorScheme: colorScheme.copyWith( + secondary: systemColors.accentColor.value, + onSecondary: systemColors.accentColorText.value, + surface: systemColors.canvas.value, + onSurface: systemColors.canvasText.value, + ), + textTheme: textTheme.apply( + displayColor: systemColors.canvasText.value, + bodyColor: systemColors.canvasText.value, + ), + ); + + final bool overrideButtons = + systemColors.buttonFace.value != null || + systemColors.buttonBorder.value != null || + systemColors.buttonText.value != null; + + if (overrideButtons) { + theme = theme.copyWith( + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + foregroundColor: systemColors.buttonText.value, + backgroundColor: systemColors.buttonFace.value, + side: + systemColors.buttonBorder.value == null + ? null + : BorderSide(color: systemColors.buttonBorder.value!), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: systemColors.buttonText.value, + backgroundColor: systemColors.buttonFace.value, + side: + systemColors.buttonBorder.value == null + ? null + : BorderSide(color: systemColors.buttonBorder.value!), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: systemColors.buttonText.value, + backgroundColor: systemColors.buttonFace.value, + side: + systemColors.buttonBorder.value == null + ? null + : BorderSide(color: systemColors.buttonBorder.value!), + ), + ), + filledButtonTheme: FilledButtonThemeData( + style: FilledButton.styleFrom( + foregroundColor: systemColors.buttonText.value, + backgroundColor: systemColors.buttonFace.value, + side: + systemColors.buttonBorder.value == null + ? null + : BorderSide(color: systemColors.buttonBorder.value!), + ), + ), + floatingActionButtonTheme: FloatingActionButtonThemeData( + backgroundColor: systemColors.buttonFace.value, + foregroundColor: systemColors.buttonText.value, + ), + ); + } + + final bool overrideInputDecoration = + systemColors.field.value != null || systemColors.fieldText.value != null; + + if (overrideInputDecoration) { + theme = theme.copyWith( + inputDecorationTheme: inputDecorationTheme.copyWith( + fillColor: systemColors.field.value, + labelStyle: + inputDecorationTheme.labelStyle?.copyWith(color: systemColors.fieldText.value) ?? + TextStyle(color: systemColors.fieldText.value), + hintStyle: + inputDecorationTheme.hintStyle?.copyWith(color: systemColors.fieldText.value) ?? + TextStyle(color: systemColors.fieldText.value), + helperStyle: + inputDecorationTheme.helperStyle?.copyWith(color: systemColors.fieldText.value) ?? + TextStyle(color: systemColors.fieldText.value), + prefixStyle: + inputDecorationTheme.prefixStyle?.copyWith(color: systemColors.fieldText.value) ?? + TextStyle(color: systemColors.fieldText.value), + suffixStyle: + inputDecorationTheme.suffixStyle?.copyWith(color: systemColors.fieldText.value) ?? + TextStyle(color: systemColors.fieldText.value), + counterStyle: + inputDecorationTheme.counterStyle?.copyWith(color: systemColors.fieldText.value) ?? + TextStyle(color: systemColors.fieldText.value), + ), + ); + } + + return theme; + } + /// Linearly interpolate between two themes. /// /// {@macro dart.ui.shadow.lerp} diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index b18a241934..5de641c1e8 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +import 'dart:ui'; + import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -377,6 +379,418 @@ void main() { expect(theme.applyElevationOverlayColor, false); }); + test( + 'ThemeData applies light system colors when useSystemColors is true', + () { + final ThemeData theme = ThemeData( + colorSchemeSeed: Colors.orange, + brightness: Brightness.light, + useSystemColors: true, + ); + + expect( + theme.colorScheme.secondary, + SystemColor.light.accentColor.value, + skip: !SystemColor.light.accentColor.isSupported, // Color not always supported. + reason: 'Theme secondary color did not match system accent color', + ); + expect( + theme.colorScheme.onSecondary, + SystemColor.light.accentColorText.value, + skip: !SystemColor.light.accentColorText.isSupported, // Color not always supported. + reason: 'Theme onSecondary color did not match system accent color text', + ); + expect( + theme.colorScheme.surface, + SystemColor.light.canvas.value, + skip: !SystemColor.light.canvas.isSupported, // Color not always supported. + reason: 'Theme surface color did not match system canvas color', + ); + expect( + theme.colorScheme.onSurface, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'Theme onSurface color did not match system canvas color text', + ); + + // Text theme + + expect( + theme.textTheme.displayLarge?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme displayLarge color did not match system text color', + ); + expect( + theme.textTheme.displayMedium?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme displayMedium color did not match system text color', + ); + expect( + theme.textTheme.displaySmall?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme displaySmall color did not match system text color', + ); + expect( + theme.textTheme.headlineLarge?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme headlineLarge color did not match system text color', + ); + expect( + theme.textTheme.headlineMedium?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme headlineMedium color did not match system text color', + ); + expect( + theme.textTheme.headlineSmall?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme headlineSmall color did not match system text color', + ); + expect( + theme.textTheme.titleLarge?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme titleLarge color did not match system text color', + ); + expect( + theme.textTheme.titleMedium?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme titleMedium color did not match system text color', + ); + expect( + theme.textTheme.titleSmall?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme titleSmall color did not match system text color', + ); + expect( + theme.textTheme.bodyLarge?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme bodyLarge color did not match system text color', + ); + expect( + theme.textTheme.bodyMedium?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme bodyMedium color did not match system text color', + ); + expect( + theme.textTheme.bodySmall?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme bodySmall color did not match system text color', + ); + expect( + theme.textTheme.labelLarge?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme labelLarge color did not match system text color', + ); + expect( + theme.textTheme.labelMedium?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme labelMedium color did not match system text color', + ); + expect( + theme.textTheme.labelSmall?.color, + SystemColor.light.canvasText.value, + skip: !SystemColor.light.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme labelSmall color did not match system text color', + ); + + // Button themes + + expect( + theme.elevatedButtonTheme.style?.foregroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.light.buttonText.value, + skip: !SystemColor.light.buttonText.isSupported, // Color not always supported. + reason: 'ElevatedButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.elevatedButtonTheme.style?.backgroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.light.buttonFace.value, + skip: !SystemColor.light.buttonFace.isSupported, // Color not always supported. + reason: 'ElevatedButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.textButtonTheme.style?.foregroundColor?.resolve({WidgetState.pressed}), + SystemColor.light.buttonText.value, + skip: !SystemColor.light.buttonText.isSupported, // Color not always supported. + reason: 'TextButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.textButtonTheme.style?.backgroundColor?.resolve({WidgetState.pressed}), + SystemColor.light.buttonFace.value, + skip: !SystemColor.light.buttonFace.isSupported, // Color not always supported. + reason: 'TextButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.outlinedButtonTheme.style?.foregroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.light.buttonText.value, + skip: !SystemColor.light.buttonText.isSupported, // Color not always supported. + reason: 'OutlinedButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.outlinedButtonTheme.style?.backgroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.light.buttonFace.value, + skip: !SystemColor.light.buttonFace.isSupported, // Color not always supported. + reason: 'OutlinedButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.filledButtonTheme.style?.foregroundColor?.resolve({WidgetState.pressed}), + SystemColor.light.buttonText.value, + skip: !SystemColor.light.buttonText.isSupported, // Color not always supported. + reason: 'FilledButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.filledButtonTheme.style?.backgroundColor?.resolve({WidgetState.pressed}), + SystemColor.light.buttonFace.value, + skip: !SystemColor.light.buttonFace.isSupported, // Color not always supported. + reason: 'FilledButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.floatingActionButtonTheme.foregroundColor, + SystemColor.light.buttonText.value, + skip: !SystemColor.light.buttonFace.isSupported, // Color not always supported. + reason: 'FloatingActionButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.floatingActionButtonTheme.backgroundColor, + SystemColor.light.buttonFace.value, + skip: !SystemColor.light.buttonFace.isSupported, // Color not always supported. + reason: 'FloatingActionButtonTheme backgroundColor did not match system button face color', + ); + }, + // Only run this test on platforms that provide system colors. + skip: !SystemColor.platformProvidesSystemColors, + ); + + test( + 'ThemeData applies dark system colors when useSystemColors is true', + () { + final ThemeData theme = ThemeData( + colorSchemeSeed: Colors.orange, + brightness: Brightness.dark, + useSystemColors: true, + ); + + expect( + theme.colorScheme.secondary, + SystemColor.dark.accentColor.value, + skip: !SystemColor.dark.accentColor.isSupported, // Color not always supported. + reason: 'Theme secondary color did not match system accent color', + ); + expect( + theme.colorScheme.onSecondary, + SystemColor.dark.accentColorText.value, + skip: !SystemColor.dark.accentColorText.isSupported, // Color not always supported. + reason: 'Theme onSecondary color did not match system accent color text', + ); + expect( + theme.colorScheme.surface, + SystemColor.dark.canvas.value, + skip: !SystemColor.dark.canvas.isSupported, // Color not always supported. + reason: 'Theme surface color did not match system canvas color', + ); + expect( + theme.colorScheme.onSurface, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'Theme onSurface color did not match system canvas color text', + ); + + // Text theme + + expect( + theme.textTheme.displayLarge?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme displayLarge color did not match system text color', + ); + expect( + theme.textTheme.displayMedium?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme displayMedium color did not match system text color', + ); + expect( + theme.textTheme.displaySmall?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme displaySmall color did not match system text color', + ); + expect( + theme.textTheme.headlineLarge?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme headlineLarge color did not match system text color', + ); + expect( + theme.textTheme.headlineMedium?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme headlineMedium color did not match system text color', + ); + expect( + theme.textTheme.headlineSmall?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme headlineSmall color did not match system text color', + ); + expect( + theme.textTheme.titleLarge?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme titleLarge color did not match system text color', + ); + expect( + theme.textTheme.titleMedium?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme titleMedium color did not match system text color', + ); + expect( + theme.textTheme.titleSmall?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme titleSmall color did not match system text color', + ); + expect( + theme.textTheme.bodyLarge?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme bodyLarge color did not match system text color', + ); + expect( + theme.textTheme.bodyMedium?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme bodyMedium color did not match system text color', + ); + expect( + theme.textTheme.bodySmall?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme bodySmall color did not match system text color', + ); + expect( + theme.textTheme.labelLarge?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme labelLarge color did not match system text color', + ); + expect( + theme.textTheme.labelMedium?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme labelMedium color did not match system text color', + ); + expect( + theme.textTheme.labelSmall?.color, + SystemColor.dark.canvasText.value, + skip: !SystemColor.dark.canvasText.isSupported, // Color not always supported. + reason: 'TextTheme labelSmall color did not match system text color', + ); + + // Button themes + + expect( + theme.elevatedButtonTheme.style?.foregroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.dark.buttonText.value, + skip: !SystemColor.dark.buttonText.isSupported, // Color not always supported. + reason: 'ElevatedButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.elevatedButtonTheme.style?.backgroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.dark.buttonFace.value, + skip: !SystemColor.dark.buttonFace.isSupported, // Color not always supported. + reason: 'ElevatedButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.textButtonTheme.style?.foregroundColor?.resolve({WidgetState.pressed}), + SystemColor.dark.buttonText.value, + skip: !SystemColor.dark.buttonText.isSupported, // Color not always supported. + reason: 'TextButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.textButtonTheme.style?.backgroundColor?.resolve({WidgetState.pressed}), + SystemColor.dark.buttonFace.value, + skip: !SystemColor.dark.buttonFace.isSupported, // Color not always supported. + reason: 'TextButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.outlinedButtonTheme.style?.foregroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.dark.buttonText.value, + skip: !SystemColor.dark.buttonText.isSupported, // Color not always supported. + reason: 'OutlinedButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.outlinedButtonTheme.style?.backgroundColor?.resolve({ + WidgetState.pressed, + }), + SystemColor.dark.buttonFace.value, + skip: !SystemColor.dark.buttonFace.isSupported, // Color not always supported. + reason: 'OutlinedButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.filledButtonTheme.style?.foregroundColor?.resolve({WidgetState.pressed}), + SystemColor.dark.buttonText.value, + skip: !SystemColor.dark.buttonText.isSupported, // Color not always supported. + reason: 'FilledButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.filledButtonTheme.style?.backgroundColor?.resolve({WidgetState.pressed}), + SystemColor.dark.buttonFace.value, + skip: !SystemColor.dark.buttonFace.isSupported, // Color not always supported. + reason: 'FilledButtonTheme backgroundColor did not match system button face color', + ); + + expect( + theme.floatingActionButtonTheme.foregroundColor, + SystemColor.dark.buttonText.value, + skip: !SystemColor.dark.buttonFace.isSupported, // Color not always supported. + reason: 'FloatingActionButtonTheme foregroundColor did not match system button text color', + ); + expect( + theme.floatingActionButtonTheme.backgroundColor, + SystemColor.dark.buttonFace.value, + skip: !SystemColor.dark.buttonFace.isSupported, // Color not always supported. + reason: 'FloatingActionButtonTheme backgroundColor did not match system button face color', + ); + }, + // Only run this test on platforms that provide system colors. + skip: !SystemColor.platformProvidesSystemColors, + ); + test( 'ThemeData.light() can generate a default M3 light colorScheme when useMaterial3 is true', () {