diff --git a/packages/flutter/lib/material.dart b/packages/flutter/lib/material.dart index 3501e02636..aa7c82b212 100644 --- a/packages/flutter/lib/material.dart +++ b/packages/flutter/lib/material.dart @@ -129,6 +129,7 @@ export 'src/material/text_button_theme.dart'; export 'src/material/text_field.dart'; export 'src/material/text_form_field.dart'; export 'src/material/text_selection.dart'; +export 'src/material/text_selection_theme.dart'; export 'src/material/text_theme.dart'; export 'src/material/theme.dart'; export 'src/material/theme_data.dart'; diff --git a/packages/flutter/lib/src/material/selectable_text.dart b/packages/flutter/lib/src/material/selectable_text.dart index dbebf14c9e..be77133753 100644 --- a/packages/flutter/lib/src/material/selectable_text.dart +++ b/packages/flutter/lib/src/material/selectable_text.dart @@ -13,6 +13,7 @@ import 'package:flutter/gestures.dart'; import 'feedback.dart'; import 'text_selection.dart'; +import 'text_selection_theme.dart'; import 'theme.dart'; /// An eyeballed value that moves the cursor slightly left of where it is @@ -584,7 +585,8 @@ class _SelectableTextState extends State with AutomaticKeepAlive 'inherit false style must supply fontSize and textBaseline', ); - final ThemeData themeData = Theme.of(context); + final ThemeData theme = Theme.of(context); + final TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); final FocusNode focusNode = _effectiveFocusNode; TextSelectionControls textSelectionControls; @@ -592,16 +594,23 @@ class _SelectableTextState extends State with AutomaticKeepAlive bool cursorOpacityAnimates; Offset cursorOffset; Color cursorColor = widget.cursorColor; + Color selectionColor; Radius cursorRadius = widget.cursorRadius; - switch (themeData.platform) { + switch (theme.platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: forcePressEnabled = true; textSelectionControls = cupertinoTextSelectionControls; paintCursorAboveText = true; cursorOpacityAnimates = true; - cursorColor ??= CupertinoTheme.of(context).primaryColor; + if (theme.useTextSelectionTheme) { + cursorColor ??= selectionTheme.cursorColor ?? CupertinoTheme.of(context).primaryColor; + selectionColor = selectionTheme.selectionColor ?? CupertinoTheme.of(context).primaryColor; + } else { + cursorColor ??= CupertinoTheme.of(context).primaryColor; + selectionColor = theme.textSelectionColor; + } cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); break; @@ -614,7 +623,13 @@ class _SelectableTextState extends State with AutomaticKeepAlive textSelectionControls = materialTextSelectionControls; paintCursorAboveText = false; cursorOpacityAnimates = false; - cursorColor ??= themeData.cursorColor; + if (theme.useTextSelectionTheme) { + cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; + selectionColor = selectionTheme.selectionColor ?? theme.colorScheme.primary; + } else { + cursorColor ??= theme.cursorColor; + selectionColor = theme.textSelectionColor; + } break; } @@ -644,7 +659,7 @@ class _SelectableTextState extends State with AutomaticKeepAlive toolbarOptions: widget.toolbarOptions, minLines: widget.minLines, maxLines: widget.maxLines ?? defaultTextStyle.maxLines, - selectionColor: themeData.textSelectionColor, + selectionColor: selectionColor, selectionControls: widget.selectionEnabled ? textSelectionControls : null, onSelectionChanged: _handleSelectionChanged, onSelectionHandleTapped: _handleSelectionHandleTapped, diff --git a/packages/flutter/lib/src/material/text_field.dart b/packages/flutter/lib/src/material/text_field.dart index 42ff4eba7d..2080d2cfbd 100644 --- a/packages/flutter/lib/src/material/text_field.dart +++ b/packages/flutter/lib/src/material/text_field.dart @@ -22,6 +22,7 @@ import 'material_localizations.dart'; import 'material_state.dart'; import 'selectable_text.dart' show iOSHorizontalOffset; import 'text_selection.dart'; +import 'text_selection_theme.dart'; import 'theme.dart'; export 'package:flutter/services.dart' show TextInputType, TextInputAction, TextCapitalization, SmartQuotesType, SmartDashesType; @@ -637,10 +638,22 @@ class TextField extends StatefulWidget { /// {@macro flutter.widgets.editableText.cursorRadius} final Radius cursorRadius; - /// The color to use when painting the cursor. + /// The color of the cursor. /// - /// Defaults to [ThemeData.cursorColor] or [CupertinoThemeData.primaryColor] - /// depending on [ThemeData.platform]. + /// The cursor indicates the current location of text insertion point in + /// the field. + /// + /// If this is null it will default to a value based on the following: + /// + /// * If the ambient [ThemeData.useTextSelectionTheme] is true then it + /// will use the value of the ambient [TextSelectionThemeData.cursorColor]. + /// If that is null then if the [ThemeData.platform] is [TargetPlatform.iOS] + /// or [TargetPlatform.macOS] then it will use [CupertinoThemeData.primaryColor]. + /// Otherwise it will use the value of [ColorScheme.primary] of [ThemeData.colorScheme]. + /// + /// * If the ambient [ThemeData.useTextSelectionTheme] is false then it + /// will use either [ThemeData.cursorColor] or [CupertinoThemeData.primaryColor] + /// depending on [ThemeData.platform]. final Color cursorColor; /// Controls how tall the selection highlight boxes are computed to be. @@ -1017,6 +1030,11 @@ class _TextFieldState extends State implements TextSelectionGestureDe } } + Color _defaultSelectionColor(BuildContext context, Color primary) { + final bool isDark = Theme.of(context).brightness == Brightness.dark; + return primary.withOpacity(isDark ? 0.40 : 0.12); + } + @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); @@ -1028,9 +1046,10 @@ class _TextFieldState extends State implements TextSelectionGestureDe 'inherit false style must supply fontSize and textBaseline', ); - final ThemeData themeData = Theme.of(context); - final TextStyle style = themeData.textTheme.subtitle1.merge(widget.style); - final Brightness keyboardAppearance = widget.keyboardAppearance ?? themeData.primaryColorBrightness; + final ThemeData theme = Theme.of(context); + final TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context); + final TextStyle style = theme.textTheme.subtitle1.merge(widget.style); + final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.primaryColorBrightness; final TextEditingController controller = _effectiveController; final FocusNode focusNode = _effectiveFocusNode; final List formatters = widget.inputFormatters ?? []; @@ -1042,20 +1061,27 @@ class _TextFieldState extends State implements TextSelectionGestureDe bool cursorOpacityAnimates; Offset cursorOffset; Color cursorColor = widget.cursorColor; + Color selectionColor; Color autocorrectionTextRectColor; Radius cursorRadius = widget.cursorRadius; - switch (themeData.platform) { + switch (theme.platform) { case TargetPlatform.iOS: case TargetPlatform.macOS: forcePressEnabled = true; textSelectionControls = cupertinoTextSelectionControls; paintCursorAboveText = true; cursorOpacityAnimates = true; - cursorColor ??= CupertinoTheme.of(context).primaryColor; + if (theme.useTextSelectionTheme) { + cursorColor ??= selectionTheme.cursorColor ?? CupertinoTheme.of(context).primaryColor; + selectionColor = selectionTheme.selectionColor ?? _defaultSelectionColor(context, CupertinoTheme.of(context).primaryColor); + } else { + cursorColor ??= CupertinoTheme.of(context).primaryColor; + selectionColor = theme.textSelectionColor; + } cursorRadius ??= const Radius.circular(2.0); cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); - autocorrectionTextRectColor = themeData.textSelectionColor; + autocorrectionTextRectColor = selectionColor; break; case TargetPlatform.android: @@ -1066,7 +1092,13 @@ class _TextFieldState extends State implements TextSelectionGestureDe textSelectionControls = materialTextSelectionControls; paintCursorAboveText = false; cursorOpacityAnimates = false; - cursorColor ??= themeData.cursorColor; + if (theme.useTextSelectionTheme) { + cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary; + selectionColor = selectionTheme.selectionColor ?? _defaultSelectionColor(context, theme.colorScheme.primary); + } else { + cursorColor ??= theme.cursorColor; + selectionColor = theme.textSelectionColor; + } break; } @@ -1096,7 +1128,7 @@ class _TextFieldState extends State implements TextSelectionGestureDe maxLines: widget.maxLines, minLines: widget.minLines, expands: widget.expands, - selectionColor: themeData.textSelectionColor, + selectionColor: selectionColor, selectionControls: widget.selectionEnabled ? textSelectionControls : null, onChanged: widget.onChanged, onSelectionChanged: _handleSelectionChanged, diff --git a/packages/flutter/lib/src/material/text_selection.dart b/packages/flutter/lib/src/material/text_selection.dart index 7664037b48..1e5219bed7 100644 --- a/packages/flutter/lib/src/material/text_selection.dart +++ b/packages/flutter/lib/src/material/text_selection.dart @@ -20,6 +20,7 @@ import 'icon_button.dart'; import 'icons.dart'; import 'material.dart'; import 'material_localizations.dart'; +import 'text_selection_theme.dart'; import 'theme.dart'; const double _kHandleSize = 22.0; @@ -795,12 +796,16 @@ class _MaterialTextSelectionControls extends TextSelectionControls { /// Builder for material-style text selection handles. @override Widget buildHandle(BuildContext context, TextSelectionHandleType type, double textHeight) { + final ThemeData theme = Theme.of(context); + final Color handleColor = theme.useTextSelectionTheme ? + TextSelectionTheme.of(context).selectionHandleColor ?? theme.colorScheme.primary : + theme.textSelectionHandleColor; final Widget handle = SizedBox( width: _kHandleSize, height: _kHandleSize, child: CustomPaint( painter: _TextSelectionHandlePainter( - color: Theme.of(context).textSelectionHandleColor, + color: handleColor, ), ), ); diff --git a/packages/flutter/lib/src/material/text_selection_theme.dart b/packages/flutter/lib/src/material/text_selection_theme.dart new file mode 100644 index 0000000000..99929c0255 --- /dev/null +++ b/packages/flutter/lib/src/material/text_selection_theme.dart @@ -0,0 +1,168 @@ +// 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. + +// @dart = 2.8 + +import 'package:flutter/foundation.dart'; +import 'package:flutter/widgets.dart'; + +import 'theme.dart'; + +/// Defines the visual properties needed for text selection in [TextField] and +/// [SelectableText] widgets. +/// +/// Used by [TextSelectionTheme] to control the visual properties of text +/// selection in a widget subtree. +/// +/// Use [TextSelectionTheme.of] to access the closest ancestor +/// [TextSelectionTheme] of the current [BuildContext]. +/// +/// See also: +/// +/// * [TextSelectionTheme], an [InheritedWidget] that propagates the theme down its +/// subtree. +/// * [InputDecorationTheme], which defines most other visual properties of +/// text fields. +@immutable +class TextSelectionThemeData with Diagnosticable { + /// Creates the set of properties used to configure [TextField]s. + const TextSelectionThemeData({ + this.cursorColor, + this.selectionColor, + this.selectionHandleColor, + }); + + /// The color of the cursor in the text field. + /// + /// The cursor indicates the current location of text insertion point in + /// the field. + final Color cursorColor; + + /// The background color of selected text. + final Color selectionColor; + + /// The color of the selection handles on the text field. + /// + /// Selection handles are used to indicate the bounds of the selected text, + /// or as a handle to drag the cursor to a new location in the text. + final Color selectionHandleColor; + + /// Creates a copy of this object with the given fields replaced with the + /// specified values. + TextSelectionThemeData copyWith({ + Color cursorColor, + Color selectionColor, + Color selectionHandleColor, + }) { + return TextSelectionThemeData( + cursorColor: cursorColor ?? this.cursorColor, + selectionColor: selectionColor ?? this.selectionColor, + selectionHandleColor: selectionHandleColor ?? this.selectionHandleColor, + ); + } + + /// Linearly interpolate between two text field themes. + /// + /// If both arguments are null, then null is returned. + /// + /// {@macro dart.ui.shadow.lerp} + static TextSelectionThemeData lerp(TextSelectionThemeData a, TextSelectionThemeData b, double t) { + if (a == null && b == null) + return null; + assert(t != null); + return TextSelectionThemeData( + cursorColor: Color.lerp(a?.cursorColor, b?.cursorColor, t), + selectionColor: Color.lerp(a?.selectionColor, b?.selectionColor, t), + selectionHandleColor: Color.lerp(a?.selectionHandleColor, b?.selectionHandleColor, t), + ); + } + + @override + int get hashCode { + return hashValues( + cursorColor, + selectionColor, + selectionHandleColor, + ); + } + + @override + bool operator==(Object other) { + if (identical(this, other)) + return true; + if (other.runtimeType != runtimeType) + return false; + return other is TextSelectionThemeData + && other.cursorColor == cursorColor + && other.selectionColor == selectionColor + && other.selectionHandleColor == selectionHandleColor; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add(ColorProperty('selectionColor', selectionColor, defaultValue: null)); + properties.add(ColorProperty('selectionHandleColor', selectionHandleColor, defaultValue: null)); + } +} + +/// An inherited widget that defines the appearance of text selection in +/// this widget's subtree. +/// +/// Values specified here are used for [TextField] and [SelectableText] +/// properties that are not given an explicit non-null value. +/// +/// {@tool snippet} +/// +/// Here is an example of a text selection theme that applies a blue cursor +/// color with light blue selection handles to the child text field. +/// +/// ```dart +/// TextSelectionTheme( +/// data: TextSelectionThemeData( +/// cursorColor: Colors.blue, +/// selectionHandleColor: Colors.lightBlue, +/// ), +/// child: TextField(), +/// ), +/// ``` +/// {@end-tool} +class TextSelectionTheme extends InheritedTheme { + /// Creates a text selection theme widget that specifies the text + /// selection properties for all widgets below it in the widget tree. + /// + /// The data argument must not be null. + const TextSelectionTheme({ + Key key, + @required this.data, + Widget child, + }) : assert(data != null), super(key: key, child: child); + + /// The properties for descendant [TextField] and [SelectableText] widgets. + final TextSelectionThemeData data; + + /// Returns the [data] from the closest [TextSelectionTheme] ancestor. If + /// there is no ancestor, it returns [ThemeData.textSelectionTheme]. + /// Applications can assume that the returned value will not be null. + /// + /// Typical usage is as follows: + /// + /// ```dart + /// TextSelectionThemeData theme = TextSelectionTheme.of(context); + /// ``` + static TextSelectionThemeData of(BuildContext context) { + final TextSelectionTheme selectionTheme = context.dependOnInheritedWidgetOfExactType(); + return selectionTheme?.data ?? Theme.of(context).textSelectionTheme; + } + + @override + Widget wrap(BuildContext context, Widget child) { + final TextSelectionTheme ancestorTheme = context.findAncestorWidgetOfExactType(); + return identical(this, ancestorTheme) ? child : TextSelectionTheme(data: data, child: child); + } + + @override + bool updateShouldNotify(TextSelectionTheme oldWidget) => data != oldWidget.data; +} diff --git a/packages/flutter/lib/src/material/theme_data.dart b/packages/flutter/lib/src/material/theme_data.dart index b59d2a3917..a0a78fbf9f 100644 --- a/packages/flutter/lib/src/material/theme_data.dart +++ b/packages/flutter/lib/src/material/theme_data.dart @@ -37,6 +37,7 @@ import 'slider_theme.dart'; import 'snack_bar_theme.dart'; import 'tab_bar_theme.dart'; import 'text_button_theme.dart'; +import 'text_selection_theme.dart'; import 'text_theme.dart'; import 'time_picker_theme.dart'; import 'toggle_buttons_theme.dart'; @@ -281,7 +282,9 @@ class ThemeData with Diagnosticable { TextButtonThemeData textButtonTheme, ElevatedButtonThemeData elevatedButtonTheme, OutlinedButtonThemeData outlinedButtonTheme, + TextSelectionThemeData textSelectionTheme, bool fixTextFieldOutlineLabel, + bool useTextSelectionTheme, }) { assert(colorScheme?.brightness == null || brightness == null || colorScheme.brightness == brightness); final Brightness _brightness = brightness ?? colorScheme?.brightness ?? Brightness.light; @@ -322,7 +325,6 @@ class ThemeData with Diagnosticable { // Spec doesn't specify a dark theme secondaryHeaderColor, this is a guess. secondaryHeaderColor ??= isDark ? Colors.grey[700] : primarySwatch[50]; textSelectionColor ??= isDark ? accentColor : primarySwatch[200]; - // TODO(hansmuller): We need a TextFieldTheme to handle this instead, https://github.com/flutter/flutter/issues/56082 cursorColor = cursorColor ?? const Color.fromRGBO(66, 133, 244, 1.0); textSelectionHandleColor ??= isDark ? Colors.tealAccent[400] : primarySwatch[300]; backgroundColor ??= isDark ? Colors.grey[700] : primarySwatch[200]; @@ -397,7 +399,10 @@ class ThemeData with Diagnosticable { textButtonTheme ??= const TextButtonThemeData(); elevatedButtonTheme ??= const ElevatedButtonThemeData(); outlinedButtonTheme ??= const OutlinedButtonThemeData(); + textSelectionTheme ??= const TextSelectionThemeData(); + fixTextFieldOutlineLabel ??= false; + useTextSelectionTheme ??= false; return ThemeData.raw( visualDensity: visualDensity, @@ -469,7 +474,9 @@ class ThemeData with Diagnosticable { textButtonTheme: textButtonTheme, elevatedButtonTheme: elevatedButtonTheme, outlinedButtonTheme: outlinedButtonTheme, + textSelectionTheme: textSelectionTheme, fixTextFieldOutlineLabel: fixTextFieldOutlineLabel, + useTextSelectionTheme: useTextSelectionTheme, ); } @@ -553,7 +560,9 @@ class ThemeData with Diagnosticable { @required this.textButtonTheme, @required this.elevatedButtonTheme, @required this.outlinedButtonTheme, + @required this.textSelectionTheme, @required this.fixTextFieldOutlineLabel, + @required this.useTextSelectionTheme, }) : assert(visualDensity != null), assert(primaryColor != null), assert(primaryColorBrightness != null), @@ -620,7 +629,10 @@ class ThemeData with Diagnosticable { assert(textButtonTheme != null), assert(elevatedButtonTheme != null), assert(outlinedButtonTheme != null), - assert(fixTextFieldOutlineLabel != null); + assert(fixTextFieldOutlineLabel != null), + assert(textSelectionTheme != null), + assert(fixTextFieldOutlineLabel != null), + assert(useTextSelectionTheme != null); /// Create a [ThemeData] based on the colors in the given [colorScheme] and /// text styles of the optional [textTheme]. @@ -1092,6 +1104,9 @@ class ThemeData with Diagnosticable { /// [OutlinedButton]s. final OutlinedButtonThemeData outlinedButtonTheme; + /// A theme for customizing the appearance and layout of [TextField] widgets. + final TextSelectionThemeData textSelectionTheme; + /// A temporary flag to allow apps to opt-in to a /// [small fix](https://github.com/flutter/flutter/issues/54028) for the Y /// coordinate of the floating label in a [TextField] [OutlineInputBorder]. @@ -1104,6 +1119,18 @@ class ThemeData with Diagnosticable { /// stable release (1.19). final bool fixTextFieldOutlineLabel; + /// A temporary flag to allow apps to opt-in to the new [TextSelectionTheme], with + /// its new defaults for the [cursorColor] and [textSelectionHandleColor]. + /// + /// Setting this flag to true will cause the [textSelectionTheme] to be used + /// instead of the [cursorColor] and [textSelectionHandleColor] by [TextField] + /// and [SelectableText] widgets. In addition, the default values of these + /// colors have changed to [ColorScheme.primary]. + /// + /// The flag is currently false by default. It will be removed after migration + /// to the [TextSelectionTheme] has been completed. + final bool useTextSelectionTheme; + /// Creates a copy of this theme but with the given fields replaced with the new values. /// /// The [brightness] value is applied to the [colorScheme]. @@ -1178,7 +1205,9 @@ class ThemeData with Diagnosticable { TextButtonThemeData textButtonTheme, ElevatedButtonThemeData elevatedButtonTheme, OutlinedButtonThemeData outlinedButtonTheme, + TextSelectionThemeData textSelectionTheme, bool fixTextFieldOutlineLabel, + bool useTextSelectionTheme, }) { cupertinoOverrideTheme = cupertinoOverrideTheme?.noDefault(); return ThemeData.raw( @@ -1251,7 +1280,9 @@ class ThemeData with Diagnosticable { textButtonTheme: textButtonTheme ?? this.textButtonTheme, elevatedButtonTheme: elevatedButtonTheme ?? this.elevatedButtonTheme, outlinedButtonTheme: outlinedButtonTheme ?? this.outlinedButtonTheme, + textSelectionTheme: textSelectionTheme ?? this.textSelectionTheme, fixTextFieldOutlineLabel: fixTextFieldOutlineLabel ?? this.fixTextFieldOutlineLabel, + useTextSelectionTheme: useTextSelectionTheme ?? this.useTextSelectionTheme, ); } @@ -1402,7 +1433,9 @@ class ThemeData with Diagnosticable { textButtonTheme: TextButtonThemeData.lerp(a.textButtonTheme, b.textButtonTheme, t), elevatedButtonTheme: ElevatedButtonThemeData.lerp(a.elevatedButtonTheme, b.elevatedButtonTheme, t), outlinedButtonTheme: OutlinedButtonThemeData.lerp(a.outlinedButtonTheme, b.outlinedButtonTheme, t), + textSelectionTheme: TextSelectionThemeData .lerp(a.textSelectionTheme, b.textSelectionTheme, t), fixTextFieldOutlineLabel: t < 0.5 ? a.fixTextFieldOutlineLabel : b.fixTextFieldOutlineLabel, + useTextSelectionTheme: t < 0.5 ? a.useTextSelectionTheme : b.useTextSelectionTheme, ); } @@ -1481,7 +1514,9 @@ class ThemeData with Diagnosticable { && other.textButtonTheme == textButtonTheme && other.elevatedButtonTheme == elevatedButtonTheme && other.outlinedButtonTheme == outlinedButtonTheme - && other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel; + && other.textSelectionTheme == textSelectionTheme + && other.fixTextFieldOutlineLabel == fixTextFieldOutlineLabel + && other.useTextSelectionTheme == useTextSelectionTheme; } @override @@ -1559,7 +1594,9 @@ class ThemeData with Diagnosticable { textButtonTheme, elevatedButtonTheme, outlinedButtonTheme, + textSelectionTheme, fixTextFieldOutlineLabel, + useTextSelectionTheme, ]; return hashList(values); } @@ -1630,6 +1667,8 @@ class ThemeData with Diagnosticable { properties.add(DiagnosticsProperty('dividerTheme', dividerTheme, defaultValue: defaultData.dividerTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('buttonBarTheme', buttonBarTheme, defaultValue: defaultData.buttonBarTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('timePickerTheme', timePickerTheme, defaultValue: defaultData.timePickerTheme, level: DiagnosticLevel.debug)); + properties.add(DiagnosticsProperty('textSelectionTheme', textSelectionTheme, defaultValue: defaultData.textSelectionTheme, level: DiagnosticLevel.debug)); + properties.add(DiagnosticsProperty('textSelectionTheme', textSelectionTheme, defaultValue: defaultData.textSelectionTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('bottomNavigationBarTheme', bottomNavigationBarTheme, defaultValue: defaultData.bottomNavigationBarTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('textButtonTheme', textButtonTheme, defaultValue: defaultData.textButtonTheme, level: DiagnosticLevel.debug)); properties.add(DiagnosticsProperty('elevatedButtonTheme', elevatedButtonTheme, defaultValue: defaultData.elevatedButtonTheme, level: DiagnosticLevel.debug)); diff --git a/packages/flutter/test/material/text_selection_theme_test.dart b/packages/flutter/test/material/text_selection_theme_test.dart new file mode 100644 index 0000000000..ba3def44ea --- /dev/null +++ b/packages/flutter/test/material/text_selection_theme_test.dart @@ -0,0 +1,286 @@ +// 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. + +// @dart = 2.8 + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; +import 'package:flutter/src/material/selectable_text.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import '../rendering/mock_canvas.dart'; + +void main() { + test('TextSelectionThemeData copyWith, ==, hashCode basics', () { + expect(const TextSelectionThemeData(), const TextSelectionThemeData().copyWith()); + expect(const TextSelectionThemeData().hashCode, const TextSelectionThemeData().copyWith().hashCode); + }); + + test('TextSelectionThemeData null fields by default', () { + const TextSelectionThemeData theme = TextSelectionThemeData(); + expect(theme.cursorColor, null); + expect(theme.selectionColor, null); + expect(theme.selectionHandleColor, null); + }); + + testWidgets('Default TextSelectionThemeData debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const TextSelectionThemeData().debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, []); + }); + + testWidgets('TextSelectionThemeData implements debugFillProperties', (WidgetTester tester) async { + final DiagnosticPropertiesBuilder builder = DiagnosticPropertiesBuilder(); + const TextSelectionThemeData( + cursorColor: Color(0xffeeffaa), + selectionColor: Color(0x88888888), + selectionHandleColor: Color(0xaabbccdd), + ).debugFillProperties(builder); + + final List description = builder.properties + .where((DiagnosticsNode node) => !node.isFiltered(DiagnosticLevel.info)) + .map((DiagnosticsNode node) => node.toString()) + .toList(); + + expect(description, [ + 'cursorColor: Color(0xffeeffaa)', + 'selectionColor: Color(0x88888888)', + 'selectionHandleColor: Color(0xaabbccdd)', + ]); + }); + + testWidgets('Empty textSelectionTheme will use defaults', (WidgetTester tester) async { + // Test TextField's cursor & selection color. + await tester.pumpWidget( + const MaterialApp( + home: Material( + child: TextField(), + ), + ), + ); + await tester.pumpAndSettle(); + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final RenderEditable renderEditable = editableTextState.renderEditable; + expect(renderEditable.cursorColor, const Color(0x004285f4)); + expect(renderEditable.selectionColor, const Color(0xFF90CAF9)); + + // Test the selection handle color. + await tester.pumpWidget( + MaterialApp( + home: Material( + child: Builder( + builder: (BuildContext context) { + return materialTextSelectionControls.buildHandle( + context, TextSelectionHandleType.left, 10.0 + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + final RenderBox handle = tester.firstRenderObject(find.byType(CustomPaint)); + expect(handle, paints..path(color: Colors.blue[300])); + + }); + + testWidgets('Empty textSelectionTheme with useTextSelectionTheme set will use new defaults', (WidgetTester tester) async { + final ThemeData theme = ThemeData.fallback().copyWith(useTextSelectionTheme: true); + final Color primaryColor = Color(theme.colorScheme.primary.value); + + // Test TextField's cursor & selection color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Material( + child: TextField(), + ), + ), + ); + await tester.pumpAndSettle(); + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final RenderEditable renderEditable = editableTextState.renderEditable; + expect(renderEditable.cursorColor, primaryColor.withAlpha(0)); + expect(Color(renderEditable.selectionColor.value), primaryColor.withOpacity(0.12)); + + // Test the selection handle color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Builder( + builder: (BuildContext context) { + return materialTextSelectionControls.buildHandle( + context, TextSelectionHandleType.left, 10.0 + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + final RenderBox handle = tester.firstRenderObject(find.byType(CustomPaint)); + expect(handle, paints..path(color: primaryColor)); + }); + + testWidgets('ThemeDate.textSelectionTheme will be used if provided', (WidgetTester tester) async { + const TextSelectionThemeData textSelectionTheme = TextSelectionThemeData( + cursorColor: Color(0xffaabbcc), + selectionColor: Color(0x88888888), + selectionHandleColor: Color(0x00ccbbaa), + ); + final ThemeData theme = ThemeData.fallback().copyWith( + useTextSelectionTheme: true, + textSelectionTheme: textSelectionTheme, + ); + + // Test TextField's cursor & selection color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Material( + child: TextField(), + ), + ), + ); + await tester.pumpAndSettle(); + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final RenderEditable renderEditable = editableTextState.renderEditable; + expect(renderEditable.cursorColor, textSelectionTheme.cursorColor.withAlpha(0)); + expect(renderEditable.selectionColor, textSelectionTheme.selectionColor); + + // Test the selection handle color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: Builder( + builder: (BuildContext context) { + return materialTextSelectionControls.buildHandle( + context, TextSelectionHandleType.left, 10.0 + ); + }, + ), + ), + ), + ); + await tester.pumpAndSettle(); + final RenderBox handle = tester.firstRenderObject(find.byType(CustomPaint)); + expect(handle, paints..path(color: textSelectionTheme.selectionHandleColor)); + }); + + testWidgets('TextSelectionTheme widget will override ThemeDate.textSelectionTheme', (WidgetTester tester) async { + const TextSelectionThemeData defaultTextSelectionTheme = TextSelectionThemeData( + cursorColor: Color(0xffaabbcc), + selectionColor: Color(0x88888888), + selectionHandleColor: Color(0x00ccbbaa), + ); + final ThemeData theme = ThemeData.fallback().copyWith( + useTextSelectionTheme: true, + textSelectionTheme: defaultTextSelectionTheme, + ); + const TextSelectionThemeData widgetTextSelectionTheme = TextSelectionThemeData( + cursorColor: Color(0xffddeeff), + selectionColor: Color(0x44444444), + selectionHandleColor: Color(0x00ffeedd), + ); + + // Test TextField's cursor & selection color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Material( + child: TextSelectionTheme( + data: widgetTextSelectionTheme, + child: TextField(), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final RenderEditable renderEditable = editableTextState.renderEditable; + expect(renderEditable.cursorColor, widgetTextSelectionTheme.cursorColor.withAlpha(0)); + expect(renderEditable.selectionColor, widgetTextSelectionTheme.selectionColor); + + // Test the selection handle color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: Material( + child: TextSelectionTheme( + data: widgetTextSelectionTheme, + child: Builder( + builder: (BuildContext context) { + return materialTextSelectionControls.buildHandle( + context, TextSelectionHandleType.left, 10.0 + ); + }, + ), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final RenderBox handle = tester.firstRenderObject(find.byType(CustomPaint)); + expect(handle, paints..path(color: widgetTextSelectionTheme.selectionHandleColor)); + }); + + testWidgets('TextField parameters will override theme settings', (WidgetTester tester) async { + const TextSelectionThemeData defaultTextSelectionTheme = TextSelectionThemeData( + cursorColor: Color(0xffaabbcc), + selectionHandleColor: Color(0x00ccbbaa), + ); + final ThemeData theme = ThemeData.fallback().copyWith( + useTextSelectionTheme: true, + textSelectionTheme: defaultTextSelectionTheme, + ); + const TextSelectionThemeData widgetTextSelectionTheme = TextSelectionThemeData( + cursorColor: Color(0xffddeeff), + selectionHandleColor: Color(0x00ffeedd), + ); + const Color cursorColor = Color(0x88888888); + + // Test TextField's cursor color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Material( + child: TextSelectionTheme( + data: widgetTextSelectionTheme, + child: TextField(cursorColor: cursorColor), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final EditableTextState editableTextState = tester.firstState(find.byType(EditableText)); + final RenderEditable renderEditable = editableTextState.renderEditable; + expect(renderEditable.cursorColor, cursorColor.withAlpha(0)); + + // Test SelectableText's cursor color. + await tester.pumpWidget( + MaterialApp( + theme: theme, + home: const Material( + child: TextSelectionTheme( + data: widgetTextSelectionTheme, + child: SelectableText('foobar', cursorColor: cursorColor), + ), + ), + ), + ); + await tester.pumpAndSettle(); + final EditableTextState selectableTextState = tester.firstState(find.byType(EditableText)); + final RenderEditable renderSelectable = selectableTextState.renderEditable; + expect(renderSelectable.cursorColor, cursorColor.withAlpha(0)); + }); +} diff --git a/packages/flutter/test/material/theme_data_test.dart b/packages/flutter/test/material/theme_data_test.dart index f1d37023cd..3ebf2dc641 100644 --- a/packages/flutter/test/material/theme_data_test.dart +++ b/packages/flutter/test/material/theme_data_test.dart @@ -287,7 +287,9 @@ void main() { textButtonTheme: TextButtonThemeData(style: TextButton.styleFrom(primary: Colors.red)), elevatedButtonTheme: ElevatedButtonThemeData(style: ElevatedButton.styleFrom(primary: Colors.green)), outlinedButtonTheme: OutlinedButtonThemeData(style: OutlinedButton.styleFrom(primary: Colors.blue)), + textSelectionTheme: const TextSelectionThemeData(cursorColor: Colors.black), fixTextFieldOutlineLabel: false, + useTextSelectionTheme: false, ); final SliderThemeData otherSliderTheme = SliderThemeData.fromPrimaryColors( @@ -373,7 +375,9 @@ void main() { textButtonTheme: const TextButtonThemeData(), elevatedButtonTheme: const ElevatedButtonThemeData(), outlinedButtonTheme: const OutlinedButtonThemeData(), + textSelectionTheme: const TextSelectionThemeData(cursorColor: Colors.white), fixTextFieldOutlineLabel: true, + useTextSelectionTheme: true, ); final ThemeData themeDataCopy = theme.copyWith( @@ -445,7 +449,9 @@ void main() { textButtonTheme: otherTheme.textButtonTheme, elevatedButtonTheme: otherTheme.elevatedButtonTheme, outlinedButtonTheme: otherTheme.outlinedButtonTheme, + textSelectionTheme: otherTheme.textSelectionTheme, fixTextFieldOutlineLabel: otherTheme.fixTextFieldOutlineLabel, + useTextSelectionTheme: otherTheme.useTextSelectionTheme, ); expect(themeDataCopy.brightness, equals(otherTheme.brightness)); @@ -516,7 +522,9 @@ void main() { expect(themeDataCopy.textButtonTheme, equals(otherTheme.textButtonTheme)); expect(themeDataCopy.elevatedButtonTheme, equals(otherTheme.elevatedButtonTheme)); expect(themeDataCopy.outlinedButtonTheme, equals(otherTheme.outlinedButtonTheme)); + expect(themeDataCopy.textSelectionTheme, equals(otherTheme.textSelectionTheme)); expect(themeDataCopy.fixTextFieldOutlineLabel, equals(otherTheme.fixTextFieldOutlineLabel)); + expect(themeDataCopy.useTextSelectionTheme, equals(otherTheme.useTextSelectionTheme)); }); testWidgets('ThemeData.toString has less than 200 characters output', (WidgetTester tester) async {