Unverified Commit 52fd2412 authored by chunhtai's avatar chunhtai Committed by GitHub

Reland "Add default selection style (#100719)" (#101954)

parent 0d087562
...@@ -43,6 +43,12 @@ import 'theme.dart'; ...@@ -43,6 +43,12 @@ import 'theme.dart';
/// widget, which the [CupertinoApp] composes. If you use Material widgets, a /// widget, which the [CupertinoApp] composes. If you use Material widgets, a
/// [MaterialApp] also creates the needed dependencies for Cupertino widgets. /// [MaterialApp] also creates the needed dependencies for Cupertino widgets.
/// ///
/// {@template flutter.cupertino.CupertinoApp.defaultSelectionStyle}
/// The [CupertinoApp] automatically creates a [DefaultSelectionStyle] with
/// selectionColor sets to [CupertinoThemeData.primaryColor] with 0.2 opacity
/// and cursorColor sets to [CupertinoThemeData.primaryColor].
/// {@endtemplate}
///
/// Use this widget with caution on Android since it may produce behaviors /// Use this widget with caution on Android since it may produce behaviors
/// Android users are not expecting such as: /// Android users are not expecting such as:
/// ///
...@@ -584,10 +590,14 @@ class _CupertinoAppState extends State<CupertinoApp> { ...@@ -584,10 +590,14 @@ class _CupertinoAppState extends State<CupertinoApp> {
data: CupertinoUserInterfaceLevelData.base, data: CupertinoUserInterfaceLevelData.base,
child: CupertinoTheme( child: CupertinoTheme(
data: effectiveThemeData, data: effectiveThemeData,
child: HeroControllerScope( child: DefaultSelectionStyle(
controller: _heroController, selectionColor: effectiveThemeData.primaryColor.withOpacity(0.2),
child: Builder( cursorColor: effectiveThemeData.primaryColor,
builder: _buildWidgetApp, child: HeroControllerScope(
controller: _heroController,
child: Builder(
builder: _buildWidgetApp,
),
), ),
), ),
), ),
......
...@@ -710,7 +710,8 @@ class CupertinoTextField extends StatefulWidget { ...@@ -710,7 +710,8 @@ class CupertinoTextField extends StatefulWidget {
/// The color to use when painting the cursor. /// The color to use when painting the cursor.
/// ///
/// Defaults to the [CupertinoThemeData.primaryColor] of the ambient theme, /// Defaults to the [DefaultSelectionStyle.cursorColor]. If that color is
/// null, it uses the [CupertinoThemeData.primaryColor] of the ambient theme,
/// which itself defaults to [CupertinoColors.activeBlue] in the light theme /// which itself defaults to [CupertinoColors.activeBlue] in the light theme
/// and [CupertinoColors.activeOrange] in the dark theme. /// and [CupertinoColors.activeOrange] in the dark theme.
final Color? cursorColor; final Color? cursorColor;
...@@ -1178,7 +1179,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1178,7 +1179,11 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
final TextStyle placeholderStyle = textStyle.merge(resolvedPlaceholderStyle); final TextStyle placeholderStyle = textStyle.merge(resolvedPlaceholderStyle);
final Brightness keyboardAppearance = widget.keyboardAppearance ?? CupertinoTheme.brightnessOf(context); final Brightness keyboardAppearance = widget.keyboardAppearance ?? CupertinoTheme.brightnessOf(context);
final Color cursorColor = CupertinoDynamicColor.maybeResolve(widget.cursorColor, context) ?? themeData.primaryColor; final Color cursorColor = CupertinoDynamicColor.maybeResolve(
widget.cursorColor ?? DefaultSelectionStyle.of(context).cursorColor,
context,
) ?? themeData.primaryColor;
final Color disabledColor = CupertinoDynamicColor.resolve(_kDisabledBackground, context); final Color disabledColor = CupertinoDynamicColor.resolve(_kDisabledBackground, context);
final Color? decorationColor = CupertinoDynamicColor.maybeResolve(widget.decoration?.color, context); final Color? decorationColor = CupertinoDynamicColor.maybeResolve(widget.decoration?.color, context);
...@@ -1206,7 +1211,10 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio ...@@ -1206,7 +1211,10 @@ class _CupertinoTextFieldState extends State<CupertinoTextField> with Restoratio
color: enabled ? decorationColor : disabledColor, color: enabled ? decorationColor : disabledColor,
); );
final Color selectionColor = CupertinoTheme.of(context).primaryColor.withOpacity(0.2); final Color selectionColor = CupertinoDynamicColor.maybeResolve(
DefaultSelectionStyle.of(context).selectionColor,
context,
) ?? CupertinoTheme.of(context).primaryColor.withOpacity(0.2);
final Widget paddedEditable = Padding( final Widget paddedEditable = Padding(
padding: widget.padding, padding: widget.padding,
......
...@@ -82,6 +82,14 @@ enum ThemeMode { ...@@ -82,6 +82,14 @@ enum ThemeMode {
/// This widget also configures the observer of the top-level [Navigator] (if /// This widget also configures the observer of the top-level [Navigator] (if
/// any) to perform [Hero] animations. /// any) to perform [Hero] animations.
/// ///
/// {@template flutter.material.MaterialApp.defaultSelectionStyle}
/// The [MaterialApp] automatically creates a [DefaultSelectionStyle]. It uses
/// the colors in the [ThemeData.textSelectionTheme] if they are not null;
/// otherwise, the [MaterialApp] sets [DefaultSelectionStyle.selectionColor] to
/// [ColorScheme.primary] with 0.4 opacity and
/// [DefaultSelectionStyle.cursorColor] to [ColorScheme.primary].
/// {@endtemplate}
///
/// If [home], [routes], [onGenerateRoute], and [onUnknownRoute] are all null, /// If [home], [routes], [onGenerateRoute], and [onUnknownRoute] are all null,
/// and [builder] is not null, then no [Navigator] is created. /// and [builder] is not null, then no [Navigator] is created.
/// ///
...@@ -871,29 +879,35 @@ class _MaterialAppState extends State<MaterialApp> { ...@@ -871,29 +879,35 @@ class _MaterialAppState extends State<MaterialApp> {
theme = widget.highContrastTheme; theme = widget.highContrastTheme;
} }
theme ??= widget.theme ?? ThemeData.light(); theme ??= widget.theme ?? ThemeData.light();
final Color effectiveSelectionColor = theme.textSelectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
final Color effectiveCursorColor = theme.textSelectionTheme.cursorColor ?? theme.colorScheme.primary;
return ScaffoldMessenger( return ScaffoldMessenger(
key: widget.scaffoldMessengerKey, key: widget.scaffoldMessengerKey,
child: AnimatedTheme( child: DefaultSelectionStyle(
data: theme, selectionColor: effectiveSelectionColor,
child: widget.builder != null cursorColor: effectiveCursorColor,
? Builder( child: AnimatedTheme(
builder: (BuildContext context) { data: theme,
// Why are we surrounding a builder with a builder? child: widget.builder != null
// ? Builder(
// The widget.builder may contain code that invokes builder: (BuildContext context) {
// Theme.of(), which should return the theme we selected // Why are we surrounding a builder with a builder?
// above in AnimatedTheme. However, if we invoke //
// widget.builder() directly as the child of AnimatedTheme // The widget.builder may contain code that invokes
// then there is no Context separating them, and the // Theme.of(), which should return the theme we selected
// widget.builder() will not find the theme. Therefore, we // above in AnimatedTheme. However, if we invoke
// surround widget.builder with yet another builder so that // widget.builder() directly as the child of AnimatedTheme
// a context separates them and Theme.of() correctly // then there is no Context separating them, and the
// resolves to the theme we passed to AnimatedTheme. // widget.builder() will not find the theme. Therefore, we
return widget.builder!(context, child); // surround widget.builder with yet another builder so that
}, // a context separates them and Theme.of() correctly
) // resolves to the theme we passed to AnimatedTheme.
: child ?? const SizedBox.shrink(), return widget.builder!(context, child);
},
)
: child ?? const SizedBox.shrink(),
),
), ),
); );
} }
......
...@@ -12,7 +12,6 @@ import 'package:flutter/rendering.dart'; ...@@ -12,7 +12,6 @@ import 'package:flutter/rendering.dart';
import 'desktop_text_selection.dart'; import 'desktop_text_selection.dart';
import 'feedback.dart'; import 'feedback.dart';
import 'text_selection.dart'; import 'text_selection.dart';
import 'text_selection_theme.dart';
import 'theme.dart'; import 'theme.dart';
/// An eyeballed value that moves the cursor slightly left of where it is /// An eyeballed value that moves the cursor slightly left of where it is
...@@ -357,9 +356,14 @@ class SelectableText extends StatefulWidget { ...@@ -357,9 +356,14 @@ class SelectableText extends StatefulWidget {
/// {@macro flutter.widgets.editableText.cursorRadius} /// {@macro flutter.widgets.editableText.cursorRadius}
final Radius? cursorRadius; final Radius? cursorRadius;
/// The color to use when painting the cursor. /// The color of the cursor.
/// ///
/// Defaults to the theme's `cursorColor` when null. /// The cursor indicates the current text insertion point.
///
/// If null then [DefaultSelectionStyle.cursorColor] is used. If that is also
/// null and [ThemeData.platform] is [TargetPlatform.iOS] or
/// [TargetPlatform.macOS], then [CupertinoThemeData.primaryColor] is used.
/// Otherwise [ColorScheme.primary] of [ThemeData.colorScheme] is used.
final Color? cursorColor; final Color? cursorColor;
/// Controls how tall the selection highlight boxes are computed to be. /// Controls how tall the selection highlight boxes are computed to be.
...@@ -595,14 +599,14 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -595,14 +599,14 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
); );
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
final FocusNode focusNode = _effectiveFocusNode; final FocusNode focusNode = _effectiveFocusNode;
TextSelectionControls? textSelectionControls = widget.selectionControls; TextSelectionControls? textSelectionControls = widget.selectionControls;
final bool paintCursorAboveText; final bool paintCursorAboveText;
final bool cursorOpacityAnimates; final bool cursorOpacityAnimates;
Offset? cursorOffset; Offset? cursorOffset;
Color? cursorColor = widget.cursorColor; final Color cursorColor;
final Color selectionColor; final Color selectionColor;
Radius? cursorRadius = widget.cursorRadius; Radius? cursorRadius = widget.cursorRadius;
...@@ -613,8 +617,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -613,8 +617,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
textSelectionControls ??= cupertinoTextSelectionControls; textSelectionControls ??= cupertinoTextSelectionControls;
paintCursorAboveText = true; paintCursorAboveText = true;
cursorOpacityAnimates = true; cursorOpacityAnimates = true;
cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0); cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
break; break;
...@@ -625,8 +629,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -625,8 +629,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
textSelectionControls ??= cupertinoDesktopTextSelectionControls; textSelectionControls ??= cupertinoDesktopTextSelectionControls;
paintCursorAboveText = true; paintCursorAboveText = true;
cursorOpacityAnimates = true; cursorOpacityAnimates = true;
cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0); cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
break; break;
...@@ -637,8 +641,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -637,8 +641,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
textSelectionControls ??= materialTextSelectionControls; textSelectionControls ??= materialTextSelectionControls;
paintCursorAboveText = false; paintCursorAboveText = false;
cursorOpacityAnimates = false; cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
break; break;
case TargetPlatform.linux: case TargetPlatform.linux:
...@@ -647,8 +651,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio ...@@ -647,8 +651,8 @@ class _SelectableTextState extends State<SelectableText> implements TextSelectio
textSelectionControls ??= desktopTextSelectionControls; textSelectionControls ??= desktopTextSelectionControls;
paintCursorAboveText = false; paintCursorAboveText = false;
cursorOpacityAnimates = false; cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
break; break;
} }
......
...@@ -19,7 +19,6 @@ import 'material_localizations.dart'; ...@@ -19,7 +19,6 @@ import 'material_localizations.dart';
import 'material_state.dart'; import 'material_state.dart';
import 'selectable_text.dart' show iOSHorizontalOffset; import 'selectable_text.dart' show iOSHorizontalOffset;
import 'text_selection.dart'; import 'text_selection.dart';
import 'text_selection_theme.dart';
import 'theme.dart'; import 'theme.dart';
export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType; export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType;
...@@ -614,7 +613,7 @@ class TextField extends StatefulWidget { ...@@ -614,7 +613,7 @@ class TextField extends StatefulWidget {
/// the field. /// the field.
/// ///
/// If this is null it will default to the ambient /// If this is null it will default to the ambient
/// [TextSelectionThemeData.cursorColor]. If that is null, and the /// [DefaultSelectionStyle.cursorColor]. If that is null, and the
/// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS] /// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS]
/// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use /// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use
/// the value of [ColorScheme.primary] of [ThemeData.colorScheme]. /// the value of [ColorScheme.primary] of [ThemeData.colorScheme].
...@@ -1116,7 +1115,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1116,7 +1115,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
); );
final ThemeData theme = Theme.of(context); final ThemeData theme = Theme.of(context);
final TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
final TextStyle style = theme.textTheme.subtitle1!.merge(widget.style); final TextStyle style = theme.textTheme.subtitle1!.merge(widget.style);
final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness; final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness;
final TextEditingController controller = _effectiveController; final TextEditingController controller = _effectiveController;
...@@ -1134,7 +1133,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1134,7 +1133,7 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
final bool paintCursorAboveText; final bool paintCursorAboveText;
final bool cursorOpacityAnimates; final bool cursorOpacityAnimates;
Offset? cursorOffset; Offset? cursorOffset;
Color? cursorColor = widget.cursorColor; final Color cursorColor;
final Color selectionColor; final Color selectionColor;
Color? autocorrectionTextRectColor; Color? autocorrectionTextRectColor;
Radius? cursorRadius = widget.cursorRadius; Radius? cursorRadius = widget.cursorRadius;
...@@ -1147,8 +1146,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1147,8 +1146,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
textSelectionControls ??= cupertinoTextSelectionControls; textSelectionControls ??= cupertinoTextSelectionControls;
paintCursorAboveText = true; paintCursorAboveText = true;
cursorOpacityAnimates = true; cursorOpacityAnimates = true;
cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0); cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
autocorrectionTextRectColor = selectionColor; autocorrectionTextRectColor = selectionColor;
...@@ -1160,8 +1159,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1160,8 +1159,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
textSelectionControls ??= cupertinoDesktopTextSelectionControls; textSelectionControls ??= cupertinoDesktopTextSelectionControls;
paintCursorAboveText = true; paintCursorAboveText = true;
cursorOpacityAnimates = true; cursorOpacityAnimates = true;
cursorColor ??= selectionTheme.cursorColor ?? cupertinoTheme.primaryColor; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionTheme.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0); cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
handleDidGainAccessibilityFocus = () { handleDidGainAccessibilityFocus = () {
...@@ -1178,8 +1177,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1178,8 +1177,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
textSelectionControls ??= materialTextSelectionControls; textSelectionControls ??= materialTextSelectionControls;
paintCursorAboveText = false; paintCursorAboveText = false;
cursorOpacityAnimates = false; cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
break; break;
case TargetPlatform.linux: case TargetPlatform.linux:
...@@ -1187,8 +1186,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1187,8 +1186,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
textSelectionControls ??= desktopTextSelectionControls; textSelectionControls ??= desktopTextSelectionControls;
paintCursorAboveText = false; paintCursorAboveText = false;
cursorOpacityAnimates = false; cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
break; break;
case TargetPlatform.windows: case TargetPlatform.windows:
...@@ -1196,8 +1195,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements ...@@ -1196,8 +1195,8 @@ class _TextFieldState extends State<TextField> with RestorationMixin implements
textSelectionControls ??= desktopTextSelectionControls; textSelectionControls ??= desktopTextSelectionControls;
paintCursorAboveText = false; paintCursorAboveText = false;
cursorOpacityAnimates = false; cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; cursorColor = widget.cursorColor ?? selectionStyle.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40);
handleDidGainAccessibilityFocus = () { handleDidGainAccessibilityFocus = () {
// Automatically activate the TextField when it receives accessibility focus. // Automatically activate the TextField when it receives accessibility focus.
if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) {
......
...@@ -129,6 +129,9 @@ class TextSelectionThemeData with Diagnosticable { ...@@ -129,6 +129,9 @@ class TextSelectionThemeData with Diagnosticable {
/// ) /// )
/// ``` /// ```
/// {@end-tool} /// {@end-tool}
///
/// This widget also creates a [DefaultSelectionStyle] for its subtree with
/// [data].
class TextSelectionTheme extends InheritedTheme { class TextSelectionTheme extends InheritedTheme {
/// Creates a text selection theme widget that specifies the text /// Creates a text selection theme widget that specifies the text
/// selection properties for all widgets below it in the widget tree. /// selection properties for all widgets below it in the widget tree.
...@@ -137,12 +140,29 @@ class TextSelectionTheme extends InheritedTheme { ...@@ -137,12 +140,29 @@ class TextSelectionTheme extends InheritedTheme {
const TextSelectionTheme({ const TextSelectionTheme({
super.key, super.key,
required this.data, required this.data,
required super.child, required Widget child,
}) : assert(data != null); }) : assert(data != null),
_child = child,
// See `get child` override below.
super(child: const _NullWidget());
/// The properties for descendant [TextField] and [SelectableText] widgets. /// The properties for descendant [TextField] and [SelectableText] widgets.
final TextSelectionThemeData data; final TextSelectionThemeData data;
// Overriding the getter to insert `DefaultSelectionStyle` into the subtree
// without breaking API. In general, this approach should be avoided
// because it relies on an implementation detail of ProxyWidget. This
// workaround is necessary because TextSelectionTheme is const.
@override
Widget get child {
return DefaultSelectionStyle(
selectionColor: data.selectionColor,
cursorColor: data.cursorColor,
child: _child,
);
}
final Widget _child;
/// Returns the [data] from the closest [TextSelectionTheme] ancestor. If /// Returns the [data] from the closest [TextSelectionTheme] ancestor. If
/// there is no ancestor, it returns [ThemeData.textSelectionTheme]. /// there is no ancestor, it returns [ThemeData.textSelectionTheme].
/// Applications can assume that the returned value will not be null. /// Applications can assume that the returned value will not be null.
...@@ -165,3 +185,10 @@ class TextSelectionTheme extends InheritedTheme { ...@@ -165,3 +185,10 @@ class TextSelectionTheme extends InheritedTheme {
@override @override
bool updateShouldNotify(TextSelectionTheme oldWidget) => data != oldWidget.data; bool updateShouldNotify(TextSelectionTheme oldWidget) => data != oldWidget.data;
} }
class _NullWidget extends Widget {
const _NullWidget();
@override
Element createElement() => throw UnimplementedError();
}
...@@ -112,6 +112,21 @@ class Theme extends StatelessWidget { ...@@ -112,6 +112,21 @@ class Theme extends StatelessWidget {
return ThemeData.localize(theme, theme.typography.geometryThemeFor(category)); return ThemeData.localize(theme, theme.typography.geometryThemeFor(category));
} }
// The inherited themes in widgets library can not infer their values from
// Theme in material library. Wraps the child with these inherited themes to
// overrides their values directly.
Widget _wrapsWidgetThemes(BuildContext context, Widget child) {
final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context);
return IconTheme(
data: data.iconTheme,
child: DefaultSelectionStyle(
selectionColor: data.textSelectionTheme.selectionColor ?? selectionStyle.selectionColor,
cursorColor: data.textSelectionTheme.cursorColor ?? selectionStyle.cursorColor,
child: child,
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return _InheritedTheme( return _InheritedTheme(
...@@ -123,10 +138,7 @@ class Theme extends StatelessWidget { ...@@ -123,10 +138,7 @@ class Theme extends StatelessWidget {
data: MaterialBasedCupertinoThemeData( data: MaterialBasedCupertinoThemeData(
materialTheme: data, materialTheme: data,
), ),
child: IconTheme( child: _wrapsWidgetThemes(context, child),
data: data.iconTheme,
child: child,
),
), ),
); );
} }
......
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'dart:ui';
import 'framework.dart';
import 'inherited_theme.dart';
/// The selection style to apply to descendant [EditableText] widgets which
/// don't have an explicit style.
///
/// {@macro flutter.cupertino.CupertinoApp.defaultSelectionStyle}
///
/// {@macro flutter.material.MaterialApp.defaultSelectionStyle}
///
/// See also:
/// * [TextSelectionTheme]: which also creates a [DefaultSelectionStyle] for
/// the subtree.
class DefaultSelectionStyle extends InheritedTheme {
/// Creates a default selection style widget that specifies the selection
/// properties for all widgets below it in the widget tree.
const DefaultSelectionStyle({
super.key,
this.cursorColor,
this.selectionColor,
required super.child,
});
/// A const-constructable default selection style that provides fallback
/// values.
///
/// Returned from [of] when the given [BuildContext] doesn't have an enclosing
/// default selection style.
///
/// This constructor creates a [DefaultTextStyle] with an invalid [child],
/// which means the constructed value cannot be incorporated into the tree.
const DefaultSelectionStyle.fallback({ super.key })
: cursorColor = null,
selectionColor = null,
super(child: const _NullWidget());
/// The color of the text field's cursor.
///
/// The cursor indicates the current location of the text insertion point in
/// the field.
final Color? cursorColor;
/// The background color of selected text.
final Color? selectionColor;
/// The closest instance of this class that encloses the given context.
///
/// If no such instance exists, returns an instance created by
/// [DefaultSelectionStyle.fallback], which contains fallback values.
///
/// Typical usage is as follows:
///
/// ```dart
/// DefaultSelectionStyle style = DefaultSelectionStyle.of(context);
/// ```
static DefaultSelectionStyle of(BuildContext context) {
return context.dependOnInheritedWidgetOfExactType<DefaultSelectionStyle>() ?? const DefaultSelectionStyle.fallback();
}
@override
Widget wrap(BuildContext context, Widget child) {
return DefaultSelectionStyle(
cursorColor: cursorColor,
selectionColor: selectionColor,
child: child
);
}
@override
bool updateShouldNotify(DefaultSelectionStyle oldWidget) {
return cursorColor != oldWidget.cursorColor ||
selectionColor != oldWidget.selectionColor;
}
}
class _NullWidget extends StatelessWidget {
const _NullWidget();
@override
Widget build(BuildContext context) {
throw FlutterError(
'A DefaultTextStyle constructed with DefaultTextStyle.fallback cannot be incorporated into the widget tree, '
'it is meant only to provide a fallback value returned by DefaultTextStyle.of() '
'when no enclosing default text style is present in a BuildContext.',
);
}
}
...@@ -20,6 +20,7 @@ import 'basic.dart'; ...@@ -20,6 +20,7 @@ import 'basic.dart';
import 'binding.dart'; import 'binding.dart';
import 'constants.dart'; import 'constants.dart';
import 'debug.dart'; import 'debug.dart';
import 'default_selection_style.dart';
import 'focus_manager.dart'; import 'focus_manager.dart';
import 'focus_scope.dart'; import 'focus_scope.dart';
import 'focus_traversal.dart'; import 'focus_traversal.dart';
...@@ -960,6 +961,9 @@ class EditableText extends StatefulWidget { ...@@ -960,6 +961,9 @@ class EditableText extends StatefulWidget {
/// The color to use when painting the selection. /// The color to use when painting the selection.
/// ///
/// If this property is null, this widget gets the selection color from the
/// [DefaultSelectionStyle].
///
/// For [CupertinoTextField]s, the value is set to the ambient /// For [CupertinoTextField]s, the value is set to the ambient
/// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the /// [CupertinoThemeData.primaryColor] with 20% opacity. For [TextField]s, the
/// value is set to the ambient [TextSelectionThemeData.selectionColor]. /// value is set to the ambient [TextSelectionThemeData.selectionColor].
...@@ -3249,6 +3253,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3249,6 +3253,8 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
assert(debugCheckHasMediaQuery(context)); assert(debugCheckHasMediaQuery(context));
super.build(context); // See AutomaticKeepAliveClientMixin. super.build(context); // See AutomaticKeepAliveClientMixin.
final Color? effectiveSelectionColor = widget.selectionColor ?? DefaultSelectionStyle.of(context).selectionColor;
final TextSelectionControls? controls = widget.selectionControls; final TextSelectionControls? controls = widget.selectionControls;
return MouseRegion( return MouseRegion(
cursor: widget.mouseCursor ?? SystemMouseCursors.text, cursor: widget.mouseCursor ?? SystemMouseCursors.text,
...@@ -3310,7 +3316,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien ...@@ -3310,7 +3316,7 @@ class EditableTextState extends State<EditableText> with AutomaticKeepAliveClien
minLines: widget.minLines, minLines: widget.minLines,
expands: widget.expands, expands: widget.expands,
strutStyle: widget.strutStyle, strutStyle: widget.strutStyle,
selectionColor: widget.selectionColor, selectionColor: effectiveSelectionColor,
textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context), textScaleFactor: widget.textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
textAlign: widget.textAlign, textAlign: widget.textAlign,
textDirection: _textDirection, textDirection: _textDirection,
......
...@@ -34,6 +34,7 @@ export 'src/widgets/bottom_navigation_bar_item.dart'; ...@@ -34,6 +34,7 @@ export 'src/widgets/bottom_navigation_bar_item.dart';
export 'src/widgets/color_filter.dart'; export 'src/widgets/color_filter.dart';
export 'src/widgets/container.dart'; export 'src/widgets/container.dart';
export 'src/widgets/debug.dart'; export 'src/widgets/debug.dart';
export 'src/widgets/default_selection_style.dart';
export 'src/widgets/default_text_editing_shortcuts.dart'; export 'src/widgets/default_text_editing_shortcuts.dart';
export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart'; export 'src/widgets/desktop_text_selection_toolbar_layout_delegate.dart';
export 'src/widgets/dismissible.dart'; export 'src/widgets/dismissible.dart';
......
...@@ -17,6 +17,7 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color; ...@@ -17,6 +17,7 @@ import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle, Color;
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind, kSecondaryMouseButton, kDoubleTapTimeout; import 'package:flutter/gestures.dart' show DragStartBehavior, PointerDeviceKind, kSecondaryMouseButton, kDoubleTapTimeout;
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
...@@ -406,6 +407,32 @@ void main() { ...@@ -406,6 +407,32 @@ void main() {
}, },
); );
testWidgets(
'uses DefaultSelectionStyle for selection and cursor colors if provided',
(WidgetTester tester) async {
const Color selectionColor = Colors.black;
const Color cursorColor = Colors.white;
await tester.pumpWidget(
const CupertinoApp(
home: Center(
child: DefaultSelectionStyle(
selectionColor: selectionColor,
cursorColor: cursorColor,
child: CupertinoTextField(
autofocus: true,
)
),
),
),
);
await tester.pump();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.widget.selectionColor, selectionColor);
expect(state.widget.cursorColor, cursorColor);
},
);
testWidgets( testWidgets(
'multi-lined text fields are intrinsically taller no-strut', 'multi-lined text fields are intrinsically taller no-strut',
(WidgetTester tester) async { (WidgetTester tester) async {
......
...@@ -163,6 +163,7 @@ void main() { ...@@ -163,6 +163,7 @@ void main() {
' Listener\n' ' Listener\n'
' HeroControllerScope\n' ' HeroControllerScope\n'
' Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#00000]\n' ' Navigator-[GlobalObjectKey<NavigatorState> _WidgetsAppState#00000]\n'
' DefaultSelectionStyle\n'
' IconTheme\n' ' IconTheme\n'
' IconTheme\n' ' IconTheme\n'
' _InheritedCupertinoTheme\n' ' _InheritedCupertinoTheme\n'
...@@ -170,6 +171,7 @@ void main() { ...@@ -170,6 +171,7 @@ void main() {
' _InheritedTheme\n' ' _InheritedTheme\n'
' Theme\n' ' Theme\n'
' AnimatedTheme\n' ' AnimatedTheme\n'
' DefaultSelectionStyle\n'
' _ScaffoldMessengerScope\n' ' _ScaffoldMessengerScope\n'
' ScaffoldMessenger\n' ' ScaffoldMessenger\n'
' Builder\n' ' Builder\n'
...@@ -302,6 +304,7 @@ void main() { ...@@ -302,6 +304,7 @@ void main() {
' Padding\n' ' Padding\n'
' SafeArea\n' ' SafeArea\n'
' FadeTransition\n' ' FadeTransition\n'
' DefaultSelectionStyle\n'
' IconTheme\n' ' IconTheme\n'
' IconTheme\n' ' IconTheme\n'
' _InheritedCupertinoTheme\n' ' _InheritedCupertinoTheme\n'
......
...@@ -431,6 +431,27 @@ void main() { ...@@ -431,6 +431,27 @@ void main() {
skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu. skip: isContextMenuProvidedByPlatform, // [intended] only applies to platforms where we supply the context menu.
); );
testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async {
const Color selectionColor = Colors.orange;
const Color cursorColor = Colors.red;
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: DefaultSelectionStyle(
selectionColor: selectionColor,
cursorColor: cursorColor,
child: TextField(autofocus: true),
),
),
),
);
await tester.pump();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.widget.selectionColor, selectionColor);
expect(state.widget.cursorColor, cursorColor);
});
testWidgets('Activates the text field when receives semantics focus on Mac, Windows', (WidgetTester tester) async { testWidgets('Activates the text field when receives semantics focus on Mac, Windows', (WidgetTester tester) async {
final SemanticsTester semantics = SemanticsTester(tester); final SemanticsTester semantics = SemanticsTester(tester);
final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!; final SemanticsOwner semanticsOwner = tester.binding.pipelineOwner.semanticsOwner!;
......
...@@ -262,4 +262,43 @@ void main() { ...@@ -262,4 +262,43 @@ void main() {
final RenderEditable renderSelectable = selectableTextState.renderEditable; final RenderEditable renderSelectable = selectableTextState.renderEditable;
expect(renderSelectable.cursorColor, cursorColor.withAlpha(0)); expect(renderSelectable.cursorColor, cursorColor.withAlpha(0));
}); });
testWidgets('TextSelectionThem overrides DefaultSelectionStyle', (WidgetTester tester) async {
const Color themeSelectionColor = Color(0xffaabbcc);
const Color themeCursorColor = Color(0x00ccbbaa);
const Color defaultSelectionColor = Color(0xffaa1111);
const Color defaultCursorColor = Color(0x00cc2222);
final Key defaultSelectionStyle = UniqueKey();
final Key themeStyle = UniqueKey();
// Test TextField's cursor color.
await tester.pumpWidget(
MaterialApp(
home: DefaultSelectionStyle(
selectionColor: defaultSelectionColor,
cursorColor: defaultCursorColor,
child: Container(
key: defaultSelectionStyle,
child: TextSelectionTheme(
data: const TextSelectionThemeData(
selectionColor: themeSelectionColor,
cursorColor: themeCursorColor,
),
child: Placeholder(
key: themeStyle,
),
),
)
),
),
);
final BuildContext defaultSelectionStyleContext = tester.element(find.byKey(defaultSelectionStyle));
DefaultSelectionStyle style = DefaultSelectionStyle.of(defaultSelectionStyleContext);
expect(style.selectionColor, defaultSelectionColor);
expect(style.cursorColor, defaultCursorColor);
final BuildContext themeStyleContext = tester.element(find.byKey(themeStyle));
style = DefaultSelectionStyle.of(themeStyleContext);
expect(style.selectionColor, themeSelectionColor);
expect(style.cursorColor, themeCursorColor);
});
} }
...@@ -47,6 +47,46 @@ void main() { ...@@ -47,6 +47,46 @@ void main() {
expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.dark)); expect(Theme.of(tester.element(find.text('menuItem'))).brightness, equals(Brightness.dark));
}); });
testWidgets('Theme overrides selection style', (WidgetTester tester) async {
const Color defaultSelectionColor = Color(0x11111111);
const Color defaultCursorColor = Color(0x22222222);
const Color themeSelectionColor = Color(0x33333333);
const Color themeCursorColor = Color(0x44444444);
await tester.pumpWidget(
MaterialApp(
theme: ThemeData(brightness: Brightness.dark),
home: Scaffold(
body: DefaultSelectionStyle(
selectionColor: defaultSelectionColor,
cursorColor: defaultCursorColor,
child: Theme(
data: ThemeData(
textSelectionTheme: const TextSelectionThemeData(
selectionColor: themeSelectionColor,
cursorColor: themeCursorColor,
),
),
child: const TextField(),
)
),
),
),
);
// Finds RenderEditable.
final RenderObject root = tester.renderObject(find.byType(EditableText));
late RenderEditable renderEditable;
void recursiveFinder(RenderObject child) {
if (child is RenderEditable) {
renderEditable = child;
return;
}
child.visitChildren(recursiveFinder);
}
root.visitChildren(recursiveFinder);
expect(renderEditable.selectionColor, themeSelectionColor);
expect(tester.widget<EditableText>(find.byType(EditableText)).cursorColor, themeCursorColor);
});
testWidgets('Fallback theme', (WidgetTester tester) async { testWidgets('Fallback theme', (WidgetTester tester) async {
late BuildContext capturedContext; late BuildContext capturedContext;
await tester.pumpWidget( await tester.pumpWidget(
......
...@@ -660,6 +660,36 @@ void main() { ...@@ -660,6 +660,36 @@ void main() {
expect(focusNode.hasFocus, isFalse); expect(focusNode.hasFocus, isFalse);
}); });
testWidgets('use DefaultSelectionStyle for selection color', (WidgetTester tester) async {
const TextEditingValue value = TextEditingValue(
text: 'test test',
selection: TextSelection(affinity: TextAffinity.upstream, baseOffset: 5, extentOffset: 7),
);
const Color selectionColor = Colors.orange;
controller.value = value;
await tester.pumpWidget(
DefaultSelectionStyle(
selectionColor: selectionColor,
child: MediaQuery(
data: const MediaQueryData(),
child: Directionality(
textDirection: TextDirection.ltr,
child: EditableText(
controller: controller,
backgroundCursorColor: Colors.grey,
focusNode: focusNode,
keyboardType: TextInputType.multiline,
style: textStyle,
cursorColor: cursorColor,
),
),
)
),
);
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.renderEditable.selectionColor, selectionColor);
});
testWidgets('visiblePassword keyboard is requested when set explicitly', (WidgetTester tester) async { testWidgets('visiblePassword keyboard is requested when set explicitly', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
MediaQuery( MediaQuery(
......
...@@ -297,6 +297,27 @@ void main() { ...@@ -297,6 +297,27 @@ void main() {
expect(tester.testTextInput.hasAnyClients, false); expect(tester.testTextInput.hasAnyClients, false);
}); });
testWidgets('uses DefaultSelectionStyle for selection and cursor colors if provided', (WidgetTester tester) async {
const Color selectionColor = Colors.orange;
const Color cursorColor = Colors.red;
await tester.pumpWidget(
const MaterialApp(
home: Material(
child: DefaultSelectionStyle(
selectionColor: selectionColor,
cursorColor: cursorColor,
child: SelectableText('text'),
),
),
),
);
await tester.pump();
final EditableTextState state = tester.state<EditableTextState>(find.byType(EditableText));
expect(state.widget.selectionColor, selectionColor);
expect(state.widget.cursorColor, cursorColor);
});
testWidgets('Selectable Text has adaptive size', (WidgetTester tester) async { testWidgets('Selectable Text has adaptive size', (WidgetTester tester) async {
await tester.pumpWidget( await tester.pumpWidget(
boilerplate( boilerplate(
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment