From abcb01c04d019a357566f35dc333abcc85bfad3a Mon Sep 17 00:00:00 2001 From: Harry Sild <46851868+Kypsis@users.noreply.github.com> Date: Mon, 15 May 2023 15:21:49 +0300 Subject: [PATCH] fix: [MDS-517] Fix TextInput and TextArea bugs (#164) --- .../src/storybook/common/widgets/error.dart | 22 + .../lib/src/storybook/stories/text_area.dart | 24 +- .../lib/src/storybook/stories/text_input.dart | 146 +- example/lib/src/storybook/storybook.dart | 1 + lib/moon_design.dart | 2 + .../theme/text_area/text_area_properties.dart | 22 +- .../text_input/text_input_properties.dart | 41 +- .../text_input_size_properties.dart | 28 +- lib/src/widgets/common/icons/moon_icon.dart | 8 +- lib/src/widgets/text_area/text_area.dart | 276 +- .../widgets/text_input/form_text_input.dart | 401 ++ .../widgets/text_input/input_decorator.dart | 3992 +++++++++++++++++ lib/src/widgets/text_input/text_input.dart | 1611 +++++-- 13 files changed, 5981 insertions(+), 593 deletions(-) create mode 100644 example/lib/src/storybook/common/widgets/error.dart create mode 100644 lib/src/widgets/text_input/form_text_input.dart create mode 100644 lib/src/widgets/text_input/input_decorator.dart diff --git a/example/lib/src/storybook/common/widgets/error.dart b/example/lib/src/storybook/common/widgets/error.dart new file mode 100644 index 00000000..b38f802c --- /dev/null +++ b/example/lib/src/storybook/common/widgets/error.dart @@ -0,0 +1,22 @@ +import 'package:flutter/material.dart'; +import 'package:moon_design/moon_design.dart'; + +class StoryErrorWidget extends StatelessWidget { + final String errorText; + + const StoryErrorWidget({ + super.key, + required this.errorText, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const MoonIcon(MoonIcons.info_16, size: 16), + const SizedBox(width: 4), + Text(errorText), + ], + ); + } +} diff --git a/example/lib/src/storybook/stories/text_area.dart b/example/lib/src/storybook/stories/text_area.dart index 70e59f2c..43250beb 100644 --- a/example/lib/src/storybook/stories/text_area.dart +++ b/example/lib/src/storybook/stories/text_area.dart @@ -1,4 +1,5 @@ import 'package:example/src/storybook/common/color_options.dart'; +import 'package:example/src/storybook/common/widgets/error.dart'; import 'package:flutter/material.dart'; import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; @@ -82,6 +83,22 @@ class TextAreaStory extends Story { initial: true, ); + final growableKnob = context.knobs.boolean( + label: "Growable", + description: "Whether the MoonTextArea has no fixed height and is growable", + ); + + final hasFocusEffectKnob = context.knobs.boolean( + label: "hasFocusEffect", + description: "Whether to display focus effect around MoonTextInput.", + initial: true, + ); + + final showHelperKnob = context.knobs.boolean( + label: "helper", + description: "Show widget in MoonTextArea helper slot.", + ); + return Center( child: Column( mainAxisAlignment: MainAxisAlignment.center, @@ -91,10 +108,12 @@ class TextAreaStory extends Story { child: Builder( builder: (context) { return Column( + mainAxisSize: MainAxisSize.min, children: [ MoonTextArea( enabled: enabledKnob, - height: 300, + hasFocusEffect: hasFocusEffectKnob, + height: growableKnob ? null : 200, textColor: textColor, hintTextColor: hintTextColor, backgroundColor: backgroundColor, @@ -107,7 +126,8 @@ class TextAreaStory extends Story { validator: (value) => value?.length != null && value!.length < 10 ? "The text should be longer than 10 characters." : null, - errorBuilder: (context, errorText) => Text(errorText!), + helper: showHelperKnob ? const Text("Supporting text") : null, + errorBuilder: (context, errorText) => StoryErrorWidget(errorText: errorText!), ), const SizedBox(height: 16), MoonFilledButton( diff --git a/example/lib/src/storybook/stories/text_input.dart b/example/lib/src/storybook/stories/text_input.dart index ae72b2b2..b13ffb9b 100644 --- a/example/lib/src/storybook/stories/text_input.dart +++ b/example/lib/src/storybook/stories/text_input.dart @@ -1,9 +1,13 @@ import 'package:example/src/storybook/common/color_options.dart'; +import 'package:example/src/storybook/common/widgets/error.dart'; import 'package:flutter/material.dart'; import 'package:moon_design/moon_design.dart'; import 'package:storybook_flutter/storybook_flutter.dart'; -TextEditingController _textEditingController = TextEditingController(); +TextEditingController _textController = TextEditingController(); +TextEditingController _passwordController = TextEditingController(); + +bool _hidePassword = true; class TextInputStory extends Story { TextInputStory() @@ -97,6 +101,17 @@ class TextInputStory extends Story { initial: true, ); + final hasFocusEffectKnob = context.knobs.boolean( + label: "hasFocusEffect", + description: "Whether to display focus effect around MoonTextInput.", + initial: true, + ); + + final hasFloatingLabelKnob = context.knobs.boolean( + label: "hasFloatingLabel", + description: "Whether MoonTextInput has floating label.", + ); + final showLeadingKnob = context.knobs.boolean( label: "leading", description: "Show widget in MoonTextInput leading slot.", @@ -109,9 +124,9 @@ class TextInputStory extends Story { initial: true, ); - final showSupportingKnob = context.knobs.boolean( - label: "supporting", - description: "Show widget in MoonTextInput supporting slot.", + final showHelperKnob = context.knobs.boolean( + label: "helper", + description: "Show widget in MoonTextInput helper slot.", ); return Center( @@ -124,41 +139,98 @@ class TextInputStory extends Story { builder: (context) { return Column( children: [ - SizedBox( - height: 86, - child: MoonTextInput( - controller: _textEditingController, - textInputSize: textInputSizesKnob, - enabled: enabledKnob, - textColor: textColor, - hintTextColor: hintTextColor, - backgroundColor: backgroundColor, - activeBorderColor: activeBorderColor, - inactiveBorderColor: inactiveBorderColor, - errorBorderColor: errorBorderColor, - borderRadius: borderRadiusKnob != null - ? BorderRadius.circular(borderRadiusKnob.toDouble()) - : null, - hintText: "Enter your text here...", - validator: (value) => value?.length != null && value!.length < 10 - ? "The text should be longer than 10 characters." - : null, - leading: showLeadingKnob ? const MoonIcon(MoonIcons.search_24) : null, - trailing: showTrailingKnob - ? MoonButton.icon( - icon: MoonIcon( - MoonIcons.close_24, - color: DefaultTextStyle.of(context).style.color, + MoonFormTextInput( + controller: _textController, + enabled: enabledKnob, + textInputSize: textInputSizesKnob, + hasFocusEffect: hasFocusEffectKnob, + hasFloatingLabel: hasFloatingLabelKnob, + textColor: textColor, + hintTextColor: hintTextColor, + backgroundColor: backgroundColor, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + errorBorderColor: errorBorderColor, + borderRadius: + borderRadiusKnob != null ? BorderRadius.circular(borderRadiusKnob.toDouble()) : null, + hintText: "Enter your text here (over 10 characters)", + validator: (value) => value?.length != null && value!.length < 10 + ? "The text should be longer than 10 characters." + : null, + leading: showLeadingKnob + ? const MoonIcon( + MoonIcons.search_24, + size: 24, + ) + : null, + trailing: showTrailingKnob + ? MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + child: const MoonIcon( + MoonIcons.close_small_24, + size: 24, ), - buttonSize: MoonButtonSize.xs, - onTap: () => _textEditingController.clear(), - ) - : null, - supporting: showSupportingKnob ? const Text("Supporting text") : null, - errorBuilder: (context, errorText) => Text(errorText!), - ), + onTap: () => _textController.clear(), + ), + ) + : null, + helper: showHelperKnob ? const Text("Supporting text") : null, + errorBuilder: (context, errorText) => StoryErrorWidget(errorText: errorText!), + ), + const SizedBox(height: 16), + StatefulBuilder( + builder: (context, setState) { + return MoonFormTextInput( + controller: _passwordController, + enabled: enabledKnob, + keyboardType: TextInputType.visiblePassword, + obscureText: _hidePassword, + textInputSize: textInputSizesKnob, + hasFocusEffect: hasFocusEffectKnob, + hasFloatingLabel: hasFloatingLabelKnob, + textColor: textColor, + hintTextColor: hintTextColor, + backgroundColor: backgroundColor, + activeBorderColor: activeBorderColor, + inactiveBorderColor: inactiveBorderColor, + errorBorderColor: errorBorderColor, + borderRadius: borderRadiusKnob != null + ? BorderRadius.circular(borderRadiusKnob.toDouble()) + : null, + hintText: "Enter password (123abc)", + validator: (value) => value != "123abc" ? "Wrong password." : null, + leading: showLeadingKnob + ? const MoonIcon( + MoonIcons.search_24, + size: 24, + ) + : null, + trailing: showTrailingKnob + ? MouseRegion( + cursor: SystemMouseCursors.click, + child: GestureDetector( + child: IntrinsicWidth( + child: Align( + alignment: Alignment.centerRight, + child: Text( + _hidePassword ? "Show" : "Hide", + style: DefaultTextStyle.of(context) + .style + .copyWith(decoration: TextDecoration.underline), + ), + ), + ), + onTap: () => setState(() => _hidePassword = !_hidePassword), + ), + ) + : null, + helper: showHelperKnob ? const Text("Supporting text") : null, + errorBuilder: (context, errorText) => StoryErrorWidget(errorText: errorText!), + ); + }, ), - const SizedBox(height: 8), + const SizedBox(height: 24), MoonFilledButton( label: const Text("Submit"), onTap: () => Form.of(context).validate(), diff --git a/example/lib/src/storybook/storybook.dart b/example/lib/src/storybook/storybook.dart index dc31c227..39dcbe55 100644 --- a/example/lib/src/storybook/storybook.dart +++ b/example/lib/src/storybook/storybook.dart @@ -66,6 +66,7 @@ class StorybookPage extends StatelessWidget { child: Scaffold( extendBody: true, extendBodyBehindAppBar: true, + resizeToAvoidBottomInset: false, body: Padding( padding: const EdgeInsets.symmetric(horizontal: 16.0), child: child, diff --git a/lib/moon_design.dart b/lib/moon_design.dart index 340d8282..59defd96 100644 --- a/lib/moon_design.dart +++ b/lib/moon_design.dart @@ -1,3 +1,4 @@ +/// Moon Design for Flutter library moon_design; export 'package:moon_design/src/theme/accordion/accordion_theme.dart'; @@ -65,6 +66,7 @@ export 'package:moon_design/src/widgets/radio/radio.dart'; export 'package:moon_design/src/widgets/switch/switch.dart'; export 'package:moon_design/src/widgets/tag/tag.dart'; export 'package:moon_design/src/widgets/text_area/text_area.dart'; +export 'package:moon_design/src/widgets/text_input/form_text_input.dart'; export 'package:moon_design/src/widgets/text_input/text_input.dart'; export 'package:moon_design/src/widgets/toast/toast.dart'; export 'package:moon_design/src/widgets/tooltip/tooltip.dart'; diff --git a/lib/src/theme/text_area/text_area_properties.dart b/lib/src/theme/text_area/text_area_properties.dart index a706c734..82a8febd 100644 --- a/lib/src/theme/text_area/text_area_properties.dart +++ b/lib/src/theme/text_area/text_area_properties.dart @@ -11,10 +11,10 @@ class MoonTextAreaProperties extends ThemeExtension with borderRadius: MoonBorders.borders.interactiveSm, transitionDuration: const Duration(milliseconds: 200), transitionCurve: Curves.easeInOutCubic, - supportingPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s, vertical: MoonSizes.sizes.x4s), - textPadding: const EdgeInsets.all(16), + supportingPadding: EdgeInsets.only(top: MoonSizes.sizes.x4s), + textPadding: EdgeInsets.all(MoonSizes.sizes.x2s), textStyle: MoonTextStyles.body.text16, - supportingTextStyle: MoonTextStyles.body.text12, + helperTextStyle: MoonTextStyles.body.text12, ); /// TextArea border radius. @@ -26,7 +26,7 @@ class MoonTextAreaProperties extends ThemeExtension with /// TextArea transition curve. final Curve transitionCurve; - /// The padding around TextArea supporting widget or error builder. + /// The padding around TextArea helper widget or error builder. final EdgeInsetsGeometry supportingPadding; /// TextArea text padding. @@ -35,8 +35,8 @@ class MoonTextAreaProperties extends ThemeExtension with /// TextArea text style. final TextStyle textStyle; - /// TextArea supporting or error text style. - final TextStyle supportingTextStyle; + /// TextArea helper or error text style. + final TextStyle helperTextStyle; const MoonTextAreaProperties({ required this.borderRadius, @@ -45,7 +45,7 @@ class MoonTextAreaProperties extends ThemeExtension with required this.supportingPadding, required this.textPadding, required this.textStyle, - required this.supportingTextStyle, + required this.helperTextStyle, }); @override @@ -56,7 +56,7 @@ class MoonTextAreaProperties extends ThemeExtension with EdgeInsetsGeometry? supportingPadding, EdgeInsetsGeometry? textPadding, TextStyle? textStyle, - TextStyle? supportingTextStyle, + TextStyle? helperTextStyle, }) { return MoonTextAreaProperties( borderRadius: borderRadius ?? this.borderRadius, @@ -65,7 +65,7 @@ class MoonTextAreaProperties extends ThemeExtension with supportingPadding: supportingPadding ?? this.supportingPadding, textPadding: textPadding ?? this.textPadding, textStyle: textStyle ?? this.textStyle, - supportingTextStyle: supportingTextStyle ?? this.supportingTextStyle, + helperTextStyle: helperTextStyle ?? this.helperTextStyle, ); } @@ -80,7 +80,7 @@ class MoonTextAreaProperties extends ThemeExtension with supportingPadding: EdgeInsetsGeometry.lerp(supportingPadding, other.supportingPadding, t)!, textPadding: EdgeInsetsGeometry.lerp(textPadding, other.textPadding, t)!, textStyle: TextStyle.lerp(textStyle, other.textStyle, t)!, - supportingTextStyle: TextStyle.lerp(supportingTextStyle, other.supportingTextStyle, t)!, + helperTextStyle: TextStyle.lerp(helperTextStyle, other.helperTextStyle, t)!, ); } @@ -95,6 +95,6 @@ class MoonTextAreaProperties extends ThemeExtension with ..add(DiagnosticsProperty("supportingPadding", supportingPadding)) ..add(DiagnosticsProperty("textPadding", textPadding)) ..add(DiagnosticsProperty("textStyle", textStyle)) - ..add(DiagnosticsProperty("supportingTextStyle", supportingTextStyle)); + ..add(DiagnosticsProperty("helperTextStyle", helperTextStyle)); } } diff --git a/lib/src/theme/text_input/text_input_properties.dart b/lib/src/theme/text_input/text_input_properties.dart index a0a19ad0..ba923d14 100644 --- a/lib/src/theme/text_input/text_input_properties.dart +++ b/lib/src/theme/text_input/text_input_properties.dart @@ -9,9 +9,8 @@ class MoonTextInputProperties extends ThemeExtension wi static final properties = MoonTextInputProperties( transitionDuration: const Duration(milliseconds: 200), transitionCurve: Curves.easeInOutCubic, - supportingPadding: EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s, vertical: MoonSizes.sizes.x4s), - labelTextStyle: MoonTextStyles.body.text12, - supportingTextStyle: MoonTextStyles.body.text12, + helperPadding: EdgeInsets.only(top: MoonSizes.sizes.x4s), + helperTextStyle: MoonTextStyles.body.text12, ); /// TextInput transition duration. @@ -20,37 +19,31 @@ class MoonTextInputProperties extends ThemeExtension wi /// TextInput transition curve. final Curve transitionCurve; - /// The padding around TextInput supporting widget or error builder. - final EdgeInsetsGeometry supportingPadding; + /// The padding around TextInput helper widget or error builder. + final EdgeInsetsGeometry helperPadding; - /// TextInput label text style. - final TextStyle labelTextStyle; - - /// TextInput supporting or error text style. - final TextStyle supportingTextStyle; + /// TextInput helper or error text style. + final TextStyle helperTextStyle; const MoonTextInputProperties({ required this.transitionDuration, required this.transitionCurve, - required this.supportingPadding, - required this.labelTextStyle, - required this.supportingTextStyle, + required this.helperPadding, + required this.helperTextStyle, }); @override MoonTextInputProperties copyWith({ Duration? transitionDuration, Curve? transitionCurve, - EdgeInsetsGeometry? supportingPadding, - TextStyle? labelTextStyle, - TextStyle? supportingTextStyle, + EdgeInsetsGeometry? helperPadding, + TextStyle? helperTextStyle, }) { return MoonTextInputProperties( transitionDuration: transitionDuration ?? this.transitionDuration, transitionCurve: transitionCurve ?? this.transitionCurve, - supportingPadding: supportingPadding ?? this.supportingPadding, - labelTextStyle: labelTextStyle ?? this.labelTextStyle, - supportingTextStyle: supportingTextStyle ?? this.supportingTextStyle, + helperPadding: helperPadding ?? this.helperPadding, + helperTextStyle: helperTextStyle ?? this.helperTextStyle, ); } @@ -61,9 +54,8 @@ class MoonTextInputProperties extends ThemeExtension wi return MoonTextInputProperties( transitionDuration: lerpDuration(transitionDuration, other.transitionDuration, t), transitionCurve: other.transitionCurve, - supportingPadding: EdgeInsetsGeometry.lerp(supportingPadding, other.supportingPadding, t)!, - labelTextStyle: TextStyle.lerp(labelTextStyle, other.labelTextStyle, t)!, - supportingTextStyle: TextStyle.lerp(supportingTextStyle, other.supportingTextStyle, t)!, + helperPadding: EdgeInsetsGeometry.lerp(helperPadding, other.helperPadding, t)!, + helperTextStyle: TextStyle.lerp(helperTextStyle, other.helperTextStyle, t)!, ); } @@ -74,8 +66,7 @@ class MoonTextInputProperties extends ThemeExtension wi ..add(DiagnosticsProperty("type", "MoonTextInputProperties")) ..add(DiagnosticsProperty("transitionDuration", transitionDuration)) ..add(DiagnosticsProperty("transitionCurve", transitionCurve)) - ..add(DiagnosticsProperty("supportingPadding", supportingPadding)) - ..add(DiagnosticsProperty("labelTextStyle", labelTextStyle)) - ..add(DiagnosticsProperty("supportingTextStyle", supportingTextStyle)); + ..add(DiagnosticsProperty("helperPadding", helperPadding)) + ..add(DiagnosticsProperty("helperTextStyle", helperTextStyle)); } } diff --git a/lib/src/theme/text_input/text_input_size_properties.dart b/lib/src/theme/text_input/text_input_size_properties.dart index 09bc697f..35c4bbea 100644 --- a/lib/src/theme/text_input/text_input_size_properties.dart +++ b/lib/src/theme/text_input/text_input_size_properties.dart @@ -14,8 +14,11 @@ class MoonTextInputSizeProperties extends ThemeExtension? validator; @@ -213,8 +211,8 @@ class MoonTextArea extends StatefulWidget { /// Builder for the error widget. final MoonTextAreaErrorBuilder? errorBuilder; - /// The widget in the supporting slot of the text area. - final Widget? supporting; + /// The widget in the helper slot of the text area. + final Widget? helper; /// MDS TextArea widget const MoonTextArea({ @@ -226,6 +224,7 @@ class MoonTextArea extends StatefulWidget { this.enableIMEPersonalizedLearning = true, this.enableInteractiveSelection, this.enableSuggestions = true, + this.hasFocusEffect = true, this.readOnly = false, this.scribbleEnabled = true, this.showCursor, @@ -243,7 +242,7 @@ class MoonTextArea extends StatefulWidget { this.transitionDuration, this.transitionCurve, this.scrollPadding = const EdgeInsets.all(24.0), - this.supportingPadding, + this.helperPadding, this.textPadding, this.focusNode, this.validator, @@ -265,7 +264,7 @@ class MoonTextArea extends StatefulWidget { this.controller, this.textInputAction, this.textStyle, - this.supportingTextStyle, + this.helperTextStyle, this.onTap, this.onTapOutside, this.onChanged, @@ -273,7 +272,7 @@ class MoonTextArea extends StatefulWidget { this.onSaved, this.onSubmitted, this.errorBuilder, - this.supporting, + this.helper, }); @override @@ -281,10 +280,6 @@ class MoonTextArea extends StatefulWidget { } class _MoonTextAreaState extends State { - bool _isFocused = false; - bool _isHovered = false; - String? _errorText; - Color _getTextColor(BuildContext context, {required Color effectiveBackgroundColor}) { final backgroundLuminance = effectiveBackgroundColor.computeLuminance(); if (backgroundLuminance > 0.5) { @@ -294,21 +289,6 @@ class _MoonTextAreaState extends State { } } - void _setFocusStatus(bool isFocused) { - setState(() => _isFocused = isFocused); - } - - void _setHoverStatus(bool isHovered) { - setState(() => _isHovered = isHovered); - } - - String? _validateInput(String? value) { - final validationResult = widget.validator?.call(value); - _errorText = validationResult; - - return validationResult; - } - @override Widget build(BuildContext context) { final BorderRadiusGeometry effectiveBorderRadius = @@ -332,32 +312,14 @@ class _MoonTextAreaState extends State { final Color effectiveHoverBorderColor = widget.hoverBorderColor ?? context.moonTheme?.textInputTheme.colors.hoverBorderColor ?? MoonColors.light.beerus; - final double effectiveFocusEffectExtent = - context.moonEffects?.controlFocusEffect.effectExtent ?? MoonFocusEffects.lightFocusEffect.effectExtent; - - final Color focusEffectColor = - context.isDarkMode ? effectiveActiveBorderColor.withOpacity(0.4) : effectiveActiveBorderColor.withOpacity(0.2); - - final Color errorFocusEffectColor = - context.isDarkMode ? effectiveErrorBorderColor.withOpacity(0.4) : effectiveErrorBorderColor.withOpacity(0.2); - final Color effectiveTextColor = widget.textColor ?? _getTextColor(context, effectiveBackgroundColor: effectiveBackgroundColor); final Color effectiveHintTextColor = widget.hintTextColor ?? context.moonTheme?.textAreaTheme.colors.hintTextColor ?? MoonColors.light.trunks; - final double effectiveDisabledOpacityValue = context.moonTheme?.opacity.disabled ?? MoonOpacity.opacities.disabled; - - final Duration effectiveTransitionDuration = widget.transitionDuration ?? - context.moonTheme?.textAreaTheme.properties.transitionDuration ?? - const Duration(milliseconds: 200); - - final Curve effectiveTransitionCurve = - widget.transitionCurve ?? context.moonTheme?.textAreaTheme.properties.transitionCurve ?? Curves.easeInOutCubic; - - final EdgeInsetsGeometry effectiveSupportingPadding = widget.supportingPadding ?? - context.moonTheme?.textInputTheme.properties.supportingPadding ?? + final EdgeInsetsGeometry effectiveHelperPadding = widget.helperPadding ?? + context.moonTheme?.textInputTheme.properties.helperPadding ?? EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s, vertical: MoonSizes.sizes.x4s); final EdgeInsetsGeometry effectiveTextPadding = @@ -366,150 +328,76 @@ class _MoonTextAreaState extends State { final TextStyle effectiveTextStyle = widget.textStyle ?? context.moonTheme?.textAreaTheme.properties.textStyle ?? const TextStyle(fontSize: 16); - final TextStyle effectiveSupportingTextStyle = widget.supportingTextStyle ?? - context.moonTheme?.textAreaTheme.properties.supportingTextStyle ?? + final TextStyle effectiveHelperTextStyle = widget.helperTextStyle ?? + context.moonTheme?.textAreaTheme.properties.helperTextStyle ?? const TextStyle(fontSize: 12); - final OutlineInputBorder defaultBorder = OutlineInputBorder( - borderRadius: effectiveBorderRadius.smoothBorderRadius(context), - borderSide: BorderSide( - color: _isHovered ? effectiveHoverBorderColor : effectiveInactiveBorderColor, - width: _isHovered ? MoonBorders.borders.activeBorderWidth : MoonBorders.borders.defaultBorderWidth, - ), - ); - - final OutlineInputBorder focusBorder = OutlineInputBorder( - borderRadius: effectiveBorderRadius.smoothBorderRadius(context), - borderSide: BorderSide( - color: effectiveActiveBorderColor, - width: MoonBorders.borders.activeBorderWidth, - ), - ); + final Duration effectiveTransitionDuration = widget.transitionDuration ?? + context.moonTheme?.textAreaTheme.properties.transitionDuration ?? + const Duration(milliseconds: 200); - final OutlineInputBorder errorBorder = OutlineInputBorder( - borderRadius: effectiveBorderRadius.smoothBorderRadius(context), - borderSide: BorderSide( - color: effectiveErrorBorderColor, - width: MoonBorders.borders.activeBorderWidth, - ), - ); + final Curve effectiveTransitionCurve = + widget.transitionCurve ?? context.moonTheme?.textAreaTheme.properties.transitionCurve ?? Curves.easeInOutCubic; - return Semantics( - label: widget.semanticLabel, - child: RepaintBoundary( - child: AnimatedOpacity( - opacity: widget.enabled ? 1.0 : effectiveDisabledOpacityValue, - curve: effectiveTransitionCurve, - duration: effectiveTransitionDuration, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MoonFocusEffect( - show: _isFocused, - effectExtent: effectiveFocusEffectExtent, - effectColor: _errorText == null ? focusEffectColor : errorFocusEffectColor, - childBorderRadius: effectiveBorderRadius, - effectDuration: effectiveTransitionDuration, - effectCurve: effectiveTransitionCurve, - child: MouseRegion( - onEnter: (_) => _setHoverStatus(true), - onExit: (_) => _setHoverStatus(false), - child: Focus( - canRequestFocus: false, - onFocusChange: _setFocusStatus, - child: TextFormField( - autocorrect: widget.autocorrect, - autofillHints: widget.autofillHints, - autofocus: widget.autofocus, - autovalidateMode: widget.autovalidateMode, - controller: widget.controller, - cursorColor: effectiveTextColor, - enabled: widget.enabled, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - enableInteractiveSelection: widget.enableInteractiveSelection, - enableSuggestions: widget.enableSuggestions, - expands: true, - focusNode: widget.focusNode, - initialValue: widget.initialValue, - inputFormatters: widget.inputFormatters, - keyboardAppearance: widget.keyboardAppearance, - keyboardType: TextInputType.multiline, - maxLength: widget.maxLength, - maxLengthEnforcement: widget.maxLengthEnforcement, - maxLines: null, - minLines: widget.minLines, - onChanged: widget.onChanged, - onEditingComplete: widget.onEditingComplete, - onFieldSubmitted: widget.onSubmitted, - onSaved: widget.onSaved, - onTap: widget.onTap, - onTapOutside: widget.onTapOutside, - readOnly: widget.readOnly, - restorationId: widget.restorationId, - scrollController: widget.scrollController, - scrollPadding: widget.scrollPadding, - scrollPhysics: widget.scrollPhysics, - showCursor: widget.showCursor, - strutStyle: widget.strutStyle, - style: effectiveTextStyle.copyWith(color: effectiveTextColor), - textAlign: widget.textAlign, - textAlignVertical: TextAlignVertical.top, - textCapitalization: widget.textCapitalization, - textDirection: widget.textDirection, - textInputAction: widget.textInputAction, - validator: _validateInput, - decoration: InputDecoration( - filled: true, - fillColor: effectiveBackgroundColor, - focusColor: effectiveActiveBorderColor, - hoverColor: Colors.transparent, - contentPadding: effectiveTextPadding, - border: defaultBorder, - enabledBorder: defaultBorder, - disabledBorder: defaultBorder, - focusedBorder: focusBorder, - errorBorder: errorBorder, - focusedErrorBorder: errorBorder, - hintText: widget.hintText, - hintStyle: effectiveTextStyle.copyWith(color: effectiveHintTextColor), - errorStyle: const TextStyle(height: 0.1, fontSize: 0), - constraints: BoxConstraints( - minHeight: widget.height ?? 24, - maxHeight: widget.height ?? 120, - ), - ), - ), - ), - ), - ), - if (widget.supporting != null || (_errorText != null && widget.errorBuilder != null)) - RepaintBoundary( - child: AnimatedIconTheme( - color: _errorText != null && widget.errorBuilder != null - ? effectiveErrorBorderColor - : effectiveHintTextColor, - duration: effectiveTransitionDuration, - child: AnimatedDefaultTextStyle( - style: effectiveSupportingTextStyle.copyWith( - color: _errorText != null && widget.errorBuilder != null - ? effectiveErrorBorderColor - : effectiveHintTextColor, - ), - duration: effectiveTransitionDuration, - child: Padding( - padding: effectiveSupportingPadding, - child: _errorText != null && widget.errorBuilder != null - ? widget.errorBuilder!(context, _errorText) - : widget.supporting, - ), - ), - ), - ), - ], - ), - ), - ), + return MoonFormTextInput( + activeBorderColor: effectiveActiveBorderColor, + autocorrect: widget.autocorrect, + autofillHints: widget.autofillHints, + autofocus: widget.autofocus, + autovalidateMode: widget.autovalidateMode, + backgroundColor: effectiveBackgroundColor, + borderRadius: effectiveBorderRadius, + controller: widget.controller, + cursorColor: effectiveTextColor, + enabled: widget.enabled, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + enableInteractiveSelection: widget.enableInteractiveSelection, + enableSuggestions: widget.enableSuggestions, + errorBorderColor: effectiveErrorBorderColor, + errorBuilder: widget.errorBuilder, + expands: widget.height != null, + focusNode: widget.focusNode, + hasFocusEffect: widget.hasFocusEffect, + height: widget.height, + helper: widget.helper, + helperPadding: effectiveHelperPadding, + helperTextStyle: effectiveHelperTextStyle, + hintText: widget.hintText, + hintTextColor: effectiveHintTextColor, + hoverBorderColor: effectiveHoverBorderColor, + inactiveBorderColor: effectiveInactiveBorderColor, + initialValue: widget.initialValue, + inputFormatters: widget.inputFormatters, + keyboardAppearance: widget.keyboardAppearance, + keyboardType: TextInputType.multiline, + maxLength: widget.maxLength, + maxLengthEnforcement: widget.maxLengthEnforcement, + maxLines: null, + minLines: widget.minLines, + onChanged: widget.onChanged, + onEditingComplete: widget.onEditingComplete, + onFieldSubmitted: widget.onSubmitted, + onSaved: widget.onSaved, + onTap: widget.onTap, + onTapOutside: widget.onTapOutside, + padding: effectiveTextPadding, + readOnly: widget.readOnly, + restorationId: widget.restorationId, + scrollController: widget.scrollController, + scrollPadding: widget.scrollPadding, + scrollPhysics: widget.scrollPhysics, + showCursor: widget.showCursor, + strutStyle: widget.strutStyle, + style: effectiveTextStyle.copyWith(color: effectiveTextColor), + textAlign: widget.textAlign, + textAlignVertical: TextAlignVertical.top, + textCapitalization: widget.textCapitalization, + textColor: effectiveTextColor, + textDirection: widget.textDirection, + textInputAction: widget.textInputAction, + transitionCurve: effectiveTransitionCurve, + transitionDuration: effectiveTransitionDuration, + validator: widget.validator, ); } } diff --git a/lib/src/widgets/text_input/form_text_input.dart b/lib/src/widgets/text_input/form_text_input.dart new file mode 100644 index 00000000..b96e7a0f --- /dev/null +++ b/lib/src/widgets/text_input/form_text_input.dart @@ -0,0 +1,401 @@ +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; + +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' + hide FloatingLabelAlignment, FloatingLabelBehavior, InputDecoration, InputDecorator; +import 'package:flutter/services.dart'; + +import 'package:moon_design/src/widgets/text_input/input_decorator.dart'; +import 'package:moon_design/src/widgets/text_input/text_input.dart'; + +export 'package:flutter/services.dart' show SmartDashesType, SmartQuotesType; + +/// A [FormField] that contains a [MoonTextInput]. +/// +/// This is a convenience widget that wraps a [MoonTextInput] widget in a +/// [FormField]. +/// +/// A [Form] ancestor is not required. The [Form] simply makes it easier to +/// save, reset, or validate multiple fields at once. To use without a [Form], +/// pass a `GlobalKey` (see [GlobalKey]) to the constructor and use +/// [GlobalKey.currentState] to save or reset the form field. +/// +/// When a [controller] is specified, its [TextEditingController.text] +/// defines the [initialValue]. If this [FormField] is part of a scrolling +/// container that lazily constructs its children, like a [ListView] or a +/// [CustomScrollView], then a [controller] should be specified. +/// The controller's lifetime should be managed by a stateful widget ancestor +/// of the scrolling container. +/// +/// If a [controller] is not specified, [initialValue] can be used to give +/// the automatically generated controller an initial value. +/// +/// {@macro flutter.material.textfield.wantKeepAlive} +/// +/// Remember to call [TextEditingController.dispose] of the [TextEditingController] +/// when it is no longer needed. This will ensure any resources used by the object +/// are discarded. +/// +/// For a documentation about the various parameters, see [MoonTextInput]. +/// +/// {@tool snippet} +/// +/// Creates a [MoonFormTextInput] with an [InputDecoration] and validator function. +/// +/// ![If the user enters valid text, the MoonTextInput appears normally without any warnings to the user](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field.png) +/// +/// ![If the user enters invalid text, the error message returned from the validator function is displayed in dark red underneath the input](https://flutter.github.io/assets-for-api-docs/assets/material/text_form_field_error.png) +/// +/// ```dart +/// MoonFormTextInput( +/// leading: Icon(Icons.person), +/// hintText: 'What do people call you?', +/// labelText: 'Name *', +/// onSaved: (String? value) { +/// // This optional block of code can be used to run +/// // code when the user saves the form. +/// }, +/// validator: (String? value) { +/// return (value != null && value.contains('@')) ? 'Do not use the @ char.' : null; +/// }, +/// ) +/// ``` +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows how to move the focus to the next field when the user +/// presses the SPACE key. +/// +/// ** See code in examples/api/lib/material/text_form_field/text_form_field.1.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * +/// * [MoonTextInput], which is the underlying text field without the [Form] +/// integration. +class MoonFormTextInput extends FormField { + /// Creates a [FormField] that contains a [MoonTextInput]. + /// + /// When a [controller] is specified, [initialValue] must be null (the + /// default). If [controller] is null, then a [TextEditingController] + /// will be constructed automatically and its `text` will be initialized + /// to [initialValue] or the empty string. + /// + /// For documentation about the various parameters, see the [MoonTextInput] class. + MoonFormTextInput({ + super.key, + super.onSaved, + super.restorationId, + super.validator, + this.controller, + AppPrivateCommandCallback? onAppPrivateCommand, + AutovalidateMode? autovalidateMode, + bool autocorrect = true, + bool autofocus = false, + bool enableIMEPersonalizedLearning = true, + bool enableSuggestions = true, + bool expands = false, + bool hasFloatingLabel = false, + bool hasFocusEffect = true, + bool isDense = true, + bool obscureText = false, + bool readOnly = false, + bool scribbleEnabled = true, + bool? enabled, + bool? enableInteractiveSelection, + bool? showCursor, + BorderRadiusGeometry? borderRadius, + Brightness? keyboardAppearance, + Clip clipBehavior = Clip.hardEdge, + Color? activeBorderColor, + Color? backgroundColor, + Color? cursorColor, + Color? errorBorderColor, + Color? hintTextColor, + Color? hoverBorderColor, + Color? inactiveBorderColor, + Color? textColor, + Curve? transitionCurve, + double cursorWidth = 2.0, + double? cursorHeight, + double? gap, + double? height, + DragStartBehavior dragStartBehavior = DragStartBehavior.start, + Duration? transitionDuration, + EdgeInsets scrollPadding = const EdgeInsets.all(20.0), + EdgeInsetsGeometry? padding, + EdgeInsetsGeometry? helperPadding, + EditableTextContextMenuBuilder? contextMenuBuilder = _defaultContextMenuBuilder, + FocusNode? focusNode, + GestureTapCallback? onTap, + int? maxLength, + int? maxLines = 1, + int? minLines, + Iterable? autofillHints, + List? inputFormatters, + MaxLengthEnforcement? maxLengthEnforcement, + MoonTextInputErrorBuilder? errorBuilder, + MoonTextInputSize? textInputSize, + MouseCursor? mouseCursor, + Radius? cursorRadius, + ScrollController? scrollController, + ScrollPhysics? scrollPhysics, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + SpellCheckConfiguration? spellCheckConfiguration, + String obscuringCharacter = '•', + String? hintText, + String? initialValue, + StrutStyle? strutStyle, + TapRegionCallback? onTapOutside, + TextAlign textAlign = TextAlign.start, + TextAlignVertical textAlignVertical = TextAlignVertical.center, + TextCapitalization textCapitalization = TextCapitalization.none, + TextDirection? textDirection, + TextInputAction? textInputAction, + TextInputType? keyboardType, + TextMagnifierConfiguration? magnifierConfiguration, + TextSelectionControls? selectionControls, + TextStyle? style, + TextStyle? helperTextStyle, + ui.BoxHeightStyle selectionHeightStyle = ui.BoxHeightStyle.tight, + ui.BoxWidthStyle selectionWidthStyle = ui.BoxWidthStyle.tight, + ValueChanged? onChanged, + ValueChanged? onFieldSubmitted, + VoidCallback? onEditingComplete, + Widget? leading, + Widget? trailing, + Widget? helper, + }) : assert(initialValue == null || controller == null), + assert(obscuringCharacter.length == 1), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + !expands || (maxLines == null && minLines == null), + 'minLines and maxLines must be null when expands is true.', + ), + assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), + assert(maxLength == null || maxLength == MoonTextInput.noMaxLength || maxLength > 0), + super( + initialValue: controller != null ? controller.text : (initialValue ?? ''), + enabled: enabled ?? true, + autovalidateMode: autovalidateMode ?? AutovalidateMode.disabled, + builder: (FormFieldState field) { + final _TextFormFieldState state = field as _TextFormFieldState; + + void onChangedHandler(String value) { + field.didChange(value); + if (onChanged != null) { + onChanged(value); + } + } + + return UnmanagedRestorationScope( + bucket: field.bucket, + child: MoonTextInput( + activeBorderColor: activeBorderColor, + autocorrect: autocorrect, + autofillHints: autofillHints, + autofocus: autofocus, + backgroundColor: backgroundColor, + borderRadius: borderRadius, + clipBehavior: clipBehavior, + contextMenuBuilder: contextMenuBuilder, + controller: state._effectiveController, + cursorColor: cursorColor, + cursorHeight: cursorHeight, + cursorRadius: cursorRadius, + cursorWidth: cursorWidth, + dragStartBehavior: dragStartBehavior, + enabled: enabled ?? true, + enableIMEPersonalizedLearning: enableIMEPersonalizedLearning, + enableInteractiveSelection: enableInteractiveSelection ?? (!obscureText || !readOnly), + enableSuggestions: enableSuggestions, + errorBorderColor: errorBorderColor, + errorBuilder: errorBuilder, + errorText: field.errorText, + expands: expands, + focusNode: focusNode, + gap: gap, + hasFloatingLabel: hasFloatingLabel, + hasFocusEffect: hasFocusEffect, + height: height, + hintText: hintText, + hintTextColor: hintTextColor, + hoverBorderColor: hoverBorderColor, + inactiveBorderColor: inactiveBorderColor, + initialValue: initialValue, + inputFormatters: inputFormatters, + isDense: isDense, + keyboardAppearance: keyboardAppearance, + keyboardType: keyboardType, + leading: leading, + magnifierConfiguration: magnifierConfiguration, + maxLength: maxLength, + maxLengthEnforcement: maxLengthEnforcement, + maxLines: maxLines, + minLines: minLines, + mouseCursor: mouseCursor, + obscureText: obscureText, + obscuringCharacter: obscuringCharacter, + onAppPrivateCommand: onAppPrivateCommand, + onChanged: onChangedHandler, + onEditingComplete: onEditingComplete, + onSubmitted: onFieldSubmitted, + onTap: onTap, + onTapOutside: onTapOutside, + padding: padding, + readOnly: readOnly, + restorationId: restorationId, + scribbleEnabled: scribbleEnabled, + scrollController: scrollController, + scrollPadding: scrollPadding, + scrollPhysics: scrollPhysics, + selectionControls: selectionControls, + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + showCursor: showCursor, + smartDashesType: smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + smartQuotesType: smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), + spellCheckConfiguration: spellCheckConfiguration, + strutStyle: strutStyle, + helperPadding: helperPadding, + helper: helper, + helperTextStyle: helperTextStyle, + textAlign: textAlign, + textAlignVertical: textAlignVertical, + textCapitalization: textCapitalization, + textColor: textColor, + textDirection: textDirection, + textInputAction: textInputAction, + textInputSize: textInputSize, + textStyle: style, + trailing: trailing, + transitionCurve: transitionCurve, + transitionDuration: transitionDuration, + ), + ); + }, + ); + + /// Controls the text being edited. + /// + /// If null, this widget will create its own [TextEditingController] and + /// initialize its [TextEditingController.text] with [initialValue]. + final TextEditingController? controller; + + static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) { + return AdaptiveTextSelectionToolbar.editableText( + editableTextState: editableTextState, + ); + } + + @override + FormFieldState createState() => _TextFormFieldState(); +} + +class _TextFormFieldState extends FormFieldState { + RestorableTextEditingController? _controller; + + TextEditingController get _effectiveController => _textFormField.controller ?? _controller!.value; + + MoonFormTextInput get _textFormField => super.widget as MoonFormTextInput; + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + super.restoreState(oldBucket, initialRestore); + if (_controller != null) { + _registerController(); + } + // Make sure to update the internal [FormFieldState] value to sync up with + // text editing controller value. + setValue(_effectiveController.text); + } + + void _registerController() { + assert(_controller != null); + registerForRestoration(_controller!, 'controller'); + } + + void _createLocalController([TextEditingValue? value]) { + assert(_controller == null); + _controller = value == null ? RestorableTextEditingController() : RestorableTextEditingController.fromValue(value); + if (!restorePending) { + _registerController(); + } + } + + @override + void initState() { + super.initState(); + if (_textFormField.controller == null) { + _createLocalController(widget.initialValue != null ? TextEditingValue(text: widget.initialValue!) : null); + } else { + _textFormField.controller!.addListener(_handleControllerChanged); + } + } + + @override + void didUpdateWidget(MoonFormTextInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (_textFormField.controller != oldWidget.controller) { + oldWidget.controller?.removeListener(_handleControllerChanged); + _textFormField.controller?.addListener(_handleControllerChanged); + + if (oldWidget.controller != null && _textFormField.controller == null) { + _createLocalController(oldWidget.controller!.value); + } + + if (_textFormField.controller != null) { + setValue(_textFormField.controller!.text); + if (oldWidget.controller == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + } + } + } + } + + @override + void dispose() { + _textFormField.controller?.removeListener(_handleControllerChanged); + _controller?.dispose(); + super.dispose(); + } + + @override + void didChange(String? value) { + super.didChange(value); + + if (_effectiveController.text != value) { + _effectiveController.text = value ?? ''; + } + } + + @override + void reset() { + // setState will be called in the superclass, so even though state is being + // manipulated, no setState call is needed here. + _effectiveController.text = widget.initialValue ?? ''; + super.reset(); + } + + void _handleControllerChanged() { + // Suppress changes that originated from within this class. + // + // In the case where a controller has been passed in to this widget, we + // register this change listener. In these cases, we'll also receive change + // notifications for changes originating from within this class -- for + // example, the reset() method. In such cases, the FormField value will + // already have been set. + if (_effectiveController.text != value) { + didChange(_effectiveController.text); + } + } +} diff --git a/lib/src/widgets/text_input/input_decorator.dart b/lib/src/widgets/text_input/input_decorator.dart new file mode 100644 index 00000000..e7cf3bb3 --- /dev/null +++ b/lib/src/widgets/text_input/input_decorator.dart @@ -0,0 +1,3992 @@ +// TODO: check periodically vs Flutter source code for updates + +// This file is modified from the original Flutter source code to revert https://github.com/flutter/flutter/pull/55260/files +// which added height: 1 property to labelStyle causing the label to be misaligned in the input with an upward bias. +// No other changes were made. + +// 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. + +// ignore_for_file: parameter_assignments + +import 'dart:math' as math; +import 'dart:ui' show lerpDouble; + +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; + +// Examples can assume: +// late Widget _myIcon; + +const Duration _kTransitionDuration = Duration(milliseconds: 200); +const Curve _kTransitionCurve = Curves.fastOutSlowIn; +const double _kFinalLabelScale = 0.75; + +// Defines the gap in the InputDecorator's outline border where the +// floating label will appear. +class _InputBorderGap extends ChangeNotifier { + double? _start; + double? get start => _start; + set start(double? value) { + if (value != _start) { + _start = value; + notifyListeners(); + } + } + + double _extent = 0.0; + double get extent => _extent; + set extent(double value) { + if (value != _extent) { + _extent = value; + notifyListeners(); + } + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes, this class is not used in collection + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is _InputBorderGap && other.start == start && other.extent == extent; + } + + @override + // ignore: avoid_equals_and_hash_code_on_mutable_classes, this class is not used in collection + int get hashCode => Object.hash(start, extent); + + @override + String toString() => describeIdentity(this); +} + +// Used to interpolate between two InputBorders. +class _InputBorderTween extends Tween { + _InputBorderTween({super.begin, super.end}); + + @override + InputBorder lerp(double t) => ShapeBorder.lerp(begin, end, t)! as InputBorder; +} + +// Passes the _InputBorderGap parameters along to an InputBorder's paint method. +class _InputBorderPainter extends CustomPainter { + _InputBorderPainter({ + required Listenable repaint, + required this.borderAnimation, + required this.border, + required this.gapAnimation, + required this.gap, + required this.textDirection, + required this.fillColor, + required this.hoverAnimation, + required this.hoverColorTween, + }) : super(repaint: repaint); + + final Animation borderAnimation; + final _InputBorderTween border; + final Animation gapAnimation; + final _InputBorderGap gap; + final TextDirection textDirection; + final Color fillColor; + final ColorTween hoverColorTween; + final Animation hoverAnimation; + + Color get blendedColor => Color.alphaBlend(hoverColorTween.evaluate(hoverAnimation)!, fillColor); + + @override + void paint(Canvas canvas, Size size) { + final InputBorder borderValue = border.evaluate(borderAnimation); + final Rect canvasRect = Offset.zero & size; + final Color blendedFillColor = blendedColor; + if (blendedFillColor.alpha > 0) { + canvas.drawPath( + borderValue.getOuterPath(canvasRect, textDirection: textDirection), + Paint() + ..color = blendedFillColor + ..style = PaintingStyle.fill, + ); + } + + borderValue.paint( + canvas, + canvasRect, + gapStart: gap.start, + gapExtent: gap.extent, + gapPercentage: gapAnimation.value, + textDirection: textDirection, + ); + } + + @override + bool shouldRepaint(_InputBorderPainter oldPainter) { + return borderAnimation != oldPainter.borderAnimation || + hoverAnimation != oldPainter.hoverAnimation || + gapAnimation != oldPainter.gapAnimation || + border != oldPainter.border || + gap != oldPainter.gap || + textDirection != oldPainter.textDirection; + } + + @override + String toString() => describeIdentity(this); +} + +// An analog of AnimatedContainer, which can animate its shaped border, for +// _InputBorder. This specialized animated container is needed because the +// _InputBorderGap, which is computed at layout time, is required by the +// _InputBorder's paint method. +class _BorderContainer extends StatefulWidget { + const _BorderContainer({ + required this.border, + required this.gap, + required this.gapAnimation, + required this.fillColor, + required this.hoverColor, + required this.isHovering, + }); + + final InputBorder border; + final _InputBorderGap gap; + final Animation gapAnimation; + final Color fillColor; + final Color hoverColor; + final bool isHovering; + + @override + _BorderContainerState createState() => _BorderContainerState(); +} + +class _BorderContainerState extends State<_BorderContainer> with TickerProviderStateMixin { + static const Duration _kHoverDuration = Duration(milliseconds: 15); + + late AnimationController _controller; + late AnimationController _hoverColorController; + late Animation _borderAnimation; + late _InputBorderTween _border; + late Animation _hoverAnimation; + late ColorTween _hoverColorTween; + + @override + void initState() { + super.initState(); + _hoverColorController = AnimationController( + duration: _kHoverDuration, + value: widget.isHovering ? 1.0 : 0.0, + vsync: this, + ); + _controller = AnimationController( + duration: _kTransitionDuration, + vsync: this, + ); + _borderAnimation = CurvedAnimation( + parent: _controller, + curve: _kTransitionCurve, + ); + _border = _InputBorderTween( + begin: widget.border, + end: widget.border, + ); + _hoverAnimation = CurvedAnimation( + parent: _hoverColorController, + curve: Curves.linear, + ); + _hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor); + } + + @override + void dispose() { + _controller.dispose(); + _hoverColorController.dispose(); + super.dispose(); + } + + @override + void didUpdateWidget(_BorderContainer oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.border != oldWidget.border) { + _border = _InputBorderTween( + begin: oldWidget.border, + end: widget.border, + ); + _controller + ..value = 0.0 + ..forward(); + } + if (widget.hoverColor != oldWidget.hoverColor) { + _hoverColorTween = ColorTween(begin: Colors.transparent, end: widget.hoverColor); + } + if (widget.isHovering != oldWidget.isHovering) { + if (widget.isHovering) { + _hoverColorController.forward(); + } else { + _hoverColorController.reverse(); + } + } + } + + @override + Widget build(BuildContext context) { + return CustomPaint( + foregroundPainter: _InputBorderPainter( + repaint: Listenable.merge([ + _borderAnimation, + widget.gap, + _hoverColorController, + ]), + borderAnimation: _borderAnimation, + border: _border, + gapAnimation: widget.gapAnimation, + gap: widget.gap, + textDirection: Directionality.of(context), + fillColor: widget.fillColor, + hoverColorTween: _hoverColorTween, + hoverAnimation: _hoverAnimation, + ), + ); + } +} + +// Used to "shake" the floating label to the left to the left and right +// when the errorText first appears. +class _Shaker extends AnimatedWidget { + const _Shaker({ + required Animation animation, + this.child, + }) : super(listenable: animation); + + final Widget? child; + + Animation get animation => listenable as Animation; + + double get translateX { + const double shakeDelta = 4.0; + final double t = animation.value; + if (t <= 0.25) { + return -t * shakeDelta; + } else if (t < 0.75) { + return (t - 0.5) * shakeDelta; + } else { + return (1.0 - t) * 4.0 * shakeDelta; + } + } + + @override + Widget build(BuildContext context) { + return Transform( + transform: Matrix4.translationValues(translateX, 0.0, 0.0), + child: child, + ); + } +} + +// Display the helper and error text. When the error text appears +// it fades and the helper text fades out. The error text also +// slides upwards a little when it first appears. +class _HelperError extends StatefulWidget { + const _HelperError({ + this.textAlign, + this.helperText, + this.helperStyle, + this.helperMaxLines, + this.errorText, + this.errorStyle, + this.errorMaxLines, + }); + + final TextAlign? textAlign; + final String? helperText; + final TextStyle? helperStyle; + final int? helperMaxLines; + final String? errorText; + final TextStyle? errorStyle; + final int? errorMaxLines; + + @override + _HelperErrorState createState() => _HelperErrorState(); +} + +class _HelperErrorState extends State<_HelperError> with SingleTickerProviderStateMixin { + // If the height of this widget and the counter are zero ("empty") at + // layout time, no space is allocated for the subtext. + static const Widget empty = SizedBox.shrink(); + + late AnimationController _controller; + Widget? _helper; + Widget? _error; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: _kTransitionDuration, + vsync: this, + ); + if (widget.errorText != null) { + _error = _buildError(); + _controller.value = 1.0; + } else if (widget.helperText != null) { + _helper = _buildHelper(); + } + _controller.addListener(_handleChange); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + void _handleChange() { + setState(() { + // The _controller's value has changed. + }); + } + + @override + void didUpdateWidget(_HelperError old) { + super.didUpdateWidget(old); + + final String? newErrorText = widget.errorText; + final String? newHelperText = widget.helperText; + final String? oldErrorText = old.errorText; + final String? oldHelperText = old.helperText; + + final bool errorTextStateChanged = (newErrorText != null) != (oldErrorText != null); + final bool helperTextStateChanged = newErrorText == null && (newHelperText != null) != (oldHelperText != null); + + if (errorTextStateChanged || helperTextStateChanged) { + if (newErrorText != null) { + _error = _buildError(); + _controller.forward(); + } else if (newHelperText != null) { + _helper = _buildHelper(); + _controller.reverse(); + } else { + _controller.reverse(); + } + } + } + + Widget _buildHelper() { + assert(widget.helperText != null); + return Semantics( + container: true, + child: FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0).animate(_controller), + child: Text( + widget.helperText!, + style: widget.helperStyle, + textAlign: widget.textAlign, + overflow: TextOverflow.ellipsis, + maxLines: widget.helperMaxLines, + ), + ), + ); + } + + Widget _buildError() { + assert(widget.errorText != null); + return Semantics( + container: true, + liveRegion: true, + child: FadeTransition( + opacity: _controller, + child: FractionalTranslation( + translation: Tween( + begin: const Offset(0.0, -0.25), + end: Offset.zero, + ).evaluate(_controller.view), + child: Text( + widget.errorText!, + style: widget.errorStyle, + textAlign: widget.textAlign, + overflow: TextOverflow.ellipsis, + maxLines: widget.errorMaxLines, + ), + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + if (_controller.isDismissed) { + _error = null; + if (widget.helperText != null) { + return _helper = _buildHelper(); + } else { + _helper = null; + return empty; + } + } + + if (_controller.isCompleted) { + _helper = null; + if (widget.errorText != null) { + return _error = _buildError(); + } else { + _error = null; + return empty; + } + } + + if (_helper == null && widget.errorText != null) { + return _buildError(); + } + + if (_error == null && widget.helperText != null) { + return _buildHelper(); + } + + if (widget.errorText != null) { + return Stack( + children: [ + FadeTransition( + opacity: Tween(begin: 1.0, end: 0.0).animate(_controller), + child: _helper, + ), + _buildError(), + ], + ); + } + + if (widget.helperText != null) { + return Stack( + children: [ + _buildHelper(), + FadeTransition( + opacity: _controller, + child: _error, + ), + ], + ); + } + + return empty; + } +} + +/// Defines **how** the floating label should behave. +/// +/// See also: +/// +/// * [InputDecoration.floatingLabelBehavior] which defines the behavior for +/// [InputDecoration.label] or [InputDecoration.labelText]. +/// * [FloatingLabelAlignment] which defines **where** the floating label +/// should displayed. +enum FloatingLabelBehavior { + /// The label will always be positioned within the content, or hidden. + never, + + /// The label will float when the input is focused, or has content. + auto, + + /// The label will always float above the content. + always, +} + +/// Defines **where** the floating label should be displayed within an +/// [InputDecorator]. +/// +/// See also: +/// +/// * [InputDecoration.floatingLabelAlignment] which defines the alignment for +/// [InputDecoration.label] or [InputDecoration.labelText]. +/// * [FloatingLabelBehavior] which defines **how** the floating label should +/// behave. +@immutable +class FloatingLabelAlignment { + const FloatingLabelAlignment._(this._x) : assert(_x >= -1.0 && _x <= 1.0); + + // -1 denotes start, 0 denotes center, and 1 denotes end. + final double _x; + + /// Align the floating label on the leading edge of the [InputDecorator]. + /// + /// For left-to-right text ([TextDirection.ltr]), this is the left edge. + /// + /// For right-to-left text ([TextDirection.rtl]), this is the right edge. + static const FloatingLabelAlignment start = FloatingLabelAlignment._(-1.0); + + /// Aligns the floating label to the center of an [InputDecorator]. + static const FloatingLabelAlignment center = FloatingLabelAlignment._(0.0); + + @override + int get hashCode => _x.hashCode; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is FloatingLabelAlignment && _x == other._x; + } + + static String _stringify(double x) { + if (x == -1.0) { + return 'FloatingLabelAlignment.start'; + } + if (x == 0.0) { + return 'FloatingLabelAlignment.center'; + } + return 'FloatingLabelAlignment(x: ${x.toStringAsFixed(1)})'; + } + + @override + String toString() => _stringify(_x); +} + +// Identifies the children of a _RenderDecorationElement. +enum _DecorationSlot { + icon, + input, + label, + hint, + prefix, + suffix, + prefixIcon, + suffixIcon, + helperError, + counter, + container, +} + +// An analog of InputDecoration for the _Decorator widget. +@immutable +class _Decoration { + const _Decoration({ + required this.contentPadding, + required this.isCollapsed, + required this.floatingLabelHeight, + required this.floatingLabelProgress, + required this.floatingLabelAlignment, + required this.border, + required this.borderGap, + required this.alignLabelWithHint, + required this.isDense, + required this.visualDensity, + this.icon, + this.input, + this.label, + this.hint, + this.prefix, + this.suffix, + this.prefixIcon, + this.suffixIcon, + this.helperError, + this.counter, + this.container, + }); + + final EdgeInsetsGeometry contentPadding; + final bool isCollapsed; + final double floatingLabelHeight; + final double floatingLabelProgress; + final FloatingLabelAlignment floatingLabelAlignment; + final InputBorder border; + final _InputBorderGap borderGap; + final bool alignLabelWithHint; + final bool? isDense; + final VisualDensity visualDensity; + final Widget? icon; + final Widget? input; + final Widget? label; + final Widget? hint; + final Widget? prefix; + final Widget? suffix; + final Widget? prefixIcon; + final Widget? suffixIcon; + final Widget? helperError; + final Widget? counter; + final Widget? container; + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is _Decoration && + other.contentPadding == contentPadding && + other.isCollapsed == isCollapsed && + other.floatingLabelHeight == floatingLabelHeight && + other.floatingLabelProgress == floatingLabelProgress && + other.floatingLabelAlignment == floatingLabelAlignment && + other.border == border && + other.borderGap == borderGap && + other.alignLabelWithHint == alignLabelWithHint && + other.isDense == isDense && + other.visualDensity == visualDensity && + other.icon == icon && + other.input == input && + other.label == label && + other.hint == hint && + other.prefix == prefix && + other.suffix == suffix && + other.prefixIcon == prefixIcon && + other.suffixIcon == suffixIcon && + other.helperError == helperError && + other.counter == counter && + other.container == container; + } + + @override + int get hashCode => Object.hash( + contentPadding, + floatingLabelHeight, + floatingLabelProgress, + floatingLabelAlignment, + border, + borderGap, + alignLabelWithHint, + isDense, + visualDensity, + icon, + input, + label, + hint, + prefix, + suffix, + prefixIcon, + suffixIcon, + helperError, + counter, + container, + ); +} + +// A container for the layout values computed by _RenderDecoration._layout. +// These values are used by _RenderDecoration.performLayout to position +// all of the renderer children of a _RenderDecoration. +class _RenderDecorationLayout { + const _RenderDecorationLayout({ + required this.boxToBaseline, + required this.inputBaseline, // for InputBorderType.underline + required this.outlineBaseline, // for InputBorderType.outline + required this.subtextBaseline, + required this.containerHeight, + required this.subtextHeight, + }); + + final Map boxToBaseline; + final double inputBaseline; + final double outlineBaseline; + final double subtextBaseline; // helper/error counter + final double containerHeight; + final double subtextHeight; +} + +// The workhorse: layout and paint a _Decorator widget's _Decoration. +class _RenderDecoration extends RenderBox with SlottedContainerRenderObjectMixin<_DecorationSlot> { + _RenderDecoration({ + required _Decoration decoration, + required TextDirection textDirection, + required TextBaseline textBaseline, + required bool isFocused, + required bool expands, + required bool material3, + TextAlignVertical? textAlignVertical, + }) : _decoration = decoration, + _textDirection = textDirection, + _textBaseline = textBaseline, + _textAlignVertical = textAlignVertical, + _isFocused = isFocused, + _expands = expands, + _material3 = material3; + + static const double subtextGap = 8.0; + + RenderBox? get icon => childForSlot(_DecorationSlot.icon); + RenderBox? get input => childForSlot(_DecorationSlot.input); + RenderBox? get label => childForSlot(_DecorationSlot.label); + RenderBox? get hint => childForSlot(_DecorationSlot.hint); + RenderBox? get prefix => childForSlot(_DecorationSlot.prefix); + RenderBox? get suffix => childForSlot(_DecorationSlot.suffix); + RenderBox? get prefixIcon => childForSlot(_DecorationSlot.prefixIcon); + RenderBox? get suffixIcon => childForSlot(_DecorationSlot.suffixIcon); + RenderBox? get helperError => childForSlot(_DecorationSlot.helperError); + RenderBox? get counter => childForSlot(_DecorationSlot.counter); + RenderBox? get container => childForSlot(_DecorationSlot.container); + + // The returned list is ordered for hit testing. + @override + Iterable get children { + return [ + if (icon != null) icon!, + if (input != null) input!, + if (prefixIcon != null) prefixIcon!, + if (suffixIcon != null) suffixIcon!, + if (prefix != null) prefix!, + if (suffix != null) suffix!, + if (label != null) label!, + if (hint != null) hint!, + if (helperError != null) helperError!, + if (counter != null) counter!, + if (container != null) container!, + ]; + } + + _Decoration get decoration => _decoration; + _Decoration _decoration; + set decoration(_Decoration value) { + if (_decoration == value) { + return; + } + _decoration = value; + markNeedsLayout(); + } + + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + if (_textDirection == value) { + return; + } + _textDirection = value; + markNeedsLayout(); + } + + TextBaseline get textBaseline => _textBaseline; + TextBaseline _textBaseline; + set textBaseline(TextBaseline value) { + if (_textBaseline == value) { + return; + } + _textBaseline = value; + markNeedsLayout(); + } + + TextAlignVertical get _defaultTextAlignVertical => + _isOutlineAligned ? TextAlignVertical.center : TextAlignVertical.top; + TextAlignVertical get textAlignVertical => _textAlignVertical ?? _defaultTextAlignVertical; + TextAlignVertical? _textAlignVertical; + set textAlignVertical(TextAlignVertical? value) { + if (_textAlignVertical == value) { + return; + } + // No need to relayout if the effective value is still the same. + if (textAlignVertical.y == (value?.y ?? _defaultTextAlignVertical.y)) { + _textAlignVertical = value; + return; + } + _textAlignVertical = value; + markNeedsLayout(); + } + + bool get isFocused => _isFocused; + bool _isFocused; + set isFocused(bool value) { + if (_isFocused == value) { + return; + } + _isFocused = value; + markNeedsSemanticsUpdate(); + } + + bool get expands => _expands; + bool _expands = false; + set expands(bool value) { + if (_expands == value) { + return; + } + _expands = value; + markNeedsLayout(); + } + + bool get material3 => _material3; + bool _material3 = false; + set material3(bool value) { + if (_material3 == value) { + return; + } + _material3 = value; + markNeedsLayout(); + } + + // Indicates that the decoration should be aligned to accommodate an outline + // border. + bool get _isOutlineAligned { + return !decoration.isCollapsed && decoration.border.isOutline; + } + + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + if (icon != null) { + visitor(icon!); + } + if (prefix != null) { + visitor(prefix!); + } + if (prefixIcon != null) { + visitor(prefixIcon!); + } + + if (label != null) { + visitor(label!); + } + if (hint != null) { + if (isFocused) { + visitor(hint!); + } else if (label == null) { + visitor(hint!); + } + } + + if (input != null) { + visitor(input!); + } + if (suffixIcon != null) { + visitor(suffixIcon!); + } + if (suffix != null) { + visitor(suffix!); + } + if (container != null) { + visitor(container!); + } + if (helperError != null) { + visitor(helperError!); + } + if (counter != null) { + visitor(counter!); + } + } + + @override + bool get sizedByParent => false; + + static double _minWidth(RenderBox? box, double height) { + return box == null ? 0.0 : box.getMinIntrinsicWidth(height); + } + + static double _maxWidth(RenderBox? box, double height) { + return box == null ? 0.0 : box.getMaxIntrinsicWidth(height); + } + + static double _minHeight(RenderBox? box, double width) { + return box == null ? 0.0 : box.getMinIntrinsicHeight(width); + } + + static Size _boxSize(RenderBox? box) => box == null ? Size.zero : box.size; + + static BoxParentData _boxParentData(RenderBox box) => box.parentData! as BoxParentData; + + EdgeInsets get contentPadding => decoration.contentPadding as EdgeInsets; + + // Lay out the given box if needed, and return its baseline. + double _layoutLineBox(RenderBox? box, BoxConstraints constraints) { + if (box == null) { + return 0.0; + } + box.layout(constraints, parentUsesSize: true); + // Since internally, all layout is performed against the alphabetic baseline, + // (eg, ascents/descents are all relative to alphabetic, even if the font is + // an ideographic or hanging font), we should always obtain the reference + // baseline from the alphabetic baseline. The ideographic baseline is for + // use post-layout and is derived from the alphabetic baseline combined with + // the font metrics. + final double baseline = box.getDistanceToBaseline(TextBaseline.alphabetic)!; + + assert(() { + if (baseline >= 0) { + return true; + } + throw FlutterError.fromParts([ + ErrorSummary("One of InputDecorator's children reported a negative baseline offset."), + ErrorDescription( + '${box.runtimeType}, of size ${box.size}, reported a negative ' + 'alphabetic baseline of $baseline.', + ), + ]); + }()); + return baseline; + } + + // Returns a value used by performLayout to position all of the renderers. + // This method applies layout to all of the renderers except the container. + // For convenience, the container is laid out in performLayout(). + _RenderDecorationLayout _layout(BoxConstraints layoutConstraints) { + assert( + layoutConstraints.maxWidth < double.infinity, + 'An InputDecorator, which is typically created by a TextField, cannot ' + 'have an unbounded width.\n' + 'This happens when the parent widget does not provide a finite width ' + 'constraint. For example, if the InputDecorator is contained by a Row, ' + 'then its width must be constrained. An Expanded widget or a SizedBox ' + 'can be used to constrain the width of the InputDecorator or the ' + 'TextField that contains it.', + ); + + // Margin on each side of subtext (counter and helperError) + final Map boxToBaseline = {}; + final BoxConstraints boxConstraints = layoutConstraints.loosen(); + + // Layout all the widgets used by InputDecorator + boxToBaseline[icon] = _layoutLineBox(icon, boxConstraints); + final BoxConstraints containerConstraints = boxConstraints.copyWith( + maxWidth: boxConstraints.maxWidth - _boxSize(icon).width, + ); + boxToBaseline[prefixIcon] = _layoutLineBox(prefixIcon, containerConstraints); + boxToBaseline[suffixIcon] = _layoutLineBox(suffixIcon, containerConstraints); + final BoxConstraints contentConstraints = containerConstraints.copyWith( + maxWidth: containerConstraints.maxWidth - contentPadding.horizontal, + ); + boxToBaseline[prefix] = _layoutLineBox(prefix, contentConstraints); + boxToBaseline[suffix] = _layoutLineBox(suffix, contentConstraints); + + final double inputWidth = math.max( + 0.0, + constraints.maxWidth - + (_boxSize(icon).width + + contentPadding.left + + _boxSize(prefixIcon).width + + _boxSize(prefix).width + + _boxSize(suffix).width + + _boxSize(suffixIcon).width + + contentPadding.right), + ); + // Increase the available width for the label when it is scaled down. + final double invertedLabelScale = lerpDouble(1.00, 1 / _kFinalLabelScale, decoration.floatingLabelProgress)!; + double suffixIconWidth = _boxSize(suffixIcon).width; + if (decoration.border.isOutline) { + suffixIconWidth = lerpDouble(suffixIconWidth, 0.0, decoration.floatingLabelProgress)!; + } + final double labelWidth = math.max( + 0.0, + constraints.maxWidth - + (_boxSize(icon).width + + contentPadding.left + + _boxSize(prefixIcon).width + + suffixIconWidth + + contentPadding.right), + ); + boxToBaseline[label] = _layoutLineBox( + label, + boxConstraints.copyWith(maxWidth: labelWidth * invertedLabelScale), + ); + boxToBaseline[hint] = _layoutLineBox( + hint, + boxConstraints.copyWith(minWidth: inputWidth, maxWidth: inputWidth), + ); + boxToBaseline[counter] = _layoutLineBox(counter, contentConstraints); + + // The helper or error text can occupy the full width less the space + // occupied by the icon and counter. + boxToBaseline[helperError] = _layoutLineBox( + helperError, + contentConstraints.copyWith( + maxWidth: math.max(0.0, contentConstraints.maxWidth - _boxSize(counter).width), + ), + ); + + // The height of the input needs to accommodate label above and counter and + // helperError below, when they exist. + final double labelHeight = label == null ? 0 : decoration.floatingLabelHeight; + final double topHeight = + decoration.border.isOutline ? math.max(labelHeight - boxToBaseline[label]!, 0) : labelHeight; + final double counterHeight = counter == null ? 0 : boxToBaseline[counter]! + subtextGap; + final bool helperErrorExists = helperError?.size != null && helperError!.size.height > 0; + final double helperErrorHeight = !helperErrorExists ? 0 : helperError!.size.height + subtextGap; + final double bottomHeight = math.max( + counterHeight, + helperErrorHeight, + ); + final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment; + boxToBaseline[input] = _layoutLineBox( + input, + boxConstraints + .deflate( + EdgeInsets.only( + top: contentPadding.top + topHeight + densityOffset.dy / 2, + bottom: contentPadding.bottom + bottomHeight + densityOffset.dy / 2, + ), + ) + .copyWith( + minWidth: inputWidth, + maxWidth: inputWidth, + ), + ); + + // The field can be occupied by a hint or by the input itself + final double hintHeight = hint?.size.height ?? 0; + final double inputDirectHeight = input?.size.height ?? 0; + final double inputHeight = math.max(hintHeight, inputDirectHeight); + final double inputInternalBaseline = math.max( + boxToBaseline[input]!, + boxToBaseline[hint]!, + ); + + // Calculate the amount that prefix/suffix affects height above and below + // the input. + final double prefixHeight = prefix?.size.height ?? 0; + final double suffixHeight = suffix?.size.height ?? 0; + final double fixHeight = math.max( + boxToBaseline[prefix]!, + boxToBaseline[suffix]!, + ); + final double fixAboveInput = math.max(0, fixHeight - inputInternalBaseline); + final double fixBelowBaseline = math.max( + prefixHeight - boxToBaseline[prefix]!, + suffixHeight - boxToBaseline[suffix]!, + ); + // TODO(justinmc): fixBelowInput should have no effect when there is no + // prefix/suffix below the input. + // https://github.com/flutter/flutter/issues/66050 + final double fixBelowInput = math.max( + 0, + fixBelowBaseline - (inputHeight - inputInternalBaseline), + ); + + // Calculate the height of the input text container. + final double prefixIconHeight = prefixIcon?.size.height ?? 0; + final double suffixIconHeight = suffixIcon?.size.height ?? 0; + final double fixIconHeight = math.max(prefixIconHeight, suffixIconHeight); + final double contentHeight = math.max( + fixIconHeight, + topHeight + + contentPadding.top + + fixAboveInput + + inputHeight + + fixBelowInput + + contentPadding.bottom + + densityOffset.dy, + ); + final double minContainerHeight = + decoration.isDense! || decoration.isCollapsed || expands ? 0.0 : kMinInteractiveDimension; + final double maxContainerHeight = boxConstraints.maxHeight - bottomHeight; + final double containerHeight = + expands ? maxContainerHeight : math.min(math.max(contentHeight, minContainerHeight), maxContainerHeight); + + // Ensure the text is vertically centered in cases where the content is + // shorter than kMinInteractiveDimension. + final double interactiveAdjustment = + minContainerHeight > contentHeight ? (minContainerHeight - contentHeight) / 2.0 : 0.0; + + // Try to consider the prefix/suffix as part of the text when aligning it. + // If the prefix/suffix overflows however, allow it to extend outside of the + // input and align the remaining part of the text and prefix/suffix. + final double overflow = math.max(0, contentHeight - maxContainerHeight); + // Map textAlignVertical from -1:1 to 0:1 so that it can be used to scale + // the baseline from its minimum to maximum values. + final double textAlignVerticalFactor = (textAlignVertical.y + 1.0) / 2.0; + // Adjust to try to fit top overflow inside the input on an inverse scale of + // textAlignVertical, so that top aligned text adjusts the most and bottom + // aligned text doesn't adjust at all. + final double baselineAdjustment = fixAboveInput - overflow * (1 - textAlignVerticalFactor); + + // The baselines that will be used to draw the actual input text content. + final double topInputBaseline = contentPadding.top + + topHeight + + inputInternalBaseline + + baselineAdjustment + + interactiveAdjustment + + densityOffset.dy / 2.0; + final double maxContentHeight = containerHeight - contentPadding.vertical - topHeight - densityOffset.dy; + final double alignableHeight = fixAboveInput + inputHeight + fixBelowInput; + final double maxVerticalOffset = maxContentHeight - alignableHeight; + final double textAlignVerticalOffset = maxVerticalOffset * textAlignVerticalFactor; + final double inputBaseline = topInputBaseline + textAlignVerticalOffset; + + // The three main alignments for the baseline when an outline is present are + // + // * top (-1.0): topmost point considering padding. + // * center (0.0): the absolute center of the input ignoring padding but + // accommodating the border and floating label. + // * bottom (1.0): bottommost point considering padding. + // + // That means that if the padding is uneven, center is not the exact + // midpoint of top and bottom. To account for this, the above center and + // below center alignments are interpolated independently. + final double outlineCenterBaseline = + inputInternalBaseline + baselineAdjustment / 2.0 + (containerHeight - (2.0 + inputHeight)) / 2.0; + final double outlineTopBaseline = topInputBaseline; + final double outlineBottomBaseline = topInputBaseline + maxVerticalOffset; + final double outlineBaseline = _interpolateThree( + outlineTopBaseline, + outlineCenterBaseline, + outlineBottomBaseline, + textAlignVertical, + ); + + // Find the positions of the text below the input when it exists. + double subtextCounterBaseline = 0; + double subtextHelperBaseline = 0; + double subtextCounterHeight = 0; + double subtextHelperHeight = 0; + if (counter != null) { + subtextCounterBaseline = containerHeight + subtextGap + boxToBaseline[counter]!; + subtextCounterHeight = counter!.size.height + subtextGap; + } + if (helperErrorExists) { + subtextHelperBaseline = containerHeight + subtextGap + boxToBaseline[helperError]!; + subtextHelperHeight = helperErrorHeight; + } + final double subtextBaseline = math.max( + subtextCounterBaseline, + subtextHelperBaseline, + ); + final double subtextHeight = math.max( + subtextCounterHeight, + subtextHelperHeight, + ); + + return _RenderDecorationLayout( + boxToBaseline: boxToBaseline, + containerHeight: containerHeight, + inputBaseline: inputBaseline, + outlineBaseline: outlineBaseline, + subtextBaseline: subtextBaseline, + subtextHeight: subtextHeight, + ); + } + + // Interpolate between three stops using textAlignVertical. This is used to + // calculate the outline baseline, which ignores padding when the alignment is + // middle. When the alignment is less than zero, it interpolates between the + // centered text box's top and the top of the content padding. When the + // alignment is greater than zero, it interpolates between the centered box's + // top and the position that would align the bottom of the box with the bottom + // padding. + double _interpolateThree(double begin, double middle, double end, TextAlignVertical textAlignVertical) { + if (textAlignVertical.y <= 0) { + // It's possible for begin, middle, and end to not be in order because of + // excessive padding. Those cases are handled by using middle. + if (begin >= middle) { + return middle; + } + // Do a standard linear interpolation on the first half, between begin and + // middle. + final double t = textAlignVertical.y + 1; + return begin + (middle - begin) * t; + } + + if (middle >= end) { + return middle; + } + // Do a standard linear interpolation on the second half, between middle and + // end. + final double t = textAlignVertical.y; + return middle + (end - middle) * t; + } + + @override + double computeMinIntrinsicWidth(double height) { + return _minWidth(icon, height) + + contentPadding.left + + _minWidth(prefixIcon, height) + + _minWidth(prefix, height) + + math.max(_minWidth(input, height), _minWidth(hint, height)) + + _minWidth(suffix, height) + + _minWidth(suffixIcon, height) + + contentPadding.right; + } + + @override + double computeMaxIntrinsicWidth(double height) { + return _maxWidth(icon, height) + + contentPadding.left + + _maxWidth(prefixIcon, height) + + _maxWidth(prefix, height) + + math.max(_maxWidth(input, height), _maxWidth(hint, height)) + + _maxWidth(suffix, height) + + _maxWidth(suffixIcon, height) + + contentPadding.right; + } + + double _lineHeight(double width, List boxes) { + double height = 0.0; + for (final RenderBox? box in boxes) { + if (box == null) { + continue; + } + height = math.max(_minHeight(box, width), height); + } + return height; + // TODO(hansmuller): this should compute the overall line height for the + // boxes when they've been baseline-aligned. + // See https://github.com/flutter/flutter/issues/13715 + } + + @override + double computeMinIntrinsicHeight(double width) { + final double iconHeight = _minHeight(icon, width); + final double iconWidth = _minWidth(icon, iconHeight); + + width = math.max(width - iconWidth, 0.0); + + final double prefixIconHeight = _minHeight(prefixIcon, width); + final double prefixIconWidth = _minWidth(prefixIcon, prefixIconHeight); + + final double suffixIconHeight = _minHeight(suffixIcon, width); + final double suffixIconWidth = _minWidth(suffixIcon, suffixIconHeight); + + width = math.max(width - contentPadding.horizontal, 0.0); + + final double counterHeight = _minHeight(counter, width); + final double counterWidth = _minWidth(counter, counterHeight); + + final double helperErrorAvailableWidth = math.max(width - counterWidth, 0.0); + final double helperErrorHeight = _minHeight(helperError, helperErrorAvailableWidth); + double subtextHeight = math.max(counterHeight, helperErrorHeight); + if (subtextHeight > 0.0) { + subtextHeight += subtextGap; + } + + final double prefixHeight = _minHeight(prefix, width); + final double prefixWidth = _minWidth(prefix, prefixHeight); + + final double suffixHeight = _minHeight(suffix, width); + final double suffixWidth = _minWidth(suffix, suffixHeight); + + final double availableInputWidth = + math.max(width - prefixWidth - suffixWidth - prefixIconWidth - suffixIconWidth, 0.0); + final double inputHeight = _lineHeight(availableInputWidth, [input, hint]); + final double inputMaxHeight = [inputHeight, prefixHeight, suffixHeight].reduce(math.max); + + final Offset densityOffset = decoration.visualDensity.baseSizeAdjustment; + final double contentHeight = contentPadding.top + + (label == null ? 0.0 : decoration.floatingLabelHeight) + + inputMaxHeight + + contentPadding.bottom + + densityOffset.dy; + final double containerHeight = + [iconHeight, contentHeight, prefixIconHeight, suffixIconHeight].reduce(math.max); + final double minContainerHeight = decoration.isDense! || expands ? 0.0 : kMinInteractiveDimension; + return math.max(containerHeight, minContainerHeight) + subtextHeight; + } + + @override + double computeMaxIntrinsicHeight(double width) { + return computeMinIntrinsicHeight(width); + } + + @override + double computeDistanceToActualBaseline(TextBaseline baseline) { + return _boxParentData(input!).offset.dy + (input?.computeDistanceToActualBaseline(baseline) ?? 0.0); + } + + // Records where the label was painted. + Matrix4? _labelTransform; + + @override + Size computeDryLayout(BoxConstraints constraints) { + assert( + debugCannotComputeDryLayout( + reason: 'Layout requires baseline metrics, which are only available after a full layout.', + ), + ); + return Size.zero; + } + + @override + void performLayout() { + final BoxConstraints constraints = this.constraints; + _labelTransform = null; + final _RenderDecorationLayout layout = _layout(constraints); + + final double overallWidth = constraints.maxWidth; + final double overallHeight = layout.containerHeight + layout.subtextHeight; + + final RenderBox? container = this.container; + if (container != null) { + final BoxConstraints containerConstraints = BoxConstraints.tightFor( + height: layout.containerHeight, + width: overallWidth - _boxSize(icon).width, + ); + container.layout(containerConstraints, parentUsesSize: true); + final double x; + switch (textDirection) { + case TextDirection.rtl: + x = 0.0; + break; + case TextDirection.ltr: + x = _boxSize(icon).width; + break; + } + _boxParentData(container).offset = Offset(x, 0.0); + } + + late double height; + double centerLayout(RenderBox box, double x) { + _boxParentData(box).offset = Offset(x, (height - box.size.height) / 2.0); + return box.size.width; + } + + late double baseline; + double baselineLayout(RenderBox box, double x) { + _boxParentData(box).offset = Offset(x, baseline - layout.boxToBaseline[box]!); + return box.size.width; + } + + final double left = contentPadding.left; + final double right = overallWidth - contentPadding.right; + + height = layout.containerHeight; + baseline = _isOutlineAligned ? layout.outlineBaseline : layout.inputBaseline; + + if (icon != null) { + final double x; + switch (textDirection) { + case TextDirection.rtl: + x = overallWidth - icon!.size.width; + break; + case TextDirection.ltr: + x = 0.0; + break; + } + centerLayout(icon!, x); + } + + switch (textDirection) { + case TextDirection.rtl: + { + double start = right - _boxSize(icon).width; + double end = left; + if (prefixIcon != null) { + start += contentPadding.left; + start -= centerLayout(prefixIcon!, start - prefixIcon!.size.width); + } + if (label != null) { + if (decoration.alignLabelWithHint) { + baselineLayout(label!, start - label!.size.width); + } else { + centerLayout(label!, start - label!.size.width); + } + } + if (prefix != null) { + start -= baselineLayout(prefix!, start - prefix!.size.width); + } + if (input != null) { + baselineLayout(input!, start - input!.size.width); + } + if (hint != null) { + baselineLayout(hint!, start - hint!.size.width); + } + if (suffixIcon != null) { + end -= contentPadding.left; + end += centerLayout(suffixIcon!, end); + } + if (suffix != null) { + end += baselineLayout(suffix!, end); + } + break; + } + case TextDirection.ltr: + { + double start = left + _boxSize(icon).width; + double end = right; + if (prefixIcon != null) { + start -= contentPadding.left; + start += centerLayout(prefixIcon!, start); + } + if (label != null) { + if (decoration.alignLabelWithHint) { + baselineLayout(label!, start); + } else { + centerLayout(label!, start); + } + } + if (prefix != null) { + start += baselineLayout(prefix!, start); + } + if (input != null) { + baselineLayout(input!, start); + } + if (hint != null) { + baselineLayout(hint!, start); + } + if (suffixIcon != null) { + end += contentPadding.right; + end -= centerLayout(suffixIcon!, end - suffixIcon!.size.width); + } + if (suffix != null) { + end -= baselineLayout(suffix!, end - suffix!.size.width); + } + break; + } + } + + if (helperError != null || counter != null) { + height = layout.subtextHeight; + baseline = layout.subtextBaseline; + + switch (textDirection) { + case TextDirection.rtl: + if (helperError != null) { + baselineLayout(helperError!, right - helperError!.size.width - _boxSize(icon).width); + } + if (counter != null) { + baselineLayout(counter!, left); + } + break; + case TextDirection.ltr: + if (helperError != null) { + baselineLayout(helperError!, left + _boxSize(icon).width); + } + if (counter != null) { + baselineLayout(counter!, right - counter!.size.width); + } + break; + } + } + + if (label != null) { + final double labelX = _boxParentData(label!).offset.dx; + // +1 shifts the range of x from (-1.0, 1.0) to (0.0, 2.0). + final double floatAlign = decoration.floatingLabelAlignment._x + 1; + final double floatWidth = _boxSize(label).width * _kFinalLabelScale; + // When floating label is centered, its x is relative to + // _BorderContainer's x and is independent of label's x. + switch (textDirection) { + case TextDirection.rtl: + double offsetToPrefixIcon = 0.0; + if (prefixIcon != null && !decoration.alignLabelWithHint) { + offsetToPrefixIcon = material3 ? _boxSize(prefixIcon).width - left : 0; + } + decoration.borderGap.start = lerpDouble( + labelX + _boxSize(label).width + offsetToPrefixIcon, + _boxSize(container).width / 2.0 + floatWidth / 2.0, + floatAlign, + ); + + break; + case TextDirection.ltr: + // The value of _InputBorderGap.start is relative to the origin of the + // _BorderContainer which is inset by the icon's width. Although, when + // floating label is centered, it's already relative to _BorderContainer. + double offsetToPrefixIcon = 0.0; + if (prefixIcon != null && !decoration.alignLabelWithHint) { + offsetToPrefixIcon = material3 ? (-_boxSize(prefixIcon).width + left) : 0; + } + decoration.borderGap.start = lerpDouble( + labelX - _boxSize(icon).width + offsetToPrefixIcon, + _boxSize(container).width / 2.0 - floatWidth / 2.0, + floatAlign, + ); + break; + } + decoration.borderGap.extent = label!.size.width * _kFinalLabelScale; + } else { + decoration.borderGap.start = null; + decoration.borderGap.extent = 0.0; + } + + size = constraints.constrain(Size(overallWidth, overallHeight)); + assert(size.width == constraints.constrainWidth(overallWidth)); + assert(size.height == constraints.constrainHeight(overallHeight)); + } + + void _paintLabel(PaintingContext context, Offset offset) { + context.paintChild(label!, offset); + } + + @override + void paint(PaintingContext context, Offset offset) { + void doPaint(RenderBox? child) { + if (child != null) { + context.paintChild(child, _boxParentData(child).offset + offset); + } + } + + doPaint(container); + + if (label != null) { + final Offset labelOffset = _boxParentData(label!).offset; + final double labelHeight = _boxSize(label).height; + final double labelWidth = _boxSize(label).width; + // +1 shifts the range of x from (-1.0, 1.0) to (0.0, 2.0). + final double floatAlign = decoration.floatingLabelAlignment._x + 1; + final double floatWidth = labelWidth * _kFinalLabelScale; + final double borderWeight = decoration.border.borderSide.width; + final double t = decoration.floatingLabelProgress; + // The center of the outline border label ends up a little below the + // center of the top border line. + final bool isOutlineBorder = decoration.border.isOutline; + /* final double floatingY = isOutlineBorder ? -labelHeight * 0.25 : contentPadding.top; + final double scale = lerpDouble(1.0, 0.75, t)!; */ + + final double floatingY = + isOutlineBorder ? (-labelHeight * _kFinalLabelScale) / 2.0 + borderWeight / 2.0 : contentPadding.top; + final double scale = lerpDouble(1.0, _kFinalLabelScale, t)!; + final double centeredFloatX = + _boxParentData(container!).offset.dx + _boxSize(container).width / 2.0 - floatWidth / 2.0; + final double startX; + double floatStartX; + switch (textDirection) { + case TextDirection.rtl: // origin is on the right + startX = labelOffset.dx + labelWidth * (1.0 - scale); + floatStartX = startX; + if (prefixIcon != null && !decoration.alignLabelWithHint && isOutlineBorder) { + floatStartX += material3 ? _boxSize(prefixIcon).width - contentPadding.left : 0.0; + } + break; + case TextDirection.ltr: // origin on the left + startX = labelOffset.dx; + floatStartX = startX; + if (prefixIcon != null && !decoration.alignLabelWithHint && isOutlineBorder) { + floatStartX += material3 ? -_boxSize(prefixIcon).width + contentPadding.left : 0.0; + } + break; + } + final double floatEndX = lerpDouble(floatStartX, centeredFloatX, floatAlign)!; + final double dx = lerpDouble(startX, floatEndX, t)!; + final double dy = lerpDouble(0.0, floatingY - labelOffset.dy, t)!; + _labelTransform = Matrix4.identity() + ..translate(dx, labelOffset.dy + dy) + ..scale(scale); + layer = context.pushTransform( + needsCompositing, + offset, + _labelTransform!, + _paintLabel, + oldLayer: layer as TransformLayer?, + ); + } else { + layer = null; + } + + doPaint(icon); + doPaint(prefix); + doPaint(suffix); + doPaint(prefixIcon); + doPaint(suffixIcon); + doPaint(hint); + doPaint(input); + doPaint(helperError); + doPaint(counter); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { + for (final RenderBox child in children) { + // The label must be handled specially since we've transformed it. + final Offset offset = _boxParentData(child).offset; + final bool isHit = result.addWithPaintOffset( + offset: offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - offset); + return child.hitTest(result, position: transformed); + }, + ); + if (isHit) { + return true; + } + } + return false; + } + + @override + void applyPaintTransform(RenderObject child, Matrix4 transform) { + if (child == label && _labelTransform != null) { + final Offset labelOffset = _boxParentData(label!).offset; + transform + ..multiply(_labelTransform!) + ..translate(-labelOffset.dx, -labelOffset.dy); + } + super.applyPaintTransform(child, transform); + } +} + +class _Decorator extends RenderObjectWidget with SlottedMultiChildRenderObjectWidgetMixin<_DecorationSlot> { + const _Decorator({ + required this.textAlignVertical, + required this.decoration, + required this.textDirection, + required this.textBaseline, + required this.isFocused, + required this.expands, + }); + + final _Decoration decoration; + final TextDirection textDirection; + final TextBaseline textBaseline; + final TextAlignVertical? textAlignVertical; + final bool isFocused; + final bool expands; + + @override + Iterable<_DecorationSlot> get slots => _DecorationSlot.values; + + @override + Widget? childForSlot(_DecorationSlot slot) { + switch (slot) { + case _DecorationSlot.icon: + return decoration.icon; + case _DecorationSlot.input: + return decoration.input; + case _DecorationSlot.label: + return decoration.label; + case _DecorationSlot.hint: + return decoration.hint; + case _DecorationSlot.prefix: + return decoration.prefix; + case _DecorationSlot.suffix: + return decoration.suffix; + case _DecorationSlot.prefixIcon: + return decoration.prefixIcon; + case _DecorationSlot.suffixIcon: + return decoration.suffixIcon; + case _DecorationSlot.helperError: + return decoration.helperError; + case _DecorationSlot.counter: + return decoration.counter; + case _DecorationSlot.container: + return decoration.container; + } + } + + @override + _RenderDecoration createRenderObject(BuildContext context) { + return _RenderDecoration( + decoration: decoration, + textDirection: textDirection, + textBaseline: textBaseline, + textAlignVertical: textAlignVertical, + isFocused: isFocused, + expands: expands, + material3: Theme.of(context).useMaterial3, + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderDecoration renderObject) { + renderObject + ..decoration = decoration + ..expands = expands + ..isFocused = isFocused + ..textAlignVertical = textAlignVertical + ..textBaseline = textBaseline + ..textDirection = textDirection; + } +} + +class _AffixText extends StatelessWidget { + const _AffixText({ + required this.labelIsFloating, + this.text, + this.style, + this.child, + }); + + final bool labelIsFloating; + final String? text; + final TextStyle? style; + final Widget? child; + + @override + Widget build(BuildContext context) { + return DefaultTextStyle.merge( + style: style, + child: AnimatedOpacity( + duration: _kTransitionDuration, + curve: _kTransitionCurve, + opacity: labelIsFloating ? 1.0 : 0.0, + child: child ?? (text == null ? null : Text(text!, style: style)), + ), + ); + } +} + +/// Defines the appearance of a Material Design text field. +/// +/// [InputDecorator] displays the visual elements of a Material Design text +/// field around its input [child]. The visual elements themselves are defined +/// by an [InputDecoration] object and their layout and appearance depend +/// on the `baseStyle`, `textAlign`, `isFocused`, and `isEmpty` parameters. +/// +/// [TextField] uses this widget to decorate its [EditableText] child. +/// +/// [InputDecorator] can be used to create widgets that look and behave like a +/// [TextField] but support other kinds of input. +/// +/// Requires one of its ancestors to be a [Material] widget. The [child] widget, +/// as well as the decorative widgets specified in [decoration], must have +/// non-negative baselines. +/// +/// See also: +/// +/// * [TextField], which uses an [InputDecorator] to display a border, +/// labels, and icons, around its [EditableText] child. +/// * [Decoration] and [DecoratedBox], for drawing arbitrary decorations +/// around other widgets. +class InputDecorator extends StatefulWidget { + /// Creates a widget that displays a border, labels, and icons, + /// for a [TextField]. + /// + /// The [isFocused], [isHovering], [expands], and [isEmpty] arguments must not + /// be null. + const InputDecorator({ + super.key, + required this.decoration, + this.baseStyle, + this.textAlign, + this.textAlignVertical, + this.isFocused = false, + this.isHovering = false, + this.expands = false, + this.isEmpty = false, + this.child, + }); + + /// The text and styles to use when decorating the child. + /// + /// Null [InputDecoration] properties are initialized with the corresponding + /// values from [ThemeData.inputDecorationTheme]. + /// + /// Must not be null. + final InputDecoration decoration; + + /// The style on which to base the label, hint, counter, and error styles + /// if the [decoration] does not provide explicit styles. + /// + /// If null, [baseStyle] defaults to the `titleMedium` style from the + /// current [Theme], see [ThemeData.textTheme]. + /// + /// The [TextStyle.textBaseline] of the [baseStyle] is used to determine + /// the baseline used for text alignment. + final TextStyle? baseStyle; + + /// How the text in the decoration should be aligned horizontally. + final TextAlign? textAlign; + + /// {@template flutter.material.InputDecorator.textAlignVertical} + /// How the text should be aligned vertically. + /// + /// Determines the alignment of the baseline within the available space of + /// the input (typically a TextField). For example, TextAlignVertical.top will + /// place the baseline such that the text, and any attached decoration like + /// prefix and suffix, is as close to the top of the input as possible without + /// overflowing. The heights of the prefix and suffix are similarly included + /// for other alignment values. If the height is greater than the height + /// available, then the prefix and suffix will be allowed to overflow first + /// before the text scrolls. + /// {@endtemplate} + final TextAlignVertical? textAlignVertical; + + /// Whether the input field has focus. + /// + /// Determines the position of the label text and the color and weight of the + /// border. + /// + /// Defaults to false. + /// + /// See also: + /// + /// * [InputDecoration.hoverColor], which is also blended into the focus + /// color and fill color when the [isHovering] is true to produce the final + /// color. + final bool isFocused; + + /// Whether the input field is being hovered over by a mouse pointer. + /// + /// Determines the container fill color, which is a blend of + /// [InputDecoration.hoverColor] with [InputDecoration.fillColor] when + /// true, and [InputDecoration.fillColor] when not. + /// + /// Defaults to false. + final bool isHovering; + + /// If true, the height of the input field will be as large as possible. + /// + /// If wrapped in a widget that constrains its child's height, like Expanded + /// or SizedBox, the input field will only be affected if [expands] is set to + /// true. + /// + /// See [TextField.minLines] and [TextField.maxLines] for related ways to + /// affect the height of an input. When [expands] is true, both must be null + /// in order to avoid ambiguity in determining the height. + /// + /// Defaults to false. + final bool expands; + + /// Whether the input field is empty. + /// + /// Determines the position of the label text and whether to display the hint + /// text. + /// + /// Defaults to false. + final bool isEmpty; + + /// The widget below this widget in the tree. + /// + /// Typically an [EditableText], [DropdownButton], or [InkWell]. + final Widget? child; + + /// Whether the label needs to get out of the way of the input, either by + /// floating or disappearing. + /// + /// Will withdraw when not empty, or when focused while enabled. + bool get _labelShouldWithdraw => !isEmpty || (isFocused && decoration.enabled); + + @override + State createState() => _InputDecoratorState(); + + /// The RenderBox that defines this decorator's "container". That's the + /// area which is filled if [InputDecoration.filled] is true. It's the area + /// adjacent to [InputDecoration.icon] and above the widgets that contain + /// [InputDecoration.helperText], [InputDecoration.errorText], and + /// [InputDecoration.counterText]. + /// + /// [TextField] renders ink splashes within the container. + static RenderBox? containerOf(BuildContext context) { + final _RenderDecoration? result = context.findAncestorRenderObjectOfType<_RenderDecoration>(); + return result?.container; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('decoration', decoration)); + properties.add(DiagnosticsProperty('baseStyle', baseStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('isFocused', isFocused)); + properties.add(DiagnosticsProperty('expands', expands, defaultValue: false)); + properties.add(DiagnosticsProperty('isEmpty', isEmpty)); + } +} + +class _InputDecoratorState extends State with TickerProviderStateMixin { + late AnimationController _floatingLabelController; + late AnimationController _shakingLabelController; + final _InputBorderGap _borderGap = _InputBorderGap(); + + @override + void initState() { + super.initState(); + + final bool labelIsInitiallyFloating = widget.decoration.floatingLabelBehavior == FloatingLabelBehavior.always || + (widget.decoration.floatingLabelBehavior != FloatingLabelBehavior.never && widget._labelShouldWithdraw); + + _floatingLabelController = AnimationController( + duration: _kTransitionDuration, + vsync: this, + value: labelIsInitiallyFloating ? 1.0 : 0.0, + ); + _floatingLabelController.addListener(_handleChange); + + _shakingLabelController = AnimationController( + duration: _kTransitionDuration, + vsync: this, + ); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _effectiveDecoration = null; + } + + @override + void dispose() { + _floatingLabelController.dispose(); + _shakingLabelController.dispose(); + super.dispose(); + } + + void _handleChange() { + setState(() { + // The _floatingLabelController's value has changed. + }); + } + + InputDecoration? _effectiveDecoration; + InputDecoration get decoration => + _effectiveDecoration ??= widget.decoration.applyDefaults(Theme.of(context).inputDecorationTheme); + + TextAlign? get textAlign => widget.textAlign; + bool get isFocused => widget.isFocused; + bool get isHovering => widget.isHovering && decoration.enabled; + bool get isEmpty => widget.isEmpty; + bool get _floatingLabelEnabled { + return decoration.floatingLabelBehavior != FloatingLabelBehavior.never; + } + + @override + void didUpdateWidget(InputDecorator old) { + super.didUpdateWidget(old); + if (widget.decoration != old.decoration) { + _effectiveDecoration = null; + } + + final bool floatBehaviorChanged = widget.decoration.floatingLabelBehavior != old.decoration.floatingLabelBehavior; + + if (widget._labelShouldWithdraw != old._labelShouldWithdraw || floatBehaviorChanged) { + if (_floatingLabelEnabled && + (widget._labelShouldWithdraw || widget.decoration.floatingLabelBehavior == FloatingLabelBehavior.always)) { + _floatingLabelController.forward(); + } else { + _floatingLabelController.reverse(); + } + } + + final String? errorText = decoration.errorText; + final String? oldErrorText = old.decoration.errorText; + + if (_floatingLabelController.isCompleted && errorText != null && errorText != oldErrorText) { + _shakingLabelController + ..value = 0.0 + ..forward(); + } + } + + Color _getDefaultM2BorderColor(ThemeData themeData) { + if (!decoration.enabled && !isFocused) { + return ((decoration.filled ?? false) && !(decoration.border?.isOutline ?? false)) + ? Colors.transparent + : themeData.disabledColor; + } + if (decoration.errorText != null) { + return themeData.colorScheme.error; + } + if (isFocused) { + return themeData.colorScheme.primary; + } + if (decoration.filled!) { + return themeData.hintColor; + } + final Color enabledColor = themeData.colorScheme.onSurface.withOpacity(0.38); + if (isHovering) { + final Color hoverColor = + decoration.hoverColor ?? themeData.inputDecorationTheme.hoverColor ?? themeData.hoverColor; + return Color.alphaBlend(hoverColor.withOpacity(0.12), enabledColor); + } + return enabledColor; + } + + Color _getFillColor(ThemeData themeData, InputDecorationTheme defaults) { + if (decoration.filled != true) { + // filled == null same as filled == false + return Colors.transparent; + } + if (decoration.fillColor != null) { + return MaterialStateProperty.resolveAs(decoration.fillColor!, materialState); + } + return MaterialStateProperty.resolveAs(defaults.fillColor!, materialState); + } + + Color _getHoverColor(ThemeData themeData) { + if (decoration.filled == null || !decoration.filled! || isFocused || !decoration.enabled) { + return Colors.transparent; + } + return decoration.hoverColor ?? themeData.inputDecorationTheme.hoverColor ?? themeData.hoverColor; + } + + Color _getIconColor(ThemeData themeData, InputDecorationTheme defaults) { + return MaterialStateProperty.resolveAs(decoration.iconColor, materialState) ?? + MaterialStateProperty.resolveAs(themeData.inputDecorationTheme.iconColor, materialState) ?? + MaterialStateProperty.resolveAs(defaults.iconColor!, materialState); + } + + Color _getPrefixIconColor(ThemeData themeData, InputDecorationTheme defaults) { + return MaterialStateProperty.resolveAs(decoration.prefixIconColor, materialState) ?? + MaterialStateProperty.resolveAs(themeData.inputDecorationTheme.prefixIconColor, materialState) ?? + MaterialStateProperty.resolveAs(defaults.prefixIconColor!, materialState); + } + + Color _getSuffixIconColor(ThemeData themeData, InputDecorationTheme defaults) { + return MaterialStateProperty.resolveAs(decoration.suffixIconColor, materialState) ?? + MaterialStateProperty.resolveAs(themeData.inputDecorationTheme.suffixIconColor, materialState) ?? + MaterialStateProperty.resolveAs(defaults.suffixIconColor!, materialState); + } + + // True if the label will be shown and the hint will not. + // If we're not focused, there's no value, labelText was provided, and + // floatingLabelBehavior isn't set to always, then the label appears where the + // hint would. + bool get _hasInlineLabel { + return !widget._labelShouldWithdraw && + (decoration.labelText != null || decoration.label != null) && + decoration.floatingLabelBehavior != FloatingLabelBehavior.always; + } + + // If the label is a floating placeholder, it's always shown. + bool get _shouldShowLabel => _hasInlineLabel || _floatingLabelEnabled; + + // The base style for the inline label when they're displayed "inline", + // i.e. when they appear in place of the empty text field. + TextStyle _getInlineLabelStyle(ThemeData themeData, InputDecorationTheme defaults) { + final TextStyle defaultStyle = MaterialStateProperty.resolveAs(defaults.labelStyle!, materialState); + + final TextStyle? style = MaterialStateProperty.resolveAs(decoration.labelStyle, materialState) ?? + MaterialStateProperty.resolveAs(themeData.inputDecorationTheme.labelStyle, materialState); + + return themeData.textTheme.titleMedium!.merge(widget.baseStyle).merge(defaultStyle).merge(style); + } + + // The base style for the inline hint when they're displayed "inline", + // i.e. when they appear in place of the empty text field. + TextStyle _getInlineHintStyle(ThemeData themeData, InputDecorationTheme defaults) { + final TextStyle defaultStyle = MaterialStateProperty.resolveAs(defaults.hintStyle!, materialState); + + final TextStyle? style = MaterialStateProperty.resolveAs(decoration.hintStyle, materialState) ?? + MaterialStateProperty.resolveAs(themeData.inputDecorationTheme.hintStyle, materialState); + + return themeData.textTheme.titleMedium!.merge(widget.baseStyle).merge(defaultStyle).merge(style); + } + + TextStyle _getFloatingLabelStyle(ThemeData themeData, InputDecorationTheme defaults) { + TextStyle defaultTextStyle = MaterialStateProperty.resolveAs(defaults.floatingLabelStyle!, materialState); + if (decoration.errorText != null && decoration.errorStyle?.color != null) { + defaultTextStyle = defaultTextStyle.copyWith(color: decoration.errorStyle?.color); + } + defaultTextStyle = defaultTextStyle.merge(decoration.floatingLabelStyle ?? decoration.labelStyle); + + final TextStyle? style = MaterialStateProperty.resolveAs(decoration.floatingLabelStyle, materialState) ?? + MaterialStateProperty.resolveAs(themeData.inputDecorationTheme.floatingLabelStyle, materialState); + + return themeData.textTheme.titleMedium! + .merge(widget.baseStyle) + .merge(defaultTextStyle) + .merge(style) + .copyWith(height: 1); + } + + TextStyle _getHelperStyle(ThemeData themeData, InputDecorationTheme defaults) { + return MaterialStateProperty.resolveAs(defaults.helperStyle!, materialState) + .merge(MaterialStateProperty.resolveAs(decoration.helperStyle, materialState)); + } + + TextStyle _getErrorStyle(ThemeData themeData, InputDecorationTheme defaults) { + return MaterialStateProperty.resolveAs(defaults.errorStyle!, materialState).merge(decoration.errorStyle); + } + + Set get materialState { + return { + if (!decoration.enabled) MaterialState.disabled, + if (isFocused) MaterialState.focused, + if (isHovering) MaterialState.hovered, + if (decoration.errorText != null) MaterialState.error, + }; + } + + InputBorder _getDefaultBorder(ThemeData themeData, InputDecorationTheme defaults) { + final InputBorder border = + MaterialStateProperty.resolveAs(decoration.border, materialState) ?? const UnderlineInputBorder(); + + if (decoration.border is MaterialStateProperty) { + return border; + } + + if (border.borderSide == BorderSide.none) { + return border; + } + + if (themeData.useMaterial3) { + if (decoration.filled!) { + return border.copyWith( + borderSide: MaterialStateProperty.resolveAs(defaults.activeIndicatorBorder, materialState), + ); + } else { + return border.copyWith( + borderSide: MaterialStateProperty.resolveAs(defaults.outlineBorder, materialState), + ); + } + } else { + return border.copyWith( + borderSide: BorderSide( + color: _getDefaultM2BorderColor(themeData), + width: (decoration.isCollapsed || decoration.border == InputBorder.none || !decoration.enabled) + ? 0.0 + : isFocused + ? 2.0 + : 1.0, + ), + ); + } + } + + @override + Widget build(BuildContext context) { + final ThemeData themeData = Theme.of(context); + final InputDecorationTheme defaults = + Theme.of(context).useMaterial3 ? _InputDecoratorDefaultsM3(context) : _InputDecoratorDefaultsM2(context); + + final TextStyle labelStyle = _getInlineLabelStyle(themeData, defaults); + final TextBaseline textBaseline = labelStyle.textBaseline!; + + final TextStyle hintStyle = _getInlineHintStyle(themeData, defaults); + final String? hintText = decoration.hintText; + final Widget? hint = hintText == null + ? null + : AnimatedOpacity( + opacity: (isEmpty && !_hasInlineLabel) ? 1.0 : 0.0, + duration: _kTransitionDuration, + curve: _kTransitionCurve, + alwaysIncludeSemantics: isEmpty || (decoration.labelText == null && decoration.label == null), + child: Text( + hintText, + style: hintStyle, + textDirection: decoration.hintTextDirection, + overflow: hintStyle.overflow ?? TextOverflow.ellipsis, + textAlign: textAlign, + maxLines: decoration.hintMaxLines, + ), + ); + + final bool isError = decoration.errorText != null; + InputBorder? border; + if (!decoration.enabled) { + border = isError ? decoration.errorBorder : decoration.disabledBorder; + } else if (isFocused) { + border = isError ? decoration.focusedErrorBorder : decoration.focusedBorder; + } else { + border = isError ? decoration.errorBorder : decoration.enabledBorder; + } + border ??= _getDefaultBorder(themeData, defaults); + + final Widget container = _BorderContainer( + border: border, + gap: _borderGap, + gapAnimation: _floatingLabelController.view, + fillColor: _getFillColor(themeData, defaults), + hoverColor: _getHoverColor(themeData), + isHovering: isHovering, + ); + + final Widget? label = decoration.labelText == null && decoration.label == null + ? null + : _Shaker( + animation: _shakingLabelController.view, + child: AnimatedOpacity( + duration: _kTransitionDuration, + curve: _kTransitionCurve, + opacity: _shouldShowLabel ? 1.0 : 0.0, + child: AnimatedDefaultTextStyle( + duration: _kTransitionDuration, + curve: _kTransitionCurve, + style: widget._labelShouldWithdraw ? _getFloatingLabelStyle(themeData, defaults) : labelStyle, + child: decoration.label ?? + Text( + decoration.labelText!, + overflow: TextOverflow.ellipsis, + textAlign: textAlign, + ), + ), + ), + ); + + final Widget? prefix = decoration.prefix == null && decoration.prefixText == null + ? null + : _AffixText( + labelIsFloating: widget._labelShouldWithdraw, + text: decoration.prefixText, + style: MaterialStateProperty.resolveAs(decoration.prefixStyle, materialState) ?? hintStyle, + child: decoration.prefix, + ); + + final Widget? suffix = decoration.suffix == null && decoration.suffixText == null + ? null + : _AffixText( + labelIsFloating: widget._labelShouldWithdraw, + text: decoration.suffixText, + style: MaterialStateProperty.resolveAs(decoration.suffixStyle, materialState) ?? hintStyle, + child: decoration.suffix, + ); + + final bool decorationIsDense = decoration.isDense ?? false; + final double iconSize = decorationIsDense ? 18.0 : 24.0; + + final Widget? icon = decoration.icon == null + ? null + : MouseRegion( + cursor: SystemMouseCursors.basic, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 16.0), + child: IconTheme.merge( + data: IconThemeData( + color: _getIconColor(themeData, defaults), + size: iconSize, + ), + child: decoration.icon!, + ), + ), + ); + + final Widget? prefixIcon = decoration.prefixIcon == null + ? null + : Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: MouseRegion( + cursor: SystemMouseCursors.basic, + child: ConstrainedBox( + constraints: decoration.prefixIconConstraints ?? + themeData.visualDensity.effectiveConstraints( + const BoxConstraints( + minWidth: kMinInteractiveDimension, + minHeight: kMinInteractiveDimension, + ), + ), + child: IconTheme.merge( + data: IconThemeData( + color: _getPrefixIconColor(themeData, defaults), + size: iconSize, + ), + child: decoration.prefixIcon!, + ), + ), + ), + ); + + final Widget? suffixIcon = decoration.suffixIcon == null + ? null + : Center( + widthFactor: 1.0, + heightFactor: 1.0, + child: MouseRegion( + cursor: SystemMouseCursors.basic, + child: ConstrainedBox( + constraints: decoration.suffixIconConstraints ?? + themeData.visualDensity.effectiveConstraints( + const BoxConstraints( + minWidth: kMinInteractiveDimension, + minHeight: kMinInteractiveDimension, + ), + ), + child: IconTheme.merge( + data: IconThemeData( + color: _getSuffixIconColor(themeData, defaults), + size: iconSize, + ), + child: decoration.suffixIcon!, + ), + ), + ), + ); + + final Widget helperError = _HelperError( + textAlign: textAlign, + helperText: decoration.helperText, + helperStyle: _getHelperStyle(themeData, defaults), + helperMaxLines: decoration.helperMaxLines, + errorText: decoration.errorText, + errorStyle: _getErrorStyle(themeData, defaults), + errorMaxLines: decoration.errorMaxLines, + ); + + Widget? counter; + if (decoration.counter != null) { + counter = decoration.counter; + } else if (decoration.counterText != null && decoration.counterText != '') { + counter = Semantics( + container: true, + liveRegion: isFocused, + child: Text( + decoration.counterText!, + style: _getHelperStyle(themeData, defaults) + .merge(MaterialStateProperty.resolveAs(decoration.counterStyle, materialState)), + overflow: TextOverflow.ellipsis, + semanticsLabel: decoration.semanticCounterText, + ), + ); + } + + // The _Decoration widget and _RenderDecoration assume that contentPadding + // has been resolved to EdgeInsets. + final TextDirection textDirection = Directionality.of(context); + final EdgeInsets? decorationContentPadding = decoration.contentPadding?.resolve(textDirection); + + final EdgeInsets contentPadding; + final double floatingLabelHeight; + if (decoration.isCollapsed) { + floatingLabelHeight = 0.0; + contentPadding = decorationContentPadding ?? EdgeInsets.zero; + } else if (!border.isOutline) { + // 4.0: the vertical gap between the inline elements and the floating label. + floatingLabelHeight = (4.0 + 0.75 * labelStyle.fontSize!) * MediaQuery.textScaleFactorOf(context); + if (decoration.filled ?? false) { + contentPadding = decorationContentPadding ?? + (decorationIsDense + ? const EdgeInsets.fromLTRB(12.0, 8.0, 12.0, 8.0) + : const EdgeInsets.fromLTRB(12.0, 12.0, 12.0, 12.0)); + } else { + // Not left or right padding for underline borders that aren't filled + // is a small concession to backwards compatibility. This eliminates + // the most noticeable layout change introduced by #13734. + contentPadding = decorationContentPadding ?? + (decorationIsDense + ? const EdgeInsets.fromLTRB(0.0, 8.0, 0.0, 8.0) + : const EdgeInsets.fromLTRB(0.0, 12.0, 0.0, 12.0)); + } + } else { + floatingLabelHeight = 0.0; + contentPadding = decorationContentPadding ?? + (decorationIsDense + ? const EdgeInsets.fromLTRB(12.0, 20.0, 12.0, 12.0) + : const EdgeInsets.fromLTRB(12.0, 24.0, 12.0, 16.0)); + } + + final _Decorator decorator = _Decorator( + decoration: _Decoration( + contentPadding: contentPadding, + isCollapsed: decoration.isCollapsed, + floatingLabelHeight: floatingLabelHeight, + floatingLabelAlignment: decoration.floatingLabelAlignment!, + floatingLabelProgress: _floatingLabelController.value, + border: border, + borderGap: _borderGap, + alignLabelWithHint: decoration.alignLabelWithHint ?? false, + isDense: decoration.isDense, + visualDensity: themeData.visualDensity, + icon: icon, + input: widget.child, + label: label, + hint: hint, + prefix: prefix, + suffix: suffix, + prefixIcon: prefixIcon, + suffixIcon: suffixIcon, + helperError: helperError, + counter: counter, + container: container, + ), + textDirection: textDirection, + textBaseline: textBaseline, + textAlignVertical: widget.textAlignVertical, + isFocused: isFocused, + expands: widget.expands, + ); + + final BoxConstraints? constraints = decoration.constraints ?? themeData.inputDecorationTheme.constraints; + if (constraints != null) { + return ConstrainedBox( + constraints: constraints, + child: decorator, + ); + } + return decorator; + } +} + +/// The border, labels, icons, and styles used to decorate a Material +/// Design text field. +/// +/// The [TextField] and [InputDecorator] classes use [InputDecoration] objects +/// to describe their decoration. (In fact, this class is merely the +/// configuration of an [InputDecorator], which does all the heavy lifting.) +/// +/// {@tool dartpad} +/// This sample shows how to style a `TextField` using an `InputDecorator`. The +/// TextField displays a "send message" icon to the left of the input area, +/// which is surrounded by a border an all sides. It displays the `hintText` +/// inside the input area to help the user understand what input is required. It +/// displays the `helperText` and `counterText` below the input area. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration.png) +/// +/// ** See code in examples/api/lib/material/input_decorator/input_decoration.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This sample shows how to style a "collapsed" `TextField` using an +/// `InputDecorator`. The collapsed `TextField` surrounds the hint text and +/// input area with a border, but does not add padding around them. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration_collapsed.png) +/// +/// ** See code in examples/api/lib/material/input_decorator/input_decoration.1.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This sample shows how to create a `TextField` with hint text, a red border +/// on all sides, and an error message. To display a red border and error +/// message, provide `errorText` to the [InputDecoration] constructor. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration_error.png) +/// +/// ** See code in examples/api/lib/material/input_decorator/input_decoration.2.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This sample shows how to style a `TextField` with a round border and +/// additional text before and after the input area. It displays "Prefix" before +/// the input area, and "Suffix" after the input area. +/// +/// ![](https://flutter.github.io/assets-for-api-docs/assets/material/input_decoration_prefix_suffix.png) +/// +/// ** See code in examples/api/lib/material/input_decorator/input_decoration.3.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This sample shows how to style a `TextField` with a prefixIcon that changes color +/// based on the `MaterialState`. The color defaults to gray, be blue while focused +/// and red if in an error state. +/// +/// ** See code in examples/api/lib/material/input_decorator/input_decoration.material_state.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This sample shows how to style a `TextField` with a prefixIcon that changes color +/// based on the `MaterialState` through the use of `ThemeData`. The color defaults +/// to gray, be blue while focused and red if in an error state. +/// +/// ** See code in examples/api/lib/material/input_decorator/input_decoration.material_state.1.dart ** +/// {@end-tool} +/// +/// See also: +/// +/// * [TextField], which is a text input widget that uses an +/// [InputDecoration]. +/// * [InputDecorator], which is a widget that draws an [InputDecoration] +/// around an input child widget. +/// * [Decoration] and [DecoratedBox], for drawing borders and backgrounds +/// around a child widget. +@immutable +class InputDecoration { + /// Creates a bundle of the border, labels, icons, and styles used to + /// decorate a Material Design text field. + /// + /// Unless specified by [ThemeData.inputDecorationTheme], [InputDecorator] + /// defaults [isDense] to false and [filled] to false. The default border is + /// an instance of [UnderlineInputBorder]. If [border] is [InputBorder.none] + /// then no border is drawn. + /// + /// The [enabled] argument must not be null. + /// + /// Only one of [prefix] and [prefixText] can be specified. + /// + /// Similarly, only one of [suffix] and [suffixText] can be specified. + const InputDecoration({ + this.icon, + this.iconColor, + this.label, + this.labelText, + this.labelStyle, + this.floatingLabelStyle, + this.helperText, + this.helperStyle, + this.helperMaxLines, + this.hintText, + this.hintStyle, + this.hintTextDirection, + this.hintMaxLines, + this.errorText, + this.errorStyle, + this.errorMaxLines, + this.floatingLabelBehavior, + this.floatingLabelAlignment, + this.isCollapsed = false, + this.isDense, + this.contentPadding, + this.prefixIcon, + this.prefixIconConstraints, + this.prefix, + this.prefixText, + this.prefixStyle, + this.prefixIconColor, + this.suffixIcon, + this.suffix, + this.suffixText, + this.suffixStyle, + this.suffixIconColor, + this.suffixIconConstraints, + this.counter, + this.counterText, + this.counterStyle, + this.filled, + this.fillColor, + this.focusColor, + this.hoverColor, + this.errorBorder, + this.focusedBorder, + this.focusedErrorBorder, + this.disabledBorder, + this.enabledBorder, + this.border, + this.enabled = true, + this.semanticCounterText, + this.alignLabelWithHint, + this.constraints, + }) : assert(!(label != null && labelText != null), 'Declaring both label and labelText is not supported.'), + assert(!(prefix != null && prefixText != null), 'Declaring both prefix and prefixText is not supported.'), + assert(!(suffix != null && suffixText != null), 'Declaring both suffix and suffixText is not supported.'); + + /// Defines an [InputDecorator] that is the same size as the input field. + /// + /// This type of input decoration does not include a border by default. + /// + /// Sets the [isCollapsed] property to true. + const InputDecoration.collapsed({ + required this.hintText, + this.floatingLabelBehavior, + this.floatingLabelAlignment, + this.hintStyle, + this.hintTextDirection, + this.filled = false, + this.fillColor, + this.focusColor, + this.hoverColor, + this.border = InputBorder.none, + this.enabled = true, + }) : icon = null, + iconColor = null, + label = null, + labelText = null, + labelStyle = null, + floatingLabelStyle = null, + helperText = null, + helperStyle = null, + helperMaxLines = null, + hintMaxLines = null, + errorText = null, + errorStyle = null, + errorMaxLines = null, + isDense = false, + contentPadding = EdgeInsets.zero, + isCollapsed = true, + prefixIcon = null, + prefix = null, + prefixText = null, + prefixStyle = null, + prefixIconColor = null, + prefixIconConstraints = null, + suffix = null, + suffixIcon = null, + suffixText = null, + suffixStyle = null, + suffixIconColor = null, + suffixIconConstraints = null, + counter = null, + counterText = null, + counterStyle = null, + errorBorder = null, + focusedBorder = null, + focusedErrorBorder = null, + disabledBorder = null, + enabledBorder = null, + semanticCounterText = null, + alignLabelWithHint = false, + constraints = null; + + /// An icon to show before the input field and outside of the decoration's + /// container. + /// + /// The size and color of the icon is configured automatically using an + /// [IconTheme] and therefore does not need to be explicitly given in the + /// icon widget. + /// + /// The trailing edge of the icon is padded by 16dps. + /// + /// The decoration's container is the area which is filled if [filled] is + /// true and bordered per the [border]. It's the area adjacent to + /// [icon] and above the widgets that contain [helperText], + /// [errorText], and [counterText]. + /// + /// See [Icon], [ImageIcon]. + final Widget? icon; + + /// The color of the [icon]. + /// + /// If [iconColor] is a [MaterialStateColor], then the effective + /// color can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + final Color? iconColor; + + /// Optional widget that describes the input field. + /// + /// {@template flutter.material.inputDecoration.label} + /// When the input field is empty and unfocused, the label is displayed on + /// top of the input field (i.e., at the same location on the screen where + /// text may be entered in the input field). When the input field receives + /// focus (or if the field is non-empty), depending on [floatingLabelAlignment], + /// the label moves above, either vertically adjacent to, or to the center of + /// the input field. + /// {@endtemplate} + /// + /// This can be used, for example, to add multiple [TextStyle]'s to a label that would + /// otherwise be specified using [labelText], which only takes one [TextStyle]. + /// + /// {@tool dartpad} + /// This example shows a `TextField` with a [Text.rich] widget as the [label]. + /// The widget contains multiple [Text] widgets with different [TextStyle]'s. + /// + /// ** See code in examples/api/lib/material/input_decorator/input_decoration.label.0.dart ** + /// {@end-tool} + /// + /// Only one of [label] and [labelText] can be specified. + final Widget? label; + + /// Optional text that describes the input field. + /// + /// {@macro flutter.material.inputDecoration.label} + /// + /// If a more elaborate label is required, consider using [label] instead. + /// Only one of [label] and [labelText] can be specified. + final String? labelText; + + /// {@template flutter.material.inputDecoration.labelStyle} + /// The style to use for [InputDecoration.labelText] when the label is on top + /// of the input field. + /// + /// If [labelStyle] is a [MaterialStateTextStyle], then the effective + /// text style can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + /// + /// When the [InputDecoration.labelText] is above (i.e., vertically adjacent to) + /// the input field, the text uses the [floatingLabelStyle] instead. + /// + /// If null, defaults to a value derived from the base [TextStyle] for the + /// input field and the current [Theme]. + /// + /// Note that if you specify this style it will override the default behavior + /// of [InputDecoration] that changes the color of the label to the + /// [InputDecoration.errorStyle] color or [ColorScheme.error]. + /// + /// {@tool dartpad} + /// It's possible to override the label style for just the error state, or + /// just the default state, or both. + /// + /// In this example the [labelStyle] is specified with a [MaterialStateProperty] + /// which resolves to a text style whose color depends on the decorator's + /// error state. + /// + /// ** See code in examples/api/lib/material/input_decorator/input_decoration.label_style_error.0.dart ** + /// {@end-tool} + /// {@endtemplate} + final TextStyle? labelStyle; + + /// {@template flutter.material.inputDecoration.floatingLabelStyle} + /// The style to use for [InputDecoration.labelText] when the label is + /// above (i.e., vertically adjacent to) the input field. + /// + /// When the [InputDecoration.labelText] is on top of the input field, the + /// text uses the [labelStyle] instead. + /// + /// If [floatingLabelStyle] is a [MaterialStateTextStyle], then the effective + /// text style can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + /// + /// If null, defaults to [labelStyle]. + /// + /// Note that if you specify this style it will override the default behavior + /// of [InputDecoration] that changes the color of the label to the + /// [InputDecoration.errorStyle] color or [ColorScheme.error]. + /// + /// {@tool dartpad} + /// It's possible to override the label style for just the error state, or + /// just the default state, or both. + /// + /// In this example the [floatingLabelStyle] is specified with a + /// [MaterialStateProperty] which resolves to a text style whose color depends + /// on the decorator's error state. + /// + /// ** See code in examples/api/lib/material/input_decorator/input_decoration.floating_label_style_error.0.dart ** + /// {@end-tool} + /// {@endtemplate} + final TextStyle? floatingLabelStyle; + + /// Text that provides context about the [InputDecorator.child]'s value, such + /// as how the value will be used. + /// + /// If non-null, the text is displayed below the [InputDecorator.child], in + /// the same location as [errorText]. If a non-null [errorText] value is + /// specified then the helper text is not shown. + final String? helperText; + + /// The style to use for the [helperText]. + /// + /// If [helperStyle] is a [MaterialStateTextStyle], then the effective + /// text style can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + final TextStyle? helperStyle; + + /// The maximum number of lines the [helperText] can occupy. + /// + /// Defaults to null, which means that the [helperText] will be limited + /// to a single line with [TextOverflow.ellipsis]. + /// + /// This value is passed along to the [Text.maxLines] attribute + /// of the [Text] widget used to display the helper. + /// + /// See also: + /// + /// * [errorMaxLines], the equivalent but for the [errorText]. + final int? helperMaxLines; + + /// Text that suggests what sort of input the field accepts. + /// + /// Displayed on top of the [InputDecorator.child] (i.e., at the same location + /// on the screen where text may be entered in the [InputDecorator.child]) + /// when the input [isEmpty] and either (a) [labelText] is null or (b) the + /// input has the focus. + final String? hintText; + + /// The style to use for the [hintText]. + /// + /// If [hintStyle] is a [MaterialStateTextStyle], then the effective + /// text style can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + /// + /// Also used for the [labelText] when the [labelText] is displayed on + /// top of the input field (i.e., at the same location on the screen where + /// text may be entered in the [InputDecorator.child]). + /// + /// If null, defaults to a value derived from the base [TextStyle] for the + /// input field and the current [Theme]. + final TextStyle? hintStyle; + + /// The direction to use for the [hintText]. + /// + /// If null, defaults to a value derived from [Directionality] for the + /// input field and the current context. + final TextDirection? hintTextDirection; + + /// The maximum number of lines the [hintText] can occupy. + /// + /// Defaults to the value of [TextField.maxLines] attribute. + /// + /// This value is passed along to the [Text.maxLines] attribute + /// of the [Text] widget used to display the hint text. [TextOverflow.ellipsis] is + /// used to handle the overflow when it is limited to single line. + final int? hintMaxLines; + + /// Text that appears below the [InputDecorator.child] and the border. + /// + /// If non-null, the border's color animates to red and the [helperText] is + /// not shown. + /// + /// In a [TextFormField], this is overridden by the value returned from + /// [TextFormField.validator], if that is not null. + final String? errorText; + + /// {@template flutter.material.inputDecoration.errorStyle} + /// The style to use for the [InputDecoration.errorText]. + /// + /// If null, defaults of a value derived from the base [TextStyle] for the + /// input field and the current [Theme]. + /// + /// By default the color of style will be used by the label of + /// [InputDecoration] if [InputDecoration.errorText] is not null. See + /// [InputDecoration.labelStyle] or [InputDecoration.floatingLabelStyle] for + /// an example of how to replicate this behavior if you have specified either + /// style. + /// {@endtemplate} + final TextStyle? errorStyle; + + /// The maximum number of lines the [errorText] can occupy. + /// + /// Defaults to null, which means that the [errorText] will be limited + /// to a single line with [TextOverflow.ellipsis]. + /// + /// This value is passed along to the [Text.maxLines] attribute + /// of the [Text] widget used to display the error. + /// + /// See also: + /// + /// * [helperMaxLines], the equivalent but for the [helperText]. + final int? errorMaxLines; + + /// {@template flutter.material.inputDecoration.floatingLabelBehavior} + /// Defines **how** the floating label should behave. + /// + /// When [FloatingLabelBehavior.auto] the label will float to the top only when + /// the field is focused or has some text content, otherwise it will appear + /// in the field in place of the content. + /// + /// When [FloatingLabelBehavior.always] the label will always float at the top + /// of the field above the content. + /// + /// When [FloatingLabelBehavior.never] the label will always appear in an empty + /// field in place of the content. + /// {@endtemplate} + /// + /// If null, [InputDecorationTheme.floatingLabelBehavior] will be used. + /// + /// See also: + /// + /// * [floatingLabelAlignment] which defines **where** the floating label + /// should be displayed. + final FloatingLabelBehavior? floatingLabelBehavior; + + /// {@template flutter.material.inputDecoration.floatingLabelAlignment} + /// Defines **where** the floating label should be displayed. + /// + /// [FloatingLabelAlignment.start] aligns the floating label to the leftmost + /// (when [TextDirection.ltr]) or rightmost (when [TextDirection.rtl]), + /// possible position, which is vertically adjacent to the label, on top of + /// the field. + /// + /// [FloatingLabelAlignment.center] aligns the floating label to the center on + /// top of the field. + /// {@endtemplate} + /// + /// If null, [InputDecorationTheme.floatingLabelAlignment] will be used. + /// + /// See also: + /// + /// * [floatingLabelBehavior] which defines **how** the floating label should + /// behave. + final FloatingLabelAlignment? floatingLabelAlignment; + + /// Whether the [InputDecorator.child] is part of a dense form (i.e., uses less vertical + /// space). + /// + /// Defaults to false. + final bool? isDense; + + /// The padding for the input decoration's container. + /// + /// {@macro flutter.material.input_decorator.container_description} + /// + /// By default the [contentPadding] reflects [isDense] and the type of the + /// [border]. + /// + /// If [isCollapsed] is true then [contentPadding] is [EdgeInsets.zero]. + /// + /// If `isOutline` property of [border] is false and if [filled] is true then + /// [contentPadding] is `EdgeInsets.fromLTRB(12, 8, 12, 8)` when [isDense] + /// is true and `EdgeInsets.fromLTRB(12, 12, 12, 12)` when [isDense] is false. + /// If `isOutline` property of [border] is false and if [filled] is false then + /// [contentPadding] is `EdgeInsets.fromLTRB(0, 8, 0, 8)` when [isDense] is + /// true and `EdgeInsets.fromLTRB(0, 12, 0, 12)` when [isDense] is false. + /// + /// If `isOutline` property of [border] is true then [contentPadding] is + /// `EdgeInsets.fromLTRB(12, 20, 12, 12)` when [isDense] is true + /// and `EdgeInsets.fromLTRB(12, 24, 12, 16)` when [isDense] is false. + final EdgeInsetsGeometry? contentPadding; + + /// Whether the decoration is the same size as the input field. + /// + /// A collapsed decoration cannot have [labelText], [errorText], an [icon]. + /// + /// To create a collapsed input decoration, use [InputDecoration.collapsed]. + final bool isCollapsed; + + /// An icon that appears before the [prefix] or [prefixText] and before + /// the editable part of the text field, within the decoration's container. + /// + /// The size and color of the prefix icon is configured automatically using an + /// [IconTheme] and therefore does not need to be explicitly given in the + /// icon widget. + /// + /// The prefix icon is constrained with a minimum size of 48px by 48px, but + /// can be expanded beyond that. Anything larger than 24px will require + /// additional padding to ensure it matches the Material Design spec of 12px + /// padding between the left edge of the input and leading edge of the prefix + /// icon. The following snippet shows how to pad the leading edge of the + /// prefix icon: + /// + /// ```dart + /// prefixIcon: Padding( + /// padding: const EdgeInsetsDirectional.only(start: 12.0), + /// child: _myIcon, // _myIcon is a 48px-wide widget. + /// ) + /// ``` + /// + /// {@macro flutter.material.input_decorator.container_description} + /// + /// The prefix icon alignment can be changed using [Align] with a fixed `widthFactor` and + /// `heightFactor`. + /// + /// {@tool dartpad} + /// This example shows how the prefix icon alignment can be changed using [Align] with + /// a fixed `widthFactor` and `heightFactor`. + /// + /// ** See code in examples/api/lib/material/input_decorator/input_decoration.prefix_icon.0.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [Icon] and [ImageIcon], which are typically used to show icons. + /// * [prefix] and [prefixText], which are other ways to show content + /// before the text field (but after the icon). + /// * [suffixIcon], which is the same but on the trailing edge. + /// * [Align] A widget that aligns its child within itself and optionally + /// sizes itself based on the child's size. + final Widget? prefixIcon; + + /// The constraints for the prefix icon. + /// + /// This can be used to modify the [BoxConstraints] surrounding [prefixIcon]. + /// + /// This property is particularly useful for getting the decoration's height + /// less than 48px. This can be achieved by setting [isDense] to true and + /// setting the constraints' minimum height and width to a value lower than + /// 48px. + /// + /// {@tool dartpad} + /// This example shows the differences between two `TextField` widgets when + /// [prefixIconConstraints] is set to the default value and when one is not. + /// + /// Note that [isDense] must be set to true to be able to + /// set the constraints smaller than 48px. + /// + /// If null, [BoxConstraints] with a minimum width and height of 48px is + /// used. + /// + /// ** See code in examples/api/lib/material/input_decorator/input_decoration.prefix_icon_constraints.0.dart ** + /// {@end-tool} + final BoxConstraints? prefixIconConstraints; + + /// Optional widget to place on the line before the input. + /// + /// This can be used, for example, to add some padding to text that would + /// otherwise be specified using [prefixText], or to add a custom widget in + /// front of the input. The widget's baseline is lined up with the input + /// baseline. + /// + /// Only one of [prefix] and [prefixText] can be specified. + /// + /// The [prefix] appears after the [prefixIcon], if both are specified. + /// + /// See also: + /// + /// * [suffix], the equivalent but on the trailing edge. + final Widget? prefix; + + /// Optional text prefix to place on the line before the input. + /// + /// Uses the [prefixStyle]. Uses [hintStyle] if [prefixStyle] isn't specified. + /// The prefix text is not returned as part of the user's input. + /// + /// If a more elaborate prefix is required, consider using [prefix] instead. + /// Only one of [prefix] and [prefixText] can be specified. + /// + /// The [prefixText] appears after the [prefixIcon], if both are specified. + /// + /// See also: + /// + /// * [suffixText], the equivalent but on the trailing edge. + final String? prefixText; + + /// The style to use for the [prefixText]. + /// + /// If [prefixStyle] is a [MaterialStateTextStyle], then the effective + /// text style can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + /// + /// If null, defaults to the [hintStyle]. + /// + /// See also: + /// + /// * [suffixStyle], the equivalent but on the trailing edge. + final TextStyle? prefixStyle; + + /// Optional color of the prefixIcon + /// + /// Defaults to [iconColor] + /// + /// If [prefixIconColor] is a [MaterialStateColor], then the effective + /// color can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + final Color? prefixIconColor; + + /// An icon that appears after the editable part of the text field and + /// after the [suffix] or [suffixText], within the decoration's container. + /// + /// The size and color of the suffix icon is configured automatically using an + /// [IconTheme] and therefore does not need to be explicitly given in the + /// icon widget. + /// + /// The suffix icon is constrained with a minimum size of 48px by 48px, but + /// can be expanded beyond that. Anything larger than 24px will require + /// additional padding to ensure it matches the Material Design spec of 12px + /// padding between the right edge of the input and trailing edge of the + /// prefix icon. The following snippet shows how to pad the trailing edge of + /// the suffix icon: + /// + /// ```dart + /// suffixIcon: Padding( + /// padding: const EdgeInsetsDirectional.only(end: 12.0), + /// child: _myIcon, // myIcon is a 48px-wide widget. + /// ) + /// ``` + /// + /// The decoration's container is the area which is filled if [filled] is + /// true and bordered per the [border]. It's the area adjacent to + /// [icon] and above the widgets that contain [helperText], + /// [errorText], and [counterText]. + /// + /// The suffix icon alignment can be changed using [Align] with a fixed `widthFactor` and + /// `heightFactor`. + /// + /// {@tool dartpad} + /// This example shows how the suffix icon alignment can be changed using [Align] with + /// a fixed `widthFactor` and `heightFactor`. + /// + /// ** See code in examples/api/lib/material/input_decorator/input_decoration.suffix_icon.0.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [Icon] and [ImageIcon], which are typically used to show icons. + /// * [suffix] and [suffixText], which are other ways to show content + /// after the text field (but before the icon). + /// * [prefixIcon], which is the same but on the leading edge. + /// * [Align] A widget that aligns its child within itself and optionally + /// sizes itself based on the child's size. + final Widget? suffixIcon; + + /// Optional widget to place on the line after the input. + /// + /// This can be used, for example, to add some padding to the text that would + /// otherwise be specified using [suffixText], or to add a custom widget after + /// the input. The widget's baseline is lined up with the input baseline. + /// + /// Only one of [suffix] and [suffixText] can be specified. + /// + /// The [suffix] appears before the [suffixIcon], if both are specified. + /// + /// See also: + /// + /// * [prefix], the equivalent but on the leading edge. + final Widget? suffix; + + /// Optional text suffix to place on the line after the input. + /// + /// Uses the [suffixStyle]. Uses [hintStyle] if [suffixStyle] isn't specified. + /// The suffix text is not returned as part of the user's input. + /// + /// If a more elaborate suffix is required, consider using [suffix] instead. + /// Only one of [suffix] and [suffixText] can be specified. + /// + /// The [suffixText] appears before the [suffixIcon], if both are specified. + /// + /// See also: + /// + /// * [prefixText], the equivalent but on the leading edge. + final String? suffixText; + + /// The style to use for the [suffixText]. + /// + /// If [suffixStyle] is a [MaterialStateTextStyle], then the effective text + /// style can depend on the [MaterialState.focused] state, i.e. if the + /// [TextField] is focused or not. + /// + /// If null, defaults to the [hintStyle]. + /// + /// See also: + /// + /// * [prefixStyle], the equivalent but on the leading edge. + final TextStyle? suffixStyle; + + /// Optional color of the [suffixIcon]. + /// + /// Defaults to [iconColor] + /// + /// If [suffixIconColor] is a [MaterialStateColor], then the effective + /// color can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + final Color? suffixIconColor; + + /// The constraints for the suffix icon. + /// + /// This can be used to modify the [BoxConstraints] surrounding [suffixIcon]. + /// + /// This property is particularly useful for getting the decoration's height + /// less than 48px. This can be achieved by setting [isDense] to true and + /// setting the constraints' minimum height and width to a value lower than + /// 48px. + /// + /// If null, a [BoxConstraints] with a minimum width and height of 48px is + /// used. + /// + /// {@tool dartpad} + /// This example shows the differences between two `TextField` widgets when + /// [suffixIconConstraints] is set to the default value and when one is not. + /// + /// Note that [isDense] must be set to true to be able to + /// set the constraints smaller than 48px. + /// + /// If null, [BoxConstraints] with a minimum width and height of 48px is + /// used. + /// + /// ** See code in examples/api/lib/material/input_decorator/input_decoration.suffix_icon_constraints.0.dart ** + /// {@end-tool} + final BoxConstraints? suffixIconConstraints; + + /// Optional text to place below the line as a character count. + /// + /// Rendered using [counterStyle]. Uses [helperStyle] if [counterStyle] is + /// null. + /// + /// The semantic label can be replaced by providing a [semanticCounterText]. + /// + /// If null or an empty string and [counter] isn't specified, then nothing + /// will appear in the counter's location. + final String? counterText; + + /// Optional custom counter widget to go in the place otherwise occupied by + /// [counterText]. If this property is non null, then [counterText] is + /// ignored. + final Widget? counter; + + /// The style to use for the [counterText]. + /// + /// If [counterStyle] is a [MaterialStateTextStyle], then the effective + /// text style can depend on the [MaterialState.focused] state, i.e. + /// if the [TextField] is focused or not. + /// + /// If null, defaults to the [helperStyle]. + final TextStyle? counterStyle; + + /// If true the decoration's container is filled with [fillColor]. + /// + /// When [InputDecorator.isHovering] is true, the [hoverColor] is also blended + /// into the final fill color. + /// + /// Typically this field set to true if [border] is an [UnderlineInputBorder]. + /// + /// {@template flutter.material.input_decorator.container_description} + /// The decoration's container is the area which is filled if [filled] is true + /// and bordered per the [border]. It's the area adjacent to [icon] and above + /// the widgets that contain [helperText], [errorText], and [counterText]. + /// {@endtemplate} + /// + /// This property is false by default. + final bool? filled; + + /// The base fill color of the decoration's container color. + /// + /// When [InputDecorator.isHovering] is true, the [hoverColor] is also blended + /// into the final fill color. + /// + /// By default the [fillColor] is based on the current + /// [InputDecorationTheme.fillColor]. + /// + /// {@macro flutter.material.input_decorator.container_description} + final Color? fillColor; + + /// The fill color of the decoration's container when it has the input focus. + /// + /// By default the [focusColor] is based on the current + /// [InputDecorationTheme.focusColor]. + /// + /// This [focusColor] is ignored by [TextField] and [TextFormField] because + /// they don't respond to focus changes by changing their decorator's + /// container color, they respond by changing their border to the + /// [focusedBorder], which you can change the color of. + /// + /// {@macro flutter.material.input_decorator.container_description} + final Color? focusColor; + + /// The color of the highlight for the decoration shown if the container + /// is being hovered over by a mouse. + /// + /// If [filled] is true, the [hoverColor] is blended with [fillColor] and + /// fills the decoration's container. + /// + /// If [filled] is false, and [InputDecorator.isFocused] is false, the color + /// is blended over the [enabledBorder]'s color. + /// + /// By default the [hoverColor] is based on the current [Theme]. + /// + /// {@macro flutter.material.input_decorator.container_description} + final Color? hoverColor; + + /// The border to display when the [InputDecorator] does not have the focus and + /// is showing an error. + /// + /// See also: + /// + /// * [InputDecorator.isFocused], which is true if the [InputDecorator]'s child + /// has the focus. + /// * [InputDecoration.errorText], the error shown by the [InputDecorator], if non-null. + /// * [border], for a description of where the [InputDecorator] border appears. + /// * [UnderlineInputBorder], an [InputDecorator] border which draws a horizontal + /// line at the bottom of the input decorator's container. + /// * [OutlineInputBorder], an [InputDecorator] border which draws a + /// rounded rectangle around the input decorator's container. + /// * [InputBorder.none], which doesn't draw a border. + /// * [focusedBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is null. + /// * [focusedErrorBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is non-null. + /// * [disabledBorder], displayed when [InputDecoration.enabled] is false + /// and [InputDecoration.errorText] is null. + /// * [enabledBorder], displayed when [InputDecoration.enabled] is true + /// and [InputDecoration.errorText] is null. + final InputBorder? errorBorder; + + /// The border to display when the [InputDecorator] has the focus and is not + /// showing an error. + /// + /// See also: + /// + /// * [InputDecorator.isFocused], which is true if the [InputDecorator]'s child + /// has the focus. + /// * [InputDecoration.errorText], the error shown by the [InputDecorator], if non-null. + /// * [border], for a description of where the [InputDecorator] border appears. + /// * [UnderlineInputBorder], an [InputDecorator] border which draws a horizontal + /// line at the bottom of the input decorator's container. + /// * [OutlineInputBorder], an [InputDecorator] border which draws a + /// rounded rectangle around the input decorator's container. + /// * [InputBorder.none], which doesn't draw a border. + /// * [errorBorder], displayed when [InputDecorator.isFocused] is false + /// and [InputDecoration.errorText] is non-null. + /// * [focusedErrorBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is non-null. + /// * [disabledBorder], displayed when [InputDecoration.enabled] is false + /// and [InputDecoration.errorText] is null. + /// * [enabledBorder], displayed when [InputDecoration.enabled] is true + /// and [InputDecoration.errorText] is null. + final InputBorder? focusedBorder; + + /// The border to display when the [InputDecorator] has the focus and is + /// showing an error. + /// + /// See also: + /// + /// * [InputDecorator.isFocused], which is true if the [InputDecorator]'s child + /// has the focus. + /// * [InputDecoration.errorText], the error shown by the [InputDecorator], if non-null. + /// * [border], for a description of where the [InputDecorator] border appears. + /// * [UnderlineInputBorder], an [InputDecorator] border which draws a horizontal + /// line at the bottom of the input decorator's container. + /// * [OutlineInputBorder], an [InputDecorator] border which draws a + /// rounded rectangle around the input decorator's container. + /// * [InputBorder.none], which doesn't draw a border. + /// * [errorBorder], displayed when [InputDecorator.isFocused] is false + /// and [InputDecoration.errorText] is non-null. + /// * [focusedBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is null. + /// * [disabledBorder], displayed when [InputDecoration.enabled] is false + /// and [InputDecoration.errorText] is null. + /// * [enabledBorder], displayed when [InputDecoration.enabled] is true + /// and [InputDecoration.errorText] is null. + final InputBorder? focusedErrorBorder; + + /// The border to display when the [InputDecorator] is disabled and is not + /// showing an error. + /// + /// See also: + /// + /// * [InputDecoration.enabled], which is false if the [InputDecorator] is disabled. + /// * [InputDecoration.errorText], the error shown by the [InputDecorator], if non-null. + /// * [border], for a description of where the [InputDecorator] border appears. + /// * [UnderlineInputBorder], an [InputDecorator] border which draws a horizontal + /// line at the bottom of the input decorator's container. + /// * [OutlineInputBorder], an [InputDecorator] border which draws a + /// rounded rectangle around the input decorator's container. + /// * [InputBorder.none], which doesn't draw a border. + /// * [errorBorder], displayed when [InputDecorator.isFocused] is false + /// and [InputDecoration.errorText] is non-null. + /// * [focusedBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is null. + /// * [focusedErrorBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is non-null. + /// * [enabledBorder], displayed when [InputDecoration.enabled] is true + /// and [InputDecoration.errorText] is null. + final InputBorder? disabledBorder; + + /// The border to display when the [InputDecorator] is enabled and is not + /// showing an error. + /// + /// See also: + /// + /// * [InputDecoration.enabled], which is false if the [InputDecorator] is disabled. + /// * [InputDecoration.errorText], the error shown by the [InputDecorator], if non-null. + /// * [border], for a description of where the [InputDecorator] border appears. + /// * [UnderlineInputBorder], an [InputDecorator] border which draws a horizontal + /// line at the bottom of the input decorator's container. + /// * [OutlineInputBorder], an [InputDecorator] border which draws a + /// rounded rectangle around the input decorator's container. + /// * [InputBorder.none], which doesn't draw a border. + /// * [errorBorder], displayed when [InputDecorator.isFocused] is false + /// and [InputDecoration.errorText] is non-null. + /// * [focusedBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is null. + /// * [focusedErrorBorder], displayed when [InputDecorator.isFocused] is true + /// and [InputDecoration.errorText] is non-null. + /// * [disabledBorder], displayed when [InputDecoration.enabled] is false + /// and [InputDecoration.errorText] is null. + final InputBorder? enabledBorder; + + /// The shape of the border to draw around the decoration's container. + /// + /// If [border] is a [MaterialStateUnderlineInputBorder] + /// or [MaterialStateOutlineInputBorder], then the effective border can depend on + /// the [MaterialState.focused] state, i.e. if the [TextField] is focused or not. + /// + /// If [border] derives from [InputBorder] the border's [InputBorder.borderSide], + /// i.e. the border's color and width, will be overridden to reflect the input + /// decorator's state. Only the border's shape is used. If custom [BorderSide] + /// values are desired for a given state, all four borders – [errorBorder], + /// [focusedBorder], [enabledBorder], [disabledBorder] – must be set. + /// + /// The decoration's container is the area which is filled if [filled] is + /// true and bordered per the [border]. It's the area adjacent to + /// [InputDecoration.icon] and above the widgets that contain + /// [InputDecoration.helperText], [InputDecoration.errorText], and + /// [InputDecoration.counterText]. + /// + /// The border's bounds, i.e. the value of `border.getOuterPath()`, define + /// the area to be filled. + /// + /// This property is only used when the appropriate one of [errorBorder], + /// [focusedBorder], [focusedErrorBorder], [disabledBorder], or [enabledBorder] + /// is not specified. This border's [InputBorder.borderSide] property is + /// configured by the InputDecorator, depending on the values of + /// [InputDecoration.errorText], [InputDecoration.enabled], + /// [InputDecorator.isFocused] and the current [Theme]. + /// + /// Typically one of [UnderlineInputBorder] or [OutlineInputBorder]. + /// If null, InputDecorator's default is `const UnderlineInputBorder()`. + /// + /// See also: + /// + /// * [InputBorder.none], which doesn't draw a border. + /// * [UnderlineInputBorder], which draws a horizontal line at the + /// bottom of the input decorator's container. + /// * [OutlineInputBorder], an [InputDecorator] border which draws a + /// rounded rectangle around the input decorator's container. + final InputBorder? border; + + /// If false [helperText],[errorText], and [counterText] are not displayed, + /// and the opacity of the remaining visual elements is reduced. + /// + /// This property is true by default. + final bool enabled; + + /// A semantic label for the [counterText]. + /// + /// Defaults to null. + /// + /// If provided, this replaces the semantic label of the [counterText]. + final String? semanticCounterText; + + /// Typically set to true when the [InputDecorator] contains a multiline + /// [TextField] ([TextField.maxLines] is null or > 1) to override the default + /// behavior of aligning the label with the center of the [TextField]. + /// + /// Defaults to false. + final bool? alignLabelWithHint; + + /// Defines minimum and maximum sizes for the [InputDecorator]. + /// + /// Typically the decorator will fill the horizontal space it is given. For + /// larger screens, it may be useful to have the maximum width clamped to + /// a given value so it doesn't fill the whole screen. This property + /// allows you to control how big the decorator will be in its available + /// space. + /// + /// If null, then the ambient [ThemeData.inputDecorationTheme]'s + /// [InputDecorationTheme.constraints] will be used. If that + /// is null then the decorator will fill the available width with + /// a default height based on text size. + final BoxConstraints? constraints; + + /// Creates a copy of this input decoration with the given fields replaced + /// by the new values. + InputDecoration copyWith({ + Widget? icon, + Color? iconColor, + Widget? label, + String? labelText, + TextStyle? labelStyle, + TextStyle? floatingLabelStyle, + String? helperText, + TextStyle? helperStyle, + int? helperMaxLines, + String? hintText, + TextStyle? hintStyle, + TextDirection? hintTextDirection, + int? hintMaxLines, + String? errorText, + TextStyle? errorStyle, + int? errorMaxLines, + FloatingLabelBehavior? floatingLabelBehavior, + FloatingLabelAlignment? floatingLabelAlignment, + bool? isCollapsed, + bool? isDense, + EdgeInsetsGeometry? contentPadding, + Widget? prefixIcon, + Widget? prefix, + String? prefixText, + BoxConstraints? prefixIconConstraints, + TextStyle? prefixStyle, + Color? prefixIconColor, + Widget? suffixIcon, + Widget? suffix, + String? suffixText, + TextStyle? suffixStyle, + Color? suffixIconColor, + BoxConstraints? suffixIconConstraints, + Widget? counter, + String? counterText, + TextStyle? counterStyle, + bool? filled, + Color? fillColor, + Color? focusColor, + Color? hoverColor, + InputBorder? errorBorder, + InputBorder? focusedBorder, + InputBorder? focusedErrorBorder, + InputBorder? disabledBorder, + InputBorder? enabledBorder, + InputBorder? border, + bool? enabled, + String? semanticCounterText, + bool? alignLabelWithHint, + BoxConstraints? constraints, + }) { + return InputDecoration( + icon: icon ?? this.icon, + iconColor: iconColor ?? this.iconColor, + label: label ?? this.label, + labelText: labelText ?? this.labelText, + labelStyle: labelStyle ?? this.labelStyle, + floatingLabelStyle: floatingLabelStyle ?? this.floatingLabelStyle, + helperText: helperText ?? this.helperText, + helperStyle: helperStyle ?? this.helperStyle, + helperMaxLines: helperMaxLines ?? this.helperMaxLines, + hintText: hintText ?? this.hintText, + hintStyle: hintStyle ?? this.hintStyle, + hintTextDirection: hintTextDirection ?? this.hintTextDirection, + hintMaxLines: hintMaxLines ?? this.hintMaxLines, + errorText: errorText ?? this.errorText, + errorStyle: errorStyle ?? this.errorStyle, + errorMaxLines: errorMaxLines ?? this.errorMaxLines, + floatingLabelBehavior: floatingLabelBehavior ?? this.floatingLabelBehavior, + floatingLabelAlignment: floatingLabelAlignment ?? this.floatingLabelAlignment, + isCollapsed: isCollapsed ?? this.isCollapsed, + isDense: isDense ?? this.isDense, + contentPadding: contentPadding ?? this.contentPadding, + prefixIcon: prefixIcon ?? this.prefixIcon, + prefix: prefix ?? this.prefix, + prefixText: prefixText ?? this.prefixText, + prefixStyle: prefixStyle ?? this.prefixStyle, + prefixIconColor: prefixIconColor ?? this.prefixIconColor, + prefixIconConstraints: prefixIconConstraints ?? this.prefixIconConstraints, + suffixIcon: suffixIcon ?? this.suffixIcon, + suffix: suffix ?? this.suffix, + suffixText: suffixText ?? this.suffixText, + suffixStyle: suffixStyle ?? this.suffixStyle, + suffixIconColor: suffixIconColor ?? this.suffixIconColor, + suffixIconConstraints: suffixIconConstraints ?? this.suffixIconConstraints, + counter: counter ?? this.counter, + counterText: counterText ?? this.counterText, + counterStyle: counterStyle ?? this.counterStyle, + filled: filled ?? this.filled, + fillColor: fillColor ?? this.fillColor, + focusColor: focusColor ?? this.focusColor, + hoverColor: hoverColor ?? this.hoverColor, + errorBorder: errorBorder ?? this.errorBorder, + focusedBorder: focusedBorder ?? this.focusedBorder, + focusedErrorBorder: focusedErrorBorder ?? this.focusedErrorBorder, + disabledBorder: disabledBorder ?? this.disabledBorder, + enabledBorder: enabledBorder ?? this.enabledBorder, + border: border ?? this.border, + enabled: enabled ?? this.enabled, + semanticCounterText: semanticCounterText ?? this.semanticCounterText, + alignLabelWithHint: alignLabelWithHint ?? this.alignLabelWithHint, + constraints: constraints ?? this.constraints, + ); + } + + /// Used by widgets like [TextField] and [InputDecorator] to create a new + /// [InputDecoration] with default values taken from the [theme]. + /// + /// Only null valued properties from this [InputDecoration] are replaced + /// by the corresponding values from [theme]. + InputDecoration applyDefaults(InputDecorationTheme theme) { + return copyWith( + labelStyle: labelStyle ?? theme.labelStyle, + floatingLabelStyle: floatingLabelStyle ?? theme.floatingLabelStyle, + helperStyle: helperStyle ?? theme.helperStyle, + helperMaxLines: helperMaxLines ?? theme.helperMaxLines, + hintStyle: hintStyle ?? theme.hintStyle, + errorStyle: errorStyle ?? theme.errorStyle, + errorMaxLines: errorMaxLines ?? theme.errorMaxLines, + floatingLabelBehavior: floatingLabelBehavior, + floatingLabelAlignment: floatingLabelAlignment, + isCollapsed: isCollapsed, + isDense: isDense ?? theme.isDense, + contentPadding: contentPadding ?? theme.contentPadding, + prefixStyle: prefixStyle ?? theme.prefixStyle, + suffixStyle: suffixStyle ?? theme.suffixStyle, + counterStyle: counterStyle ?? theme.counterStyle, + filled: filled ?? theme.filled, + fillColor: fillColor ?? theme.fillColor, + focusColor: focusColor ?? theme.focusColor, + hoverColor: hoverColor ?? theme.hoverColor, + errorBorder: errorBorder ?? theme.errorBorder, + focusedBorder: focusedBorder ?? theme.focusedBorder, + focusedErrorBorder: focusedErrorBorder ?? theme.focusedErrorBorder, + disabledBorder: disabledBorder ?? theme.disabledBorder, + enabledBorder: enabledBorder ?? theme.enabledBorder, + border: border ?? theme.border, + alignLabelWithHint: alignLabelWithHint ?? theme.alignLabelWithHint, + constraints: constraints ?? theme.constraints, + ); + } + + @override + bool operator ==(Object other) { + if (identical(this, other)) { + return true; + } + if (other.runtimeType != runtimeType) { + return false; + } + return other is InputDecoration && + other.icon == icon && + other.iconColor == iconColor && + other.label == label && + other.labelText == labelText && + other.labelStyle == labelStyle && + other.floatingLabelStyle == floatingLabelStyle && + other.helperText == helperText && + other.helperStyle == helperStyle && + other.helperMaxLines == helperMaxLines && + other.hintText == hintText && + other.hintStyle == hintStyle && + other.hintTextDirection == hintTextDirection && + other.hintMaxLines == hintMaxLines && + other.errorText == errorText && + other.errorStyle == errorStyle && + other.errorMaxLines == errorMaxLines && + other.floatingLabelBehavior == floatingLabelBehavior && + other.floatingLabelAlignment == floatingLabelAlignment && + other.isDense == isDense && + other.contentPadding == contentPadding && + other.isCollapsed == isCollapsed && + other.prefixIcon == prefixIcon && + other.prefixIconColor == prefixIconColor && + other.prefix == prefix && + other.prefixText == prefixText && + other.prefixStyle == prefixStyle && + other.prefixIconConstraints == prefixIconConstraints && + other.suffixIcon == suffixIcon && + other.suffixIconColor == suffixIconColor && + other.suffix == suffix && + other.suffixText == suffixText && + other.suffixStyle == suffixStyle && + other.suffixIconConstraints == suffixIconConstraints && + other.counter == counter && + other.counterText == counterText && + other.counterStyle == counterStyle && + other.filled == filled && + other.fillColor == fillColor && + other.focusColor == focusColor && + other.hoverColor == hoverColor && + other.errorBorder == errorBorder && + other.focusedBorder == focusedBorder && + other.focusedErrorBorder == focusedErrorBorder && + other.disabledBorder == disabledBorder && + other.enabledBorder == enabledBorder && + other.border == border && + other.enabled == enabled && + other.semanticCounterText == semanticCounterText && + other.alignLabelWithHint == alignLabelWithHint && + other.constraints == constraints; + } + + @override + int get hashCode { + final List values = [ + icon, + iconColor, + label, + labelText, + floatingLabelStyle, + labelStyle, + helperText, + helperStyle, + helperMaxLines, + hintText, + hintStyle, + hintTextDirection, + hintMaxLines, + errorText, + errorStyle, + errorMaxLines, + floatingLabelBehavior, + floatingLabelAlignment, + isDense, + contentPadding, + isCollapsed, + filled, + fillColor, + focusColor, + hoverColor, + prefixIcon, + prefixIconColor, + prefix, + prefixText, + prefixStyle, + prefixIconConstraints, + suffixIcon, + suffixIconColor, + suffix, + suffixText, + suffixStyle, + suffixIconConstraints, + counter, + counterText, + counterStyle, + errorBorder, + focusedBorder, + focusedErrorBorder, + disabledBorder, + enabledBorder, + border, + enabled, + semanticCounterText, + alignLabelWithHint, + constraints, + ]; + return Object.hashAll(values); + } + + @override + String toString() { + final List description = [ + if (icon != null) 'icon: $icon', + if (iconColor != null) 'iconColor: $iconColor', + if (label != null) 'label: $label', + if (labelText != null) 'labelText: "$labelText"', + if (floatingLabelStyle != null) 'floatingLabelStyle: "$floatingLabelStyle"', + if (helperText != null) 'helperText: "$helperText"', + if (helperMaxLines != null) 'helperMaxLines: "$helperMaxLines"', + if (hintText != null) 'hintText: "$hintText"', + if (hintMaxLines != null) 'hintMaxLines: "$hintMaxLines"', + if (errorText != null) 'errorText: "$errorText"', + if (errorStyle != null) 'errorStyle: "$errorStyle"', + if (errorMaxLines != null) 'errorMaxLines: "$errorMaxLines"', + if (floatingLabelBehavior != null) 'floatingLabelBehavior: $floatingLabelBehavior', + if (floatingLabelAlignment != null) 'floatingLabelAlignment: $floatingLabelAlignment', + if (isDense ?? false) 'isDense: $isDense', + if (contentPadding != null) 'contentPadding: $contentPadding', + if (isCollapsed) 'isCollapsed: $isCollapsed', + if (prefixIcon != null) 'prefixIcon: $prefixIcon', + if (prefixIconColor != null) 'prefixIconColor: $prefixIconColor', + if (prefix != null) 'prefix: $prefix', + if (prefixText != null) 'prefixText: $prefixText', + if (prefixStyle != null) 'prefixStyle: $prefixStyle', + if (prefixIconConstraints != null) 'prefixIconConstraints: $prefixIconConstraints', + if (suffixIcon != null) 'suffixIcon: $suffixIcon', + if (suffixIconColor != null) 'suffixIconColor: $suffixIconColor', + if (suffix != null) 'suffix: $suffix', + if (suffixText != null) 'suffixText: $suffixText', + if (suffixStyle != null) 'suffixStyle: $suffixStyle', + if (suffixIconConstraints != null) 'suffixIconConstraints: $suffixIconConstraints', + if (counter != null) 'counter: $counter', + if (counterText != null) 'counterText: $counterText', + if (counterStyle != null) 'counterStyle: $counterStyle', + if (filled ?? false) 'filled: true', + if (fillColor != null) 'fillColor: $fillColor', + if (focusColor != null) 'focusColor: $focusColor', + if (hoverColor != null) 'hoverColor: $hoverColor', + if (errorBorder != null) 'errorBorder: $errorBorder', + if (focusedBorder != null) 'focusedBorder: $focusedBorder', + if (focusedErrorBorder != null) 'focusedErrorBorder: $focusedErrorBorder', + if (disabledBorder != null) 'disabledBorder: $disabledBorder', + if (enabledBorder != null) 'enabledBorder: $enabledBorder', + if (border != null) 'border: $border', + if (!enabled) 'enabled: false', + if (semanticCounterText != null) 'semanticCounterText: $semanticCounterText', + if (alignLabelWithHint != null) 'alignLabelWithHint: $alignLabelWithHint', + if (constraints != null) 'constraints: $constraints', + ]; + return 'InputDecoration(${description.join(', ')})'; + } +} + +class _InputDecoratorDefaultsM2 extends InputDecorationTheme { + const _InputDecoratorDefaultsM2(this.context) : super(); + + final BuildContext context; + + @override + TextStyle? get hintStyle => MaterialStateTextStyle.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return TextStyle(color: Theme.of(context).disabledColor); + } + return TextStyle(color: Theme.of(context).hintColor); + }); + + @override + TextStyle? get labelStyle => MaterialStateTextStyle.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return TextStyle(color: Theme.of(context).disabledColor); + } + return TextStyle(color: Theme.of(context).hintColor); + }); + + @override + TextStyle? get floatingLabelStyle => MaterialStateTextStyle.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return TextStyle(color: Theme.of(context).disabledColor); + } + if (states.contains(MaterialState.error)) { + return TextStyle(color: Theme.of(context).colorScheme.error); + } + if (states.contains(MaterialState.focused)) { + return TextStyle(color: Theme.of(context).colorScheme.primary); + } + return TextStyle(color: Theme.of(context).hintColor); + }); + + @override + TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((Set states) { + final ThemeData themeData = Theme.of(context); + if (states.contains(MaterialState.disabled)) { + return themeData.textTheme.bodySmall!.copyWith(color: Colors.transparent); + } + + return themeData.textTheme.bodySmall!.copyWith(color: themeData.hintColor); + }); + + @override + TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((Set states) { + final ThemeData themeData = Theme.of(context); + if (states.contains(MaterialState.disabled)) { + return themeData.textTheme.bodySmall!.copyWith(color: Colors.transparent); + } + return themeData.textTheme.bodySmall!.copyWith(color: themeData.colorScheme.error); + }); + + @override + Color? get fillColor => MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + // dark theme: 5% white + // light theme: 2% black + switch (Theme.of(context).brightness) { + case Brightness.dark: + return const Color(0x0DFFFFFF); + case Brightness.light: + return const Color(0x05000000); + } + } + // dark theme: 10% white + // light theme: 4% black + switch (Theme.of(context).brightness) { + case Brightness.dark: + return const Color(0x1AFFFFFF); + case Brightness.light: + return const Color(0x0A000000); + } + }); + + @override + Color? get iconColor => MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) && !states.contains(MaterialState.focused)) { + return Theme.of(context).disabledColor; + } + if (states.contains(MaterialState.focused)) { + return Theme.of(context).colorScheme.primary; + } + switch (Theme.of(context).brightness) { + case Brightness.dark: + return Colors.white70; + case Brightness.light: + return Colors.black45; + } + }); + + @override + Color? get prefixIconColor => MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) && !states.contains(MaterialState.focused)) { + return Theme.of(context).disabledColor; + } + if (states.contains(MaterialState.focused)) { + return Theme.of(context).colorScheme.primary; + } + switch (Theme.of(context).brightness) { + case Brightness.dark: + return Colors.white70; + case Brightness.light: + return Colors.black45; + } + }); + + @override + Color? get suffixIconColor => MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled) && !states.contains(MaterialState.focused)) { + return Theme.of(context).disabledColor; + } + if (states.contains(MaterialState.focused)) { + return Theme.of(context).colorScheme.primary; + } + switch (Theme.of(context).brightness) { + case Brightness.dark: + return Colors.white70; + case Brightness.light: + return Colors.black45; + } + }); +} + +// BEGIN GENERATED TOKEN PROPERTIES - InputDecorator + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// Token database version: v0_143 + +class _InputDecoratorDefaultsM3 extends InputDecorationTheme { + _InputDecoratorDefaultsM3(this.context) : super(); + + final BuildContext context; + + late final ColorScheme _colors = Theme.of(context).colorScheme; + late final TextTheme _textTheme = Theme.of(context).textTheme; + + @override + TextStyle? get hintStyle => MaterialStateTextStyle.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return TextStyle(color: Theme.of(context).disabledColor); + } + return TextStyle(color: Theme.of(context).hintColor); + }); + + @override + Color? get fillColor => MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.04); + } + return _colors.surfaceVariant; + }); + + @override + BorderSide? get activeIndicatorBorder => MaterialStateBorderSide.resolveWith((Set states) { + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _colors.error, width: 2.0); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _colors.onErrorContainer); + } + return BorderSide(color: _colors.error); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _colors.primary, width: 2.0); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _colors.onSurface); + } + if (states.contains(MaterialState.disabled)) { + return BorderSide(color: _colors.onSurface.withOpacity(0.38)); + } + return BorderSide(color: _colors.onSurfaceVariant); + }); + + @override + BorderSide? get outlineBorder => MaterialStateBorderSide.resolveWith((Set states) { + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _colors.error, width: 2.0); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _colors.onErrorContainer); + } + return BorderSide(color: _colors.error); + } + if (states.contains(MaterialState.focused)) { + return BorderSide(color: _colors.primary, width: 2.0); + } + if (states.contains(MaterialState.hovered)) { + return BorderSide(color: _colors.onSurface); + } + if (states.contains(MaterialState.disabled)) { + return BorderSide(color: _colors.onSurface.withOpacity(0.12)); + } + return BorderSide(color: _colors.outline); + }); + + @override + Color? get iconColor => _colors.onSurfaceVariant; + + @override + Color? get prefixIconColor => MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + return _colors.onSurfaceVariant; + }); + + @override + Color? get suffixIconColor => MaterialStateColor.resolveWith((Set states) { + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.hovered)) { + return _colors.onErrorContainer; + } + return _colors.error; + } + if (states.contains(MaterialState.disabled)) { + return _colors.onSurface.withOpacity(0.38); + } + return _colors.onSurfaceVariant; + }); + + @override + TextStyle? get labelStyle => MaterialStateTextStyle.resolveWith((Set states) { + final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle(); + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith(color: _colors.error); + } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onErrorContainer); + } + return textStyle.copyWith(color: _colors.error); + } + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith(color: _colors.primary); + } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onSurfaceVariant); + } + if (states.contains(MaterialState.disabled)) { + return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38)); + } + return textStyle.copyWith(color: _colors.onSurfaceVariant); + }); + + @override + TextStyle? get floatingLabelStyle => MaterialStateTextStyle.resolveWith((Set states) { + final TextStyle textStyle = _textTheme.bodyLarge ?? const TextStyle(); + if (states.contains(MaterialState.error)) { + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith(color: _colors.error); + } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onErrorContainer); + } + return textStyle.copyWith(color: _colors.error); + } + if (states.contains(MaterialState.focused)) { + return textStyle.copyWith(color: _colors.primary); + } + if (states.contains(MaterialState.hovered)) { + return textStyle.copyWith(color: _colors.onSurfaceVariant); + } + if (states.contains(MaterialState.disabled)) { + return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38)); + } + return textStyle.copyWith(color: _colors.onSurfaceVariant); + }); + + @override + TextStyle? get helperStyle => MaterialStateTextStyle.resolveWith((Set states) { + final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle(); + if (states.contains(MaterialState.disabled)) { + return textStyle.copyWith(color: _colors.onSurface.withOpacity(0.38)); + } + return textStyle.copyWith(color: _colors.onSurfaceVariant); + }); + + @override + TextStyle? get errorStyle => MaterialStateTextStyle.resolveWith((Set states) { + final TextStyle textStyle = _textTheme.bodySmall ?? const TextStyle(); + return textStyle.copyWith(color: _colors.error); + }); +} + +// END GENERATED TOKEN PROPERTIES - InputDecorator diff --git a/lib/src/widgets/text_input/text_input.dart b/lib/src/widgets/text_input/text_input.dart index 227225cd..01904130 100644 --- a/lib/src/widgets/text_input/text_input.dart +++ b/lib/src/widgets/text_input/text_input.dart @@ -1,4 +1,12 @@ -import 'package:flutter/material.dart'; +import 'dart:ui' as ui show BoxHeightStyle, BoxWidthStyle; + +import 'package:figma_squircle/figma_squircle.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' + hide FloatingLabelAlignment, FloatingLabelBehavior, InputDecoration, InputDecorator; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; import 'package:moon_design/src/theme/borders.dart'; @@ -11,6 +19,10 @@ import 'package:moon_design/src/theme/theme.dart'; import 'package:moon_design/src/utils/extensions.dart'; import 'package:moon_design/src/widgets/common/animated_icon_theme.dart'; import 'package:moon_design/src/widgets/common/effects/focus_effect.dart'; +import 'package:moon_design/src/widgets/text_input/input_decorator.dart'; + +export 'package:flutter/services.dart' + show SmartDashesType, SmartQuotesType, TextCapitalization, TextInputAction, TextInputType; enum MoonTextInputSize { sm, @@ -21,52 +33,97 @@ enum MoonTextInputSize { typedef MoonTextInputErrorBuilder = Widget Function(BuildContext context, String? errorText); +/// A Moon Design text input. +/// +/// A text input lets the user enter text, either with hardware keyboard or with +/// an onscreen keyboard. +/// +/// The text input calls the [onChanged] callback whenever the user changes the +/// text in the field. If the user indicates that they are done typing in the +/// field (e.g., by pressing a button on the soft keyboard), the text input +/// calls the [onSubmitted] callback. +/// +/// To control the text that is displayed in the text input, use the +/// [controller]. For example, to set the initial value of the text input, use +/// a [controller] that already contains some text. The [controller] can also +/// control the selection and composing region (and to observe changes to the +/// text, selection, and composing region). +/// +/// By default, a text input has a [decoration] that draws a divider below the +/// text input. You can use the [decoration] property to control the decoration, +/// for example by adding a label or an icon. If you set the [decoration] +/// property to null, the decoration will be removed entirely, including the +/// extra padding introduced by the decoration to save space for the labels. +/// +/// If [decoration] is non-null (which is the default), the text input requires +/// one of its ancestors to be a [Material] widget. +/// +/// To integrate the [MoonTextInput] into a [Form] with other [FormField] widgets, +/// consider using [MoonFormTextInput]. +/// +/// {@template flutter.material.textfield.wantKeepAlive} +/// When the widget has focus, it will prevent itself from disposing via its +/// underlying [EditableText]'s [AutomaticKeepAliveClientMixin.wantKeepAlive] in +/// order to avoid losing the selection. Removing the focus will allow it to be +/// disposed. +/// {@endtemplate} +/// +/// Remember to call [TextEditingController.dispose] of the [TextEditingController] +/// when it is no longer needed. This will ensure we discard any resources used +/// by the object. +/// +/// ## Reading values +/// +/// A common way to read a value from a MoonTextInput is to use the [onSubmitted] +/// callback. This callback is applied to the text input's current value when +/// the user finishes editing. +/// +/// {@macro flutter.widgets.EditableText.lifeCycle} +/// +/// For most applications the [onSubmitted] callback will be sufficient for +/// reacting to user input. +/// +/// The [onEditingComplete] callback also runs when the user finishes editing. +/// It's different from [onSubmitted] because it has a default value which +/// updates the text controller and yields the keyboard focus. Applications that +/// require different behavior can override the default [onEditingComplete] +/// callback. +/// +/// Keep in mind you can also always read the current string from a MoonTextInput's +/// [TextEditingController] using [TextEditingController.text]. +/// +/// ## Handling emojis and other complex characters +/// {@macro flutter.widgets.EditableText.onChanged} +/// +/// In the live Dartpad example above, try typing the emoji 👨‍👩‍👦 +/// into the field and submitting. Because the example code measures the length +/// with `value.characters.length`, the emoji is correctly counted as a single +/// character. +/// +/// {@macro flutter.widgets.editableText.showCaretOnScreen} +/// +/// {@macro flutter.widgets.editableText.accessibility} class MoonTextInput extends StatefulWidget { - /// Used to set the auto validation mode. - final AutovalidateMode autovalidateMode; - - /// {@macro flutter.widgets.editableText.autocorrect} - final bool autocorrect; + /// If [maxLength] is set to this value, only the "current input length" + /// part of the character counter is shown. + static const int noMaxLength = -1; - /// {@macro flutter.widgets.editableText.autofocus} - final bool autofocus; - - /// If false the widget is "disabled": it ignores taps and it has a reduced opacity. - final bool enabled; - - /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} - final bool enableIMEPersonalizedLearning; - - /// {@macro flutter.widgets.editableText.enableInteractiveSelection} - final bool? enableInteractiveSelection; - - /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} - final bool enableSuggestions; + // Moon Design props - /// {@macro flutter.widgets.editableText.readOnly} - final bool readOnly; + /// Whether the text input has floating label. + final bool hasFloatingLabel; - /// {@macro flutter.widgets.editableText.scribbleEnabled} - final bool scribbleEnabled; + /// Whether the focus effect is enabled. + final bool hasFocusEffect; - /// {@macro flutter.widgets.editableText.showCursor} - final bool? showCursor; + /// Whether the input is dense ie takes less space. + /// + /// This property is true by default. + final bool isDense; /// The border radius of the input. final BorderRadiusGeometry? borderRadius; - /// The appearance of the keyboard. - /// - /// This setting is only honored on iOS devices. - /// - /// If unset, defaults to [ThemeData.brightness]. - final Brightness? keyboardAppearance; - - /// {@macro flutter.material.Material.clipBehavior} - /// - /// Defaults to [Clip.hardEdge]. - final Clip? clipBehavior; - /// The background color of the input. final Color? backgroundColor; @@ -100,68 +157,117 @@ class MoonTextInput extends StatefulWidget { /// The transition curve for disable animation. final Curve? transitionCurve; - /// {@macro flutter.widgets.editableText.scrollPadding} - final EdgeInsets scrollPadding; - /// The padding of the text input. final EdgeInsetsGeometry? padding; - /// The padding around supporting widget or error builder. - final EdgeInsetsGeometry? supportingPadding; + /// The padding around helper widget or error builder. + final EdgeInsetsGeometry? helperPadding; - /// {@macro flutter.widgets.Focus.focusNode}. - final FocusNode? focusNode; + /// The size of the text input. + final MoonTextInputSize? textInputSize; - /// The maximum number of characters (Unicode grapheme clusters) to allow in the input. - /// - /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} - final int? maxLength; + /// The text for the error + final String? errorText; - /// {@macro flutter.widgets.editableText.minLines} - /// * [expands], which determines whether the field should fill the height of its parent. - final int? minLines; + /// The text for the hint. + final String? hintText; - /// {@macro flutter.widgets.editableText.autofillHints} - /// {@macro flutter.services.AutofillConfiguration.autofillHints} - final Iterable? autofillHints; + /// The initial value of the input. + final String? initialValue; - /// {@macro flutter.widgets.editableText.inputFormatters} - final List? inputFormatters; + /// The textStyle to use for the text being edited. + final TextStyle? textStyle; - /// Determines how the [maxLength] limit should be enforced. - /// - /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} - /// - /// {@macro flutter.services.textFormatter.maxLengthEnforcement} - final MaxLengthEnforcement? maxLengthEnforcement; + /// The textStyle to use for the error state text. + final TextStyle? helperTextStyle; - /// The size of the text input. - final MoonTextInputSize? textInputSize; + /// Builder for the error widget. + final MoonTextInputErrorBuilder? errorBuilder; - /// {@macro flutter.widgets.editableText.scrollController} - final ScrollController? scrollController; + /// The widget in the leading slot of the text input. + final Widget? leading; - /// {@macro flutter.widgets.editableText.scrollPhysics} - final ScrollPhysics? scrollPhysics; + /// The widget in the trailing slot of the text input. + final Widget? trailing; - /// The text for the hint. - final String? hintText; + /// The widget in the helper slot of the text area. + final Widget? helper; - /// The initial value of the input. - final String? initialValue; + // Flutter props - /// {@template flutter.material.textfield.restorationId} - /// Restoration ID to save and restore the state of the text field. + /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.intro} /// - /// See also: + /// {@macro flutter.widgets.magnifier.intro} /// - /// * [RestorationManager], which explains how state restoration works in - /// Flutter. - /// {@endtemplate} - final String? restorationId; + /// {@macro flutter.widgets.magnifier.TextMagnifierConfiguration.details} + /// + /// By default, builds a [CupertinoTextMagnifier] on iOS and [TextMagnifier] + /// on Android, and builds nothing on all other platforms. If it is desired to + /// suppress the magnifier, consider passing [TextMagnifierConfiguration.disabled]. + /// + /// {@tool dartpad} + /// This sample demonstrates how to customize the magnifier that this text input uses. + /// + /// ** See code in examples/api/lib/widgets/text_magnifier/text_magnifier.0.dart ** + /// {@end-tool} + final TextMagnifierConfiguration? magnifierConfiguration; - /// The semantic label for the widget. - final String? semanticLabel; + /// Controls the text being edited. + /// + /// If null, this widget will create its own [TextEditingController]. + final TextEditingController? controller; + + /// Defines the keyboard focus for this widget. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the keyboard focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// myFocusNode.addListener(() { print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode]. + /// + /// ## Keyboard + /// + /// Requesting the focus will typically cause the keyboard to be shown + /// if it's not showing already. + /// + /// On Android, the user can hide the keyboard - without changing the focus - + /// with the system back button. They can restore the keyboard's visibility + /// by tapping on a text input. The user might hide the keyboard and + /// switch to a physical keyboard, or they might just need to get it + /// out of the way for a moment, to expose something it's + /// obscuring. In this case requesting the focus again will not + /// cause the focus to change, and will not make the keyboard visible. + /// + /// This widget builds an [EditableText] and will ensure that the keyboard is + /// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. + final FocusNode? focusNode; + + /// {@macro flutter.widgets.editableText.keyboardType} + final TextInputType keyboardType; + + /// The type of action button to use for the keyboard. + /// + /// Defaults to [TextInputAction.newline] if [keyboardType] is + /// [TextInputType.multiline] and [TextInputAction.done] otherwise. + final TextInputAction? textInputAction; + + /// {@macro flutter.widgets.editableText.textCapitalization} + final TextCapitalization textCapitalization; /// {@macro flutter.widgets.editableText.strutStyle} final StrutStyle? strutStyle; @@ -169,88 +275,338 @@ class MoonTextInput extends StatefulWidget { /// {@macro flutter.widgets.editableText.textAlign} final TextAlign textAlign; - /// {@macro flutter.widgets.editableText.textCapitalization} - final TextCapitalization textCapitalization; + /// {@macro flutter.material.InputDecorator.textAlignVertical} + final TextAlignVertical textAlignVertical; /// {@macro flutter.widgets.editableText.textDirection} final TextDirection? textDirection; - /// Controls the text being edited. - final TextEditingController? controller; + /// {@macro flutter.widgets.editableText.autofocus} + final bool autofocus; - /// The type of action button to use for the keyboard. - /// - /// Defaults to [TextInputAction.newline] if [keyboardType] is - /// [TextInputType.multiline] and [TextInputAction.done] otherwise. - final TextInputAction? textInputAction; + /// {@macro flutter.widgets.editableText.obscuringCharacter} + final String obscuringCharacter; - /// The style to use for the text being edited. - /// - /// This text style is also used as the base style for the [decoration]. - final TextStyle? textStyle; + /// {@macro flutter.widgets.editableText.obscureText} + final bool obscureText; - /// The style to use for the error state text. - final TextStyle? supportingTextStyle; + /// {@macro flutter.widgets.editableText.autocorrect} + final bool autocorrect; - /// Validator for the input widget. - final FormFieldValidator? validator; + /// {@macro flutter.services.TextInputConfiguration.smartDashesType} + final SmartDashesType smartDashesType; - /// A callback that is called when the user taps the input widget. - final GestureTapCallback? onTap; + /// {@macro flutter.services.TextInputConfiguration.smartQuotesType} + final SmartQuotesType smartQuotesType; - /// A callback that is called when the user taps outside the input widget. - final TapRegionCallback? onTapOutside; + /// {@macro flutter.services.TextInputConfiguration.enableSuggestions} + final bool enableSuggestions; + + /// {@macro flutter.widgets.editableText.maxLines} + /// * [expands], which determines whether the field should fill the height of + /// its parent. + final int? maxLines; + + /// {@macro flutter.widgets.editableText.minLines} + /// * [expands], which determines whether the field should fill the height of + /// its parent. + final int? minLines; + + /// {@macro flutter.widgets.editableText.expands} + final bool expands; + + /// {@macro flutter.widgets.editableText.readOnly} + final bool readOnly; + + /// {@macro flutter.widgets.editableText.showCursor} + final bool? showCursor; + + /// The maximum number of characters (Unicode grapheme clusters) to allow in + /// the text input. + /// + /// If set, a character counter will be displayed below the + /// field showing how many characters have been entered. If set to a number + /// greater than 0, it will also display the maximum number allowed. If set + /// to [MoonTextInput.noMaxLength] then only the current character count is displayed. + /// + /// After [maxLength] characters have been input, additional input + /// is ignored, unless [maxLengthEnforcement] is set to + /// [MaxLengthEnforcement.none]. + /// + /// The text input enforces the length with a [LengthLimitingTextInputFormatter], + /// which is evaluated after the supplied [inputFormatters], if any. + /// + /// This value must be either null, [MoonTextInput.noMaxLength], or greater than 0. + /// If null (the default) then there is no limit to the number of characters + /// that can be entered. If set to [MoonTextInput.noMaxLength], then no limit will + /// be enforced, but the number of characters entered will still be displayed. + /// + /// Whitespace characters (e.g. newline, space, tab) are included in the + /// character count. + /// + /// If [maxLengthEnforcement] is [MaxLengthEnforcement.none], then more than + /// [maxLength] characters may be entered, but the error counter and divider + /// will switch to the [decoration]'s [InputDecoration.errorStyle] when the + /// limit is exceeded. + /// + /// {@macro flutter.services.lengthLimitingTextInputFormatter.maxLength} + final int? maxLength; + + /// Determines how the [maxLength] limit should be enforced. + /// + /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} + /// + /// {@macro flutter.services.textFormatter.maxLengthEnforcement} + final MaxLengthEnforcement? maxLengthEnforcement; /// {@macro flutter.widgets.editableText.onChanged} /// /// See also: /// - /// * [inputFormatters]: which are called before [onChanged] runs and can validate and change ("format") the input - /// value. - /// * [onEditingComplete], [onSubmitted]: which are more specialized input change notifications. + /// * [inputFormatters], which are called before [onChanged] + /// runs and can validate and change ("format") the input value. + /// * [onEditingComplete], [onSubmitted]: + /// which are more specialized input change notifications. final ValueChanged? onChanged; /// {@macro flutter.widgets.editableText.onEditingComplete} final VoidCallback? onEditingComplete; - /// [FormState.save]. - final ValueChanged? onSaved; - /// {@macro flutter.widgets.editableText.onSubmitted} /// /// See also: /// - /// * [TextInputAction.next] and [TextInputAction.previous], which automatically shift the focus to the next/previous - /// focusable item when the user is done editing. + /// * [TextInputAction.next] and [TextInputAction.previous], which + /// automatically shift the focus to the next/previous focusable item when + /// the user is done editing. final ValueChanged? onSubmitted; - /// Builder for the error widget. - final MoonTextInputErrorBuilder? errorBuilder; + /// {@macro flutter.widgets.editableText.onAppPrivateCommand} + final AppPrivateCommandCallback? onAppPrivateCommand; - /// The widget in the leading slot of the text input. - final Widget? leading; + /// {@macro flutter.widgets.editableText.inputFormatters} + final List? inputFormatters; - /// The widget in the trailing slot of the text input. - final Widget? trailing; + /// If false the text input is "disabled": it ignores taps and its + /// [decoration] is rendered in grey. + /// + /// If non-null this property overrides the [decoration]'s + /// [InputDecoration.enabled] property. + final bool enabled; + + /// {@macro flutter.widgets.editableText.cursorWidth} + final double cursorWidth; + + /// {@macro flutter.widgets.editableText.cursorHeight} + final double? cursorHeight; - /// The widget in the supporting slot of the text area. - final Widget? supporting; + /// {@macro flutter.widgets.editableText.cursorRadius} + final Radius? cursorRadius; + /// The color of the cursor. + /// + /// The cursor indicates the current location of text insertion point in + /// the field. + /// + /// If this is null it will default to the ambient + /// [DefaultSelectionStyle.cursorColor]. If that is null, and the + /// [ThemeData.platform] is [TargetPlatform.iOS] or [TargetPlatform.macOS] + /// it will use [CupertinoThemeData.primaryColor]. Otherwise it will use + /// the value of [ColorScheme.primary] of [ThemeData.colorScheme]. + final Color? cursorColor; + + /// Controls how tall the selection highlight boxes are computed to be. + /// + /// See [ui.BoxHeightStyle] for details on available textStyles. + final ui.BoxHeightStyle selectionHeightStyle; + + /// Controls how wide the selection highlight boxes are computed to be. + /// + /// See [ui.BoxWidthStyle] for details on available textStyles. + final ui.BoxWidthStyle selectionWidthStyle; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// If unset, defaults to [ThemeData.brightness]. + final Brightness? keyboardAppearance; + + /// {@macro flutter.widgets.editableText.scrollPadding} + final EdgeInsets scrollPadding; + + /// {@macro flutter.widgets.editableText.enableInteractiveSelection} + final bool enableInteractiveSelection; + + /// {@macro flutter.widgets.editableText.selectionControls} + final TextSelectionControls? selectionControls; + + /// {@macro flutter.widgets.scrollable.dragStartBehavior} + final DragStartBehavior dragStartBehavior; + + /// {@macro flutter.widgets.editableText.selectionEnabled} + bool get selectionEnabled => enableInteractiveSelection; + + /// {@template flutter.material.textfield.onTap} + /// Called for each distinct tap except for every second tap of a double tap. + /// + /// The text input builds a [GestureDetector] to handle input events like tap, + /// to trigger focus requests, to move the caret, adjust the selection, etc. + /// Handling some of those events by wrapping the text input with a competing + /// GestureDetector is problematic. + /// + /// To unconditionally handle taps, without interfering with the text input's + /// internal gesture detector, provide this callback. + /// + /// If the text input is created with [enabled] false, taps will not be + /// recognized. + /// + /// To be notified when the text input gains or loses the focus, provide a + /// [focusNode] and add a listener to that. + /// + /// To listen to arbitrary pointer events without competing with the + /// text input's internal gesture detector, use a [Listener]. + /// {@endtemplate} + final GestureTapCallback? onTap; + + /// {@macro flutter.widgets.editableText.onTapOutside} + /// + /// See also: + /// + /// * [TapRegion] for how the region group is determined. + final TapRegionCallback? onTapOutside; + + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [MaterialStateProperty], + /// [MaterialStateProperty.resolve] is used for the following [MaterialState]s: + /// + /// * [MaterialState.error]. + /// * [MaterialState.hovered]. + /// * [MaterialState.focused]. + /// * [MaterialState.disabled]. + /// + /// If this property is null, [MaterialStateMouseCursor.textable] will be used. + /// + /// The [mouseCursor] is the only property of [MoonTextInput] that controls the + /// appearance of the mouse pointer. All other properties related to "cursor" + /// stand for the text cursor, which is usually a blinking vertical line at + /// the editing position. + final MouseCursor? mouseCursor; + + /// {@macro flutter.widgets.editableText.scrollPhysics} + final ScrollPhysics? scrollPhysics; + + /// {@macro flutter.widgets.editableText.scrollController} + final ScrollController? scrollController; + + /// {@macro flutter.widgets.editableText.autofillHints} + /// {@macro flutter.services.AutofillConfiguration.autofillHints} + final Iterable? autofillHints; + + /// {@macro flutter.material.Material.clipBehavior} + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// {@template flutter.material.textfield.restorationId} + /// Restoration ID to save and restore the state of the text input. + /// + /// If non-null, the text input will persist and restore its current scroll + /// offset and - if no [controller] has been provided - the content of the + /// text input. If a [controller] has been provided, it is the responsibility + /// of the owner of that controller to persist and restore it, e.g. by using + /// a [RestorableTextEditingController]. + /// + /// The state of this widget is persisted in a [RestorationBucket] claimed + /// from the surrounding [RestorationScope] using the provided restoration ID. + /// + /// See also: + /// + /// * [RestorationManager], which explains how state restoration works in + /// Flutter. + /// {@endtemplate} + final String? restorationId; + + /// {@macro flutter.widgets.editableText.scribbleEnabled} + final bool scribbleEnabled; + + /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} + final bool enableIMEPersonalizedLearning; + + /// {@macro flutter.widgets.EditableText.contextMenuBuilder} + /// + /// If not provided, will build a default menu based on the platform. + /// + /// See also: + /// + /// * [AdaptiveTextSelectionToolbar], which is built by default. + final EditableTextContextMenuBuilder? contextMenuBuilder; + + static Widget _defaultContextMenuBuilder(BuildContext context, EditableTextState editableTextState) { + return AdaptiveTextSelectionToolbar.editableText( + editableTextState: editableTextState, + ); + } + + /// {@macro flutter.widgets.EditableText.spellCheckConfiguration} + /// + /// If [SpellCheckConfiguration.misspelledTextStyle] is not specified in this + /// configuration, then [materialMisspelledTextStyle] is used by default. + final SpellCheckConfiguration? spellCheckConfiguration; + + /// Creates a Moon Design text input. + /// + /// The [maxLines] property can be set to null to remove the restriction on + /// the number of lines. By default, it is one, meaning this is a single-line + /// text input. [maxLines] must not be zero. + /// + /// The [maxLength] property is set to null by default, which means the + /// number of characters allowed in the text input is not restricted. If + /// [maxLength] is set a character counter will be displayed below the + /// field showing how many characters have been entered. If the value is + /// set to a positive integer it will also display the maximum allowed + /// number of characters to be entered. If the value is set to + /// [MoonTextInput.noMaxLength] then only the current length is displayed. + /// + /// After [maxLength] characters have been input, additional input + /// is ignored, unless [maxLengthEnforcement] is set to + /// [MaxLengthEnforcement.none]. + /// The text input enforces the length with a [LengthLimitingTextInputFormatter], + /// which is evaluated after the supplied [inputFormatters], if any. + /// The [maxLength] value must be either null or greater than zero. + /// + /// If [maxLengthEnforcement] is set to [MaxLengthEnforcement.none], then more + /// than [maxLength] characters may be entered, and the error counter and + /// divider will switch to the [decoration].errorStyle when the limit is + /// exceeded. + /// + /// The text cursor is not shown if [showCursor] is false or if [showCursor] + /// is null (the default) and [readOnly] is true. + /// + /// The [selectionHeightStyle] and [selectionWidthStyle] properties allow + /// changing the shape of the selection highlighting. These properties default + /// to [ui.BoxHeightStyle.tight] and [ui.BoxWidthStyle.tight] respectively and + /// must not be null. + /// + /// The [textAlign], [autofocus], [obscureText], [readOnly], [autocorrect], + /// [scrollPadding], [maxLines], [maxLength], [selectionHeightStyle], + /// [selectionWidthStyle], [enableSuggestions], and + /// [enableIMEPersonalizedLearning] arguments must not be null. + /// + /// See also: + /// + /// * [maxLength], which discusses the precise meaning of "number of + /// characters" and how it may differ from the intuitive meaning. const MoonTextInput({ super.key, - this.autovalidateMode = AutovalidateMode.disabled, - this.autocorrect = true, - this.autofocus = false, - this.enabled = true, - this.enableIMEPersonalizedLearning = true, - this.enableInteractiveSelection, - this.enableSuggestions = true, - this.readOnly = false, - this.scribbleEnabled = true, - this.showCursor, + + // Moon Design props + this.hasFloatingLabel = false, + this.hasFocusEffect = true, + this.isDense = true, this.borderRadius, - this.keyboardAppearance, - this.clipBehavior, this.backgroundColor, this.activeBorderColor, this.inactiveBorderColor, @@ -262,51 +618,337 @@ class MoonTextInput extends StatefulWidget { this.height, this.transitionDuration, this.transitionCurve, - this.scrollPadding = const EdgeInsets.all(20.0), this.padding, - this.supportingPadding, - this.focusNode, - this.maxLength, - this.minLines, - this.autofillHints, - this.inputFormatters, - this.maxLengthEnforcement, + this.helperPadding, this.textInputSize, - this.scrollController, - this.scrollPhysics, + this.errorText, this.hintText, this.initialValue, - this.restorationId, - this.semanticLabel, + this.textStyle, + this.helperTextStyle, + this.errorBuilder, + this.leading, + this.trailing, + this.helper, + + // Flutter props + this.controller, + this.focusNode, + TextInputType? keyboardType, + this.textInputAction, + this.textCapitalization = TextCapitalization.none, this.strutStyle, this.textAlign = TextAlign.start, - this.textCapitalization = TextCapitalization.none, + this.textAlignVertical = TextAlignVertical.center, this.textDirection, - this.controller, - this.textInputAction, - this.textStyle, - this.supportingTextStyle, - this.validator, - this.onTap, - this.onTapOutside, + this.readOnly = false, + this.showCursor, + this.autofocus = false, + this.obscuringCharacter = '•', + this.obscureText = false, + this.autocorrect = true, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + this.enableSuggestions = true, + this.maxLines = 1, + this.minLines, + this.expands = false, + this.maxLength, + this.maxLengthEnforcement, this.onChanged, this.onEditingComplete, - this.onSaved, this.onSubmitted, - this.errorBuilder, - this.leading, - this.trailing, - this.supporting, - }); + this.onAppPrivateCommand, + this.inputFormatters, + this.enabled = true, + this.cursorWidth = 2.0, + this.cursorHeight, + this.cursorRadius, + this.cursorColor, + this.selectionHeightStyle = ui.BoxHeightStyle.tight, + this.selectionWidthStyle = ui.BoxWidthStyle.tight, + this.keyboardAppearance, + this.scrollPadding = const EdgeInsets.all(20.0), + this.dragStartBehavior = DragStartBehavior.start, + bool? enableInteractiveSelection, + this.selectionControls, + this.onTap, + this.onTapOutside, + this.mouseCursor, + this.scrollController, + this.scrollPhysics, + this.autofillHints = const [], + this.clipBehavior = Clip.hardEdge, + this.restorationId, + this.scribbleEnabled = true, + this.enableIMEPersonalizedLearning = true, + this.contextMenuBuilder = _defaultContextMenuBuilder, + this.spellCheckConfiguration, + this.magnifierConfiguration, + }) : assert(obscuringCharacter.length == 1), + smartDashesType = smartDashesType ?? (obscureText ? SmartDashesType.disabled : SmartDashesType.enabled), + smartQuotesType = smartQuotesType ?? (obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled), + assert(maxLines == null || maxLines > 0), + assert(minLines == null || minLines > 0), + assert( + (maxLines == null) || (minLines == null) || (maxLines >= minLines), + "minLines can't be greater than maxLines", + ), + assert( + !expands || (maxLines == null && minLines == null), + 'minLines and maxLines must be null when expands is true.', + ), + assert(!obscureText || maxLines == 1, 'Obscured fields cannot be multiline.'), + assert(maxLength == null || maxLength == MoonTextInput.noMaxLength || maxLength > 0), + // Assert the following instead of setting it directly to avoid surprising the user by silently changing the value they set. + assert( + !identical(textInputAction, TextInputAction.newline) || + maxLines == 1 || + !identical(keyboardType, TextInputType.text), + 'Use keyboardType TextInputType.multiline when using TextInputAction.newline on a multiline MoonTextInput.', + ), + keyboardType = keyboardType ?? (maxLines == 1 ? TextInputType.text : TextInputType.multiline), + enableInteractiveSelection = enableInteractiveSelection ?? (!readOnly || !obscureText); + + /// The [TextStyle] used to indicate misspelled words in the Material textStyle. + /// + /// See also: + /// * [SpellCheckConfiguration.misspelledTextStyle], the textStyle configured to + /// mark misspelled words with. + /// * [CupertinoTextField.cupertinoMisspelledTextStyle], the textStyle configured + /// to mark misspelled words with in the Cupertino textStyle. + static const TextStyle materialMisspelledTextStyle = TextStyle( + decoration: TextDecoration.underline, + decorationColor: Colors.red, + decorationStyle: TextDecorationStyle.wavy, + ); @override State createState() => _MoonTextInputState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('controller', controller, defaultValue: null)); + properties.add(DiagnosticsProperty('focusNode', focusNode, defaultValue: null)); + properties.add(DiagnosticsProperty('enabled', enabled, defaultValue: null)); + properties.add(DiagnosticsProperty('keyboardType', keyboardType, defaultValue: TextInputType.text)); + properties.add(DiagnosticsProperty('textStyle', textStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('autofocus', autofocus, defaultValue: false)); + properties.add(DiagnosticsProperty('obscuringCharacter', obscuringCharacter, defaultValue: '•')); + properties.add(DiagnosticsProperty('obscureText', obscureText, defaultValue: false)); + properties.add(DiagnosticsProperty('autocorrect', autocorrect, defaultValue: true)); + properties.add( + EnumProperty( + 'smartDashesType', + smartDashesType, + defaultValue: obscureText ? SmartDashesType.disabled : SmartDashesType.enabled, + ), + ); + properties.add( + EnumProperty( + 'smartQuotesType', + smartQuotesType, + defaultValue: obscureText ? SmartQuotesType.disabled : SmartQuotesType.enabled, + ), + ); + properties.add(DiagnosticsProperty('enableSuggestions', enableSuggestions, defaultValue: true)); + properties.add(IntProperty('maxLines', maxLines, defaultValue: 1)); + properties.add(IntProperty('minLines', minLines, defaultValue: null)); + properties.add(DiagnosticsProperty('expands', expands, defaultValue: false)); + properties.add(IntProperty('maxLength', maxLength, defaultValue: null)); + properties + .add(EnumProperty('maxLengthEnforcement', maxLengthEnforcement, defaultValue: null)); + properties.add(EnumProperty('textInputAction', textInputAction, defaultValue: null)); + properties.add( + EnumProperty( + 'textCapitalization', + textCapitalization, + defaultValue: TextCapitalization.none, + ), + ); + properties.add(EnumProperty('textAlign', textAlign, defaultValue: TextAlign.start)); + properties.add(DiagnosticsProperty('textAlignVertical', textAlignVertical, defaultValue: null)); + properties.add(EnumProperty('textDirection', textDirection, defaultValue: null)); + properties.add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)); + properties.add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)); + properties.add(DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)); + properties.add(ColorProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add(DiagnosticsProperty('keyboardAppearance', keyboardAppearance, defaultValue: null)); + properties.add( + DiagnosticsProperty( + 'scrollPadding', + scrollPadding, + defaultValue: const EdgeInsets.all(20.0), + ), + ); + properties.add( + FlagProperty('selectionEnabled', value: selectionEnabled, defaultValue: true, ifFalse: 'selection disabled'), + ); + properties + .add(DiagnosticsProperty('selectionControls', selectionControls, defaultValue: null)); + properties.add(DiagnosticsProperty('scrollController', scrollController, defaultValue: null)); + properties.add(DiagnosticsProperty('scrollPhysics', scrollPhysics, defaultValue: null)); + properties.add(DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: Clip.hardEdge)); + properties.add(DiagnosticsProperty('scribbleEnabled', scribbleEnabled, defaultValue: true)); + properties.add( + DiagnosticsProperty('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true), + ); + properties.add( + DiagnosticsProperty( + 'spellCheckConfiguration', + spellCheckConfiguration, + defaultValue: null, + ), + ); + } } -class _MoonTextInputState extends State { - bool _isFocused = false; - bool _isHovered = false; - String? _errorText; +class _MoonTextInputState extends State + with RestorationMixin + implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { + late _TextFieldSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder; + + bool _isHovering = false; + bool _showSelectionHandles = false; + + RestorableTextEditingController? _controller; + TextEditingController get _effectiveController => widget.controller ?? _controller!.value; + + FocusNode? _focusNode; + FocusNode get _effectiveFocusNode => widget.focusNode ?? (_focusNode ??= FocusNode()); + + EditableTextState? get _editableText => editableTextKey.currentState; + + int get _currentLength => _effectiveController.value.text.characters.length; + + bool get _isEnabled => widget.enabled; + + bool get _hasError => _hasIntrinsicError; + + bool get _hasIntrinsicError => + widget.maxLength != null && + widget.maxLength! > 0 && + _effectiveController.value.text.characters.length > widget.maxLength!; + + bool get _canRequestFocus { + final NavigationMode mode = MediaQuery.maybeOf(context)?.navigationMode ?? NavigationMode.traditional; + switch (mode) { + case NavigationMode.traditional: + return _isEnabled; + case NavigationMode.directional: + return true; + } + } + + MaxLengthEnforcement get _effectiveMaxLengthEnforcement => + widget.maxLengthEnforcement ?? + LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement(Theme.of(context).platform); + + void _registerController() { + assert(_controller != null); + registerForRestoration(_controller!, 'controller'); + } + + void _createLocalController([TextEditingValue? value]) { + assert(_controller == null); + _controller = value == null ? RestorableTextEditingController() : RestorableTextEditingController.fromValue(value); + if (!restorePending) { + _registerController(); + } + } + + void _requestKeyboard() { + _editableText?.requestKeyboard(); + } + + bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { + // When the text input is activated by something that doesn't trigger the + // selection overlay, we shouldn't show the handles either. + if (!_selectionGestureDetectorBuilder.shouldShowSelectionToolbar) { + return false; + } + + if (cause == SelectionChangedCause.keyboard) { + return false; + } + + if (widget.readOnly && _effectiveController.selection.isCollapsed) { + return false; + } + + if (!_isEnabled) { + return false; + } + + if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.scribble) { + return true; + } + + if (_effectiveController.text.isNotEmpty) { + return true; + } + + return false; + } + + void _handleFocusChanged() { + setState(() { + // Rebuild the widget on focus change to show/hide the text selection + // highlight. + }); + } + + void _handleSelectionChanged(TextSelection selection, SelectionChangedCause? cause) { + final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); + if (willShowSelectionHandles != _showSelectionHandles) { + setState(() { + _showSelectionHandles = willShowSelectionHandles; + }); + } + + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + case TargetPlatform.fuchsia: + case TargetPlatform.android: + if (cause == SelectionChangedCause.longPress || cause == SelectionChangedCause.drag) { + _editableText?.bringIntoView(selection.extent); + } + break; + } + + switch (Theme.of(context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.fuchsia: + case TargetPlatform.android: + break; + case TargetPlatform.macOS: + case TargetPlatform.linux: + case TargetPlatform.windows: + if (cause == SelectionChangedCause.drag) { + _editableText?.hideToolbar(); + } + break; + } + } + + /// Toggle the toolbar when a selection handle is tapped. + void _handleSelectionHandleTapped() { + if (_effectiveController.selection.isCollapsed) { + _editableText!.toggleToolbar(); + } + } + + void _handleHover(bool hovering) { + if (hovering != _isHovering) { + setState(() { + _isHovering = hovering; + }); + } + } MoonTextInputSizeProperties _getMoonTextInputSize(BuildContext context, MoonTextInputSize? moonTextInputSize) { switch (moonTextInputSize) { @@ -332,23 +974,136 @@ class _MoonTextInputState extends State { } } - void _setFocusStatus(bool isFocused) { - setState(() => _isFocused = isFocused); + // AutofillClient implementation start. + @override + String get autofillId => _editableText!.autofillId; + + @override + void autofill(TextEditingValue newEditingValue) => _editableText!.autofill(newEditingValue); + + @override + TextInputConfiguration get textInputConfiguration { + final List? autofillHints = widget.autofillHints?.toList(growable: false); + final AutofillConfiguration autofillConfiguration = autofillHints != null + ? AutofillConfiguration( + uniqueIdentifier: autofillId, + autofillHints: autofillHints, + currentEditingValue: _effectiveController.value, + hintText: widget.hintText, + ) + : AutofillConfiguration.disabled; + + return _editableText!.textInputConfiguration.copyWith(autofillConfiguration: autofillConfiguration); } + // AutofillClient implementation end. + + @override + String? get restorationId => widget.restorationId; - void _setHoverStatus(bool isHovered) { - setState(() => _isHovered = isHovered); + // API for TextSelectionGestureDetectorBuilderDelegate. + @override + late bool forcePressEnabled; + + @override + final GlobalKey editableTextKey = GlobalKey(); + + @override + bool get selectionEnabled => widget.selectionEnabled; + // End of API for TextSelectionGestureDetectorBuilderDelegate. + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (_controller != null) { + _registerController(); + } } - String? _validateInput(String? value) { - final validationResult = widget.validator?.call(value); - _errorText = validationResult; + @override + void initState() { + super.initState(); + _selectionGestureDetectorBuilder = _TextFieldSelectionGestureDetectorBuilder(state: this); + if (widget.controller == null) { + _createLocalController(); + } + _effectiveFocusNode.canRequestFocus = _isEnabled; + _effectiveFocusNode.addListener(_handleFocusChanged); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _effectiveFocusNode.canRequestFocus = _canRequestFocus; + } + + @override + void didUpdateWidget(MoonTextInput oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.controller == null && oldWidget.controller != null) { + _createLocalController(oldWidget.controller!.value); + } else if (widget.controller != null && oldWidget.controller == null) { + unregisterFromRestoration(_controller!); + _controller!.dispose(); + _controller = null; + } + + if (widget.focusNode != oldWidget.focusNode) { + (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); + (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); + } + + _effectiveFocusNode.canRequestFocus = _canRequestFocus; + + if (_effectiveFocusNode.hasFocus && widget.readOnly != oldWidget.readOnly && _isEnabled) { + if (_effectiveController.selection.isCollapsed) { + _showSelectionHandles = !widget.readOnly; + } + } + } - return validationResult; + @override + void dispose() { + _effectiveFocusNode.removeListener(_handleFocusChanged); + _focusNode?.dispose(); + _controller?.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + assert(debugCheckHasMaterialLocalizations(context)); + assert(debugCheckHasDirectionality(context)); + assert( + !(widget.textStyle != null && + widget.textStyle!.inherit == false && + (widget.textStyle!.fontSize == null || widget.textStyle!.textBaseline == null)), + 'inherit false textStyle must supply fontSize and textBaseline', + ); + + Color? autocorrectionTextRectColor; + Offset? cursorOffset; + Radius? cursorRadius = widget.cursorRadius; + TextSelectionControls? textSelectionControls = widget.selectionControls; + VoidCallback? handleDidGainAccessibilityFocus; + + final ThemeData theme = Theme.of(context); + + final bool cursorOpacityAnimates; + + final bool paintCursorAboveText; + + final Brightness keyboardAppearance = widget.keyboardAppearance ?? theme.brightness; + + final Color cursorColor; + + final Color selectionColor; + + final DefaultSelectionStyle selectionStyle = DefaultSelectionStyle.of(context); + + final FocusNode focusNode = _effectiveFocusNode; + + final TextEditingController controller = _effectiveController; + final MoonTextInputSizeProperties effectiveMoonTextInputSize = _getMoonTextInputSize(context, widget.textInputSize); final BorderRadiusGeometry effectiveBorderRadius = widget.borderRadius ?? effectiveMoonTextInputSize.borderRadius; @@ -401,184 +1156,412 @@ class _MoonTextInputState extends State { final EdgeInsetsGeometry effectivePadding = widget.padding ?? effectiveMoonTextInputSize.padding; - final EdgeInsetsGeometry effectiveSupportingPadding = widget.supportingPadding ?? - context.moonTheme?.textInputTheme.properties.supportingPadding ?? + final EdgeInsetsGeometry effectiveHelperPadding = widget.helperPadding ?? + context.moonTheme?.textInputTheme.properties.helperPadding ?? EdgeInsets.symmetric(horizontal: MoonSizes.sizes.x3s, vertical: MoonSizes.sizes.x4s); final EdgeInsets resolvedDirectionalPadding = effectivePadding.resolve(Directionality.of(context)); + final EdgeInsets correctedPadding = resolvedDirectionalPadding.copyWith( + left: widget.leading != null ? 0 : resolvedDirectionalPadding.left, + right: widget.trailing != null ? 0 : resolvedDirectionalPadding.right, + ); + final TextStyle effectiveTextStyle = widget.textStyle ?? effectiveMoonTextInputSize.textStyle; - final TextStyle effectiveSupportingTextStyle = widget.supportingTextStyle ?? - context.moonTheme?.textInputTheme.properties.supportingTextStyle ?? + final TextStyle effectiveHelperTextStyle = widget.helperTextStyle ?? + context.moonTheme?.textInputTheme.properties.helperTextStyle ?? const TextStyle(fontSize: 12); - final OutlineInputBorder defaultBorder = OutlineInputBorder( + final SmoothRectangleBorder defaultBorder = SmoothRectangleBorder( borderRadius: effectiveBorderRadius.smoothBorderRadius(context), - borderSide: BorderSide( - color: _isHovered ? effectiveHoverBorderColor : effectiveInactiveBorderColor, - width: _isHovered ? MoonBorders.borders.activeBorderWidth : MoonBorders.borders.defaultBorderWidth, + side: BorderSide( + color: _isHovering ? effectiveHoverBorderColor : effectiveInactiveBorderColor, + width: _isHovering ? MoonBorders.borders.activeBorderWidth : MoonBorders.borders.defaultBorderWidth, ), ); - final double resolvedLeadingWidth = widget.leading != null - ? effectiveMoonTextInputSize.iconSizeValue + resolvedDirectionalPadding.left + effectiveGap - : resolvedDirectionalPadding.left; - - final double resolvedTrailingWidth = widget.trailing != null - ? effectiveMoonTextInputSize.iconSizeValue + resolvedDirectionalPadding.right + effectiveGap - : resolvedDirectionalPadding.right; - - final OutlineInputBorder focusBorder = OutlineInputBorder( + final SmoothRectangleBorder focusBorder = SmoothRectangleBorder( borderRadius: effectiveBorderRadius.smoothBorderRadius(context), - borderSide: BorderSide( + side: BorderSide( color: effectiveActiveBorderColor, width: MoonBorders.borders.activeBorderWidth, ), ); - final OutlineInputBorder errorBorder = OutlineInputBorder( + final SmoothRectangleBorder errorBorder = SmoothRectangleBorder( borderRadius: effectiveBorderRadius.smoothBorderRadius(context), - borderSide: BorderSide( + side: BorderSide( color: effectiveErrorBorderColor, width: MoonBorders.borders.activeBorderWidth, ), ); - return Semantics( - label: widget.semanticLabel, - child: RepaintBoundary( - child: AnimatedOpacity( - opacity: widget.enabled ? 1.0 : effectiveDisabledOpacityValue, - curve: effectiveTransitionCurve, - duration: effectiveTransitionDuration, - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MoonFocusEffect( - show: _isFocused, - effectExtent: effectiveFocusEffectExtent, - effectColor: _errorText == null ? focusEffectColor : errorFocusEffectColor, - childBorderRadius: effectiveBorderRadius, - effectDuration: effectiveTransitionDuration, - effectCurve: effectiveTransitionCurve, - child: MouseRegion( - onEnter: (_) => _setHoverStatus(true), - onExit: (_) => _setHoverStatus(false), - child: Focus( - canRequestFocus: false, - onFocusChange: _setFocusStatus, - child: TextFormField( - autocorrect: widget.autocorrect, - autofillHints: widget.autofillHints, - autofocus: widget.autofocus, - autovalidateMode: widget.autovalidateMode, - controller: widget.controller, - cursorColor: effectiveTextColor, - enabled: widget.enabled, - enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, - enableInteractiveSelection: widget.enableInteractiveSelection, - enableSuggestions: widget.enableSuggestions, - focusNode: widget.focusNode, - initialValue: widget.initialValue, - inputFormatters: widget.inputFormatters, - keyboardAppearance: widget.keyboardAppearance, - onChanged: widget.onChanged, - onEditingComplete: widget.onEditingComplete, - onFieldSubmitted: widget.onSubmitted, - onSaved: widget.onSaved, - onTap: widget.onTap, - onTapOutside: widget.onTapOutside, - readOnly: widget.readOnly, - restorationId: widget.restorationId, - scrollController: widget.scrollController, - scrollPadding: widget.scrollPadding, - scrollPhysics: widget.scrollPhysics, - showCursor: widget.showCursor, - strutStyle: widget.strutStyle, - style: effectiveTextStyle.copyWith(color: effectiveTextColor), - textAlign: widget.textAlign, - textAlignVertical: TextAlignVertical.center, - textCapitalization: widget.textCapitalization, - textDirection: widget.textDirection, - textInputAction: widget.textInputAction, - validator: _validateInput, - decoration: InputDecoration( - /* labelText: widget.hintText, - labelStyle: effectiveTextStyle.copyWith(color: effectiveHintTextColor), - alignLabelWithHint: true, */ - filled: true, - isCollapsed: true, - fillColor: effectiveBackgroundColor, - focusColor: effectiveActiveBorderColor, - hoverColor: Colors.transparent, - border: defaultBorder, - enabledBorder: defaultBorder, - disabledBorder: defaultBorder, - focusedBorder: focusBorder, - errorBorder: errorBorder, - focusedErrorBorder: errorBorder, - constraints: BoxConstraints.tightFor(height: effectiveHeight), - hintText: widget.hintText, - hintStyle: effectiveTextStyle.copyWith(color: effectiveHintTextColor), - errorStyle: const TextStyle(height: 0.1, fontSize: 0), - prefixStyle: effectiveTextStyle.copyWith(color: effectiveTextColor), - prefixIconColor: effectiveTextColor, - prefixIconConstraints: BoxConstraints.tightFor(width: resolvedLeadingWidth), - prefixIcon: widget.leading != null - ? Padding( - padding: EdgeInsetsDirectional.only( - start: resolvedDirectionalPadding.left, - end: effectiveGap, - ), - child: widget.leading, - ) - : const SizedBox.expand(), - suffixStyle: effectiveTextStyle.copyWith(color: effectiveTextColor), - suffixIconColor: effectiveTextColor, - suffixIconConstraints: BoxConstraints.tightFor(width: resolvedTrailingWidth), - suffixIcon: widget.trailing != null - ? Padding( - padding: EdgeInsetsDirectional.only( - start: effectiveGap, - end: resolvedDirectionalPadding.right, - ), - child: widget.trailing, - ) - : const SizedBox.expand(), - ), + final SmoothRectangleBorder resolvedBorder = widget.errorText != null && !widget.readOnly + ? errorBorder + : _effectiveFocusNode.hasFocus && !widget.readOnly + ? focusBorder + : defaultBorder; + + final List formatters = [ + ...?widget.inputFormatters, + if (widget.maxLength != null) + LengthLimitingTextInputFormatter( + widget.maxLength, + maxLengthEnforcement: _effectiveMaxLengthEnforcement, + ), + ]; + + // Set configuration as disabled if not otherwise specified. If specified, + // ensure that configuration uses Material text textStyle for misspelled words + // unless a custom textStyle is specified. + final SpellCheckConfiguration spellCheckConfiguration = widget.spellCheckConfiguration != null && + widget.spellCheckConfiguration != const SpellCheckConfiguration.disabled() + ? widget.spellCheckConfiguration!.copyWith( + misspelledTextStyle: + widget.spellCheckConfiguration!.misspelledTextStyle ?? MoonTextInput.materialMisspelledTextStyle, + ) + : const SpellCheckConfiguration.disabled(); + + switch (theme.platform) { + case TargetPlatform.iOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + forcePressEnabled = true; + textSelectionControls ??= cupertinoTextSelectionControls; + paintCursorAboveText = true; + cursorOpacityAnimates = true; + cursorColor = widget.cursorColor ?? effectiveTextColor; + selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); + cursorRadius ??= const Radius.circular(2.0); + cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + autocorrectionTextRectColor = selectionColor; + break; + + case TargetPlatform.macOS: + final CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context); + forcePressEnabled = false; + textSelectionControls ??= cupertinoDesktopTextSelectionControls; + paintCursorAboveText = true; + cursorOpacityAnimates = false; + cursorColor = widget.cursorColor ?? effectiveTextColor; + selectionColor = selectionStyle.selectionColor ?? cupertinoTheme.primaryColor.withOpacity(0.40); + cursorRadius ??= const Radius.circular(2.0); + cursorOffset = Offset(iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0); + handleDidGainAccessibilityFocus = () { + // Automatically activate the MoonTextInput when it receives accessibility focus. + if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { + _effectiveFocusNode.requestFocus(); + } + }; + break; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + forcePressEnabled = false; + textSelectionControls ??= materialTextSelectionControls; + paintCursorAboveText = false; + cursorOpacityAnimates = false; + cursorColor = widget.cursorColor ?? effectiveTextColor; + selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); + break; + + case TargetPlatform.linux: + forcePressEnabled = false; + textSelectionControls ??= desktopTextSelectionControls; + paintCursorAboveText = false; + cursorOpacityAnimates = false; + cursorColor = widget.cursorColor ?? effectiveTextColor; + selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); + break; + + case TargetPlatform.windows: + forcePressEnabled = false; + textSelectionControls ??= desktopTextSelectionControls; + paintCursorAboveText = false; + cursorOpacityAnimates = false; + cursorColor = widget.cursorColor ?? effectiveTextColor; + selectionColor = selectionStyle.selectionColor ?? theme.colorScheme.primary.withOpacity(0.40); + handleDidGainAccessibilityFocus = () { + // Automatically activate the MoonTextInput when it receives accessibility focus. + if (!_effectiveFocusNode.hasFocus && _effectiveFocusNode.canRequestFocus) { + _effectiveFocusNode.requestFocus(); + } + }; + break; + } + + final MouseCursor effectiveMouseCursor = MaterialStateProperty.resolveAs( + widget.mouseCursor ?? MaterialStateMouseCursor.textable, + { + if (!_isEnabled) MaterialState.disabled, + if (_isHovering) MaterialState.hovered, + if (focusNode.hasFocus) MaterialState.focused, + if (_hasError) MaterialState.error, + }, + ); + + final int? semanticsMaxValueLength; + if (_effectiveMaxLengthEnforcement != MaxLengthEnforcement.none && + widget.maxLength != null && + widget.maxLength! > 0) { + semanticsMaxValueLength = widget.maxLength; + } else { + semanticsMaxValueLength = null; + } + + Widget child = RepaintBoundary( + child: UnmanagedRestorationScope( + bucket: bucket, + child: EditableText( + key: editableTextKey, + autocorrect: widget.autocorrect, + autocorrectionTextRectColor: autocorrectionTextRectColor, + autofillClient: this, + autofillHints: widget.autofillHints, + autofocus: widget.autofocus, + backgroundCursorColor: CupertinoColors.inactiveGray, + clipBehavior: widget.clipBehavior, + contextMenuBuilder: widget.contextMenuBuilder, + controller: controller, + cursorColor: cursorColor, + cursorHeight: widget.cursorHeight, + cursorOffset: cursorOffset, + cursorOpacityAnimates: cursorOpacityAnimates, + cursorRadius: cursorRadius, + cursorWidth: widget.cursorWidth, + dragStartBehavior: widget.dragStartBehavior, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + enableInteractiveSelection: widget.enableInteractiveSelection, + enableSuggestions: widget.enableSuggestions, + expands: widget.expands, + focusNode: focusNode, + inputFormatters: formatters, + keyboardAppearance: keyboardAppearance, + keyboardType: widget.keyboardType, + magnifierConfiguration: widget.magnifierConfiguration ?? TextMagnifier.adaptiveMagnifierConfiguration, + maxLines: widget.maxLines, + minLines: widget.minLines, + mouseCursor: MouseCursor.defer, // MoonTextInput will handle the cursor + obscureText: widget.obscureText, + obscuringCharacter: widget.obscuringCharacter, + onAppPrivateCommand: widget.onAppPrivateCommand, + onChanged: widget.onChanged, + onEditingComplete: widget.onEditingComplete, + onSelectionChanged: _handleSelectionChanged, + onSelectionHandleTapped: _handleSelectionHandleTapped, + onSubmitted: widget.onSubmitted, + onTapOutside: widget.onTapOutside, + paintCursorAboveText: paintCursorAboveText, + readOnly: widget.readOnly || !_isEnabled, + rendererIgnoresPointer: true, + restorationId: 'editable', + scribbleEnabled: widget.scribbleEnabled, + scrollController: widget.scrollController, + scrollPadding: widget.scrollPadding, + scrollPhysics: widget.scrollPhysics, + // Only show the selection highlight when the text input is focused. + selectionColor: focusNode.hasFocus ? selectionColor : null, + selectionControls: widget.selectionEnabled ? textSelectionControls : null, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + showCursor: widget.showCursor, + showSelectionHandles: _showSelectionHandles, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + spellCheckConfiguration: spellCheckConfiguration, + strutStyle: widget.strutStyle, + style: effectiveTextStyle.copyWith(color: effectiveTextColor), + textAlign: widget.textAlign, + textCapitalization: widget.textCapitalization, + textDirection: widget.textDirection, + textInputAction: widget.textInputAction, + ), + ), + ); + + child = AnimatedBuilder( + animation: Listenable.merge([focusNode, controller]), + builder: (BuildContext context, Widget? child) { + return InputDecorator( + baseStyle: widget.textStyle, + expands: widget.expands, + isEmpty: controller.value.text.isEmpty, + isFocused: focusNode.hasFocus, + isHovering: _isHovering, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + decoration: InputDecoration( + border: InputBorder.none, + contentPadding: correctedPadding, + floatingLabelAlignment: FloatingLabelAlignment.start, + floatingLabelBehavior: FloatingLabelBehavior.auto, + floatingLabelStyle: effectiveTextStyle.copyWith(color: effectiveHintTextColor), + hintStyle: effectiveTextStyle.copyWith(color: effectiveHintTextColor), + hintText: widget.hasFloatingLabel ? null : widget.hintText, + isDense: widget.isDense, + labelStyle: effectiveTextStyle.copyWith(color: effectiveHintTextColor), + labelText: widget.hasFloatingLabel ? widget.hintText : null, + prefixStyle: effectiveTextStyle.copyWith(color: effectiveTextColor), + prefixIconColor: effectiveTextColor, + prefixIconConstraints: BoxConstraints.tightFor(height: effectiveHeight), + prefixIcon: widget.leading != null + ? Padding( + padding: EdgeInsetsDirectional.only( + start: resolvedDirectionalPadding.left, + end: effectiveGap, ), - ), - ), + child: widget.leading, + ) + : null, + suffixStyle: effectiveTextStyle.copyWith(color: effectiveTextColor), + suffixIconColor: effectiveTextColor, + suffixIconConstraints: BoxConstraints.tightFor(height: effectiveHeight), + suffixIcon: widget.trailing != null + ? Padding( + padding: EdgeInsetsDirectional.only(start: effectiveGap, end: resolvedDirectionalPadding.right), + child: widget.trailing, + ) + : SizedBox(height: effectiveHeight), + ), + child: child, + ); + }, + child: child, + ); + + child = AnimatedOpacity( + opacity: widget.enabled ? 1.0 : effectiveDisabledOpacityValue, + curve: effectiveTransitionCurve, + duration: effectiveTransitionDuration, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + MoonFocusEffect( + show: widget.hasFocusEffect && _effectiveFocusNode.hasFocus && !widget.readOnly, + childBorderRadius: effectiveBorderRadius.smoothBorderRadius(context), + effectColor: widget.errorText == null ? focusEffectColor : errorFocusEffectColor, + effectExtent: effectiveFocusEffectExtent, + effectDuration: effectiveTransitionDuration, + effectCurve: effectiveTransitionCurve, + // FIXME: Make the borders animate properly with theme animation fix ticket in near future + child: Container( + height: widget.keyboardType == TextInputType.multiline && widget.height == null ? null : effectiveHeight, + decoration: ShapeDecoration( + color: effectiveBackgroundColor, + shape: resolvedBorder.copyWith(side: BorderSide.none), ), - if (widget.supporting != null || (_errorText != null && widget.errorBuilder != null)) - RepaintBoundary( - child: AnimatedIconTheme( - color: _errorText != null && widget.errorBuilder != null + foregroundDecoration: ShapeDecoration(shape: resolvedBorder), + child: child, + ), + ), + if (widget.helper != null || (widget.errorText != null && widget.errorBuilder != null)) + RepaintBoundary( + child: AnimatedIconTheme( + color: widget.errorText != null && widget.errorBuilder != null + ? effectiveErrorBorderColor + : effectiveHintTextColor, + duration: effectiveTransitionDuration, + child: AnimatedDefaultTextStyle( + style: effectiveHelperTextStyle.copyWith( + color: widget.errorText != null && widget.errorBuilder != null ? effectiveErrorBorderColor : effectiveHintTextColor, - duration: effectiveTransitionDuration, - child: AnimatedDefaultTextStyle( - style: effectiveSupportingTextStyle.copyWith( - color: _errorText != null && widget.errorBuilder != null - ? effectiveErrorBorderColor - : effectiveHintTextColor, - ), - duration: effectiveTransitionDuration, - child: Padding( - padding: effectiveSupportingPadding, - child: _errorText != null && widget.errorBuilder != null - ? widget.errorBuilder!(context, _errorText) - : widget.supporting, - ), - ), + ), + duration: effectiveTransitionDuration, + child: Padding( + padding: effectiveHelperPadding, + child: widget.errorText != null && widget.errorBuilder != null + ? widget.errorBuilder!(context, widget.errorText) + : widget.helper, ), ), - ], + ), + ), + ], + ), + ); + + return MouseRegion( + cursor: effectiveMouseCursor, + onEnter: (PointerEnterEvent event) => _handleHover(true), + onExit: (PointerExitEvent event) => _handleHover(false), + child: TextFieldTapRegion( + child: IgnorePointer( + ignoring: !_isEnabled, + child: AnimatedBuilder( + animation: controller, // changes the _currentLength + builder: (BuildContext context, Widget? child) { + return Semantics( + maxValueLength: semanticsMaxValueLength, + currentValueLength: _currentLength, + onTap: widget.readOnly + ? null + : () { + if (!_effectiveController.selection.isValid) { + _effectiveController.selection = + TextSelection.collapsed(offset: _effectiveController.text.length); + } + _requestKeyboard(); + }, + onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, + child: child, + ); + }, + child: _selectionGestureDetectorBuilder.buildGestureDetector( + behavior: HitTestBehavior.translucent, + child: child, + ), ), ), ), ); } } + +class _TextFieldSelectionGestureDetectorBuilder extends TextSelectionGestureDetectorBuilder { + _TextFieldSelectionGestureDetectorBuilder({ + required _MoonTextInputState state, + }) : _state = state, + super(delegate: state); + + final _MoonTextInputState _state; + + @override + void onForcePressStart(ForcePressDetails details) { + super.onForcePressStart(details); + if (delegate.selectionEnabled && shouldShowSelectionToolbar) { + editableText.showToolbar(); + } + } + + @override + void onForcePressEnd(ForcePressDetails details) { + // Not required. + } + + @override + void onSingleTapUp(TapUpDetails details) { + super.onSingleTapUp(details); + _state._requestKeyboard(); + _state.widget.onTap?.call(); + } + + @override + void onSingleLongTapStart(LongPressStartDetails details) { + super.onSingleLongTapStart(details); + if (delegate.selectionEnabled) { + switch (Theme.of(_state.context).platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + break; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + Feedback.forLongPress(_state.context); + break; + } + } + } +}