From d48517f33395f0759873c9559033816dead1119c Mon Sep 17 00:00:00 2001 From: Lucas Oliveira <62367544+tilucasoli@users.noreply.github.com> Date: Wed, 6 Nov 2024 14:24:13 -0300 Subject: [PATCH] feat: Create Textfield (#511) * create a wrapper for EditableText * remove all stylization and use spec instead * analyze and remove some non used parameters * checkpoint 1 * finish focusNode * implement autofill * onSelectionChanged * mouseCursor * inputFormatters and readOnly * MouseRegion * implement build method * Update textfield_widget.dart * Create style * create custom attributes * helper text * prefix and suffix widgets * floating label * float * adding Textfield into demo app * delete comments * fix demo app * fix lint issues --- .../lib/components/textfield_use_case.dart | 49 + .../remix/demo/lib/main.directories.g.dart | 17 +- packages/remix/lib/remix.dart | 26 +- .../lib/src/components/progress/progress.dart | 2 - .../textfield/attributes/attributes.dart | 24 + .../textfield/attributes/attributes.g.dart | 74 ++ .../src/components/textfield/textfield.dart | 132 +++ .../src/components/textfield/textfield.g.dart | 811 ++++++++++++++++ .../components/textfield/textfield_style.dart | 182 ++++ .../components/textfield/textfield_theme.dart | 62 ++ .../textfield/textfield_widget.dart | 863 ++++++++++++++++++ .../composited_transform_follower_spec.dart | 2 +- packages/remix/lib/src/theme/remix_theme.dart | 8 + 13 files changed, 2236 insertions(+), 16 deletions(-) create mode 100644 packages/remix/demo/lib/components/textfield_use_case.dart create mode 100644 packages/remix/lib/src/components/textfield/attributes/attributes.dart create mode 100644 packages/remix/lib/src/components/textfield/attributes/attributes.g.dart create mode 100644 packages/remix/lib/src/components/textfield/textfield.dart create mode 100644 packages/remix/lib/src/components/textfield/textfield.g.dart create mode 100644 packages/remix/lib/src/components/textfield/textfield_style.dart create mode 100644 packages/remix/lib/src/components/textfield/textfield_theme.dart create mode 100644 packages/remix/lib/src/components/textfield/textfield_widget.dart diff --git a/packages/remix/demo/lib/components/textfield_use_case.dart b/packages/remix/demo/lib/components/textfield_use_case.dart new file mode 100644 index 000000000..c37ebb3bd --- /dev/null +++ b/packages/remix/demo/lib/components/textfield_use_case.dart @@ -0,0 +1,49 @@ +import 'package:demo/addons/icon_data_knob.dart'; +import 'package:flutter/material.dart' as m; +import 'package:flutter/widgets.dart'; +import 'package:remix/remix.dart'; +import 'package:widgetbook/widgetbook.dart'; +import 'package:widgetbook_annotation/widgetbook_annotation.dart' as widgetbook; + +final _key = GlobalKey(); + +@widgetbook.UseCase( + name: 'TextField Component', + type: TextField, +) +Widget buildButtonUseCase(BuildContext context) { + final iconKnob = context.knobs.iconData(label: 'icons', initialValue: null); + return KeyedSubtree( + key: _key, + child: Scaffold( + body: Center( + child: SizedBox( + width: 300, + child: TextField( + suffix: context.knobs + .boolean(label: 'SuffixWidget', initialValue: false) + ? IconButton( + m.Icons.close_rounded, + variants: const [FortalezaIconButtonStyle.soft], + onPressed: () {}, + ) + : null, + prefixBuilder: iconKnob != null ? (spec) => spec(iconKnob) : null, + maxLines: context.knobs.int.input( + label: 'Max Lines', + initialValue: 1, + ), + hintText: context.knobs.string( + label: 'Hint Text', + initialValue: 'Hint Text', + ), + helperText: context.knobs.string( + label: 'Helper Text', + initialValue: 'Helper Text', + ), + ), + ), + ), + ), + ); +} diff --git a/packages/remix/demo/lib/main.directories.g.dart b/packages/remix/demo/lib/main.directories.g.dart index f1518b42b..db0b73003 100644 --- a/packages/remix/demo/lib/main.directories.g.dart +++ b/packages/remix/demo/lib/main.directories.g.dart @@ -28,7 +28,8 @@ import 'package:demo/components/select_use_case.dart' as _i17; import 'package:demo/components/slider.dart' as _i18; import 'package:demo/components/spinner_use_case.dart' as _i19; import 'package:demo/components/switch_use_case.dart' as _i20; -import 'package:demo/components/toast_use_case.dart' as _i21; +import 'package:demo/components/textfield_use_case.dart' as _i21; +import 'package:demo/components/toast_use_case.dart' as _i22; import 'package:widgetbook/widgetbook.dart' as _i1; final directories = <_i1.WidgetbookNode>[ @@ -263,6 +264,18 @@ final directories = <_i1.WidgetbookNode>[ ) ], ), + _i1.WidgetbookFolder( + name: 'textfield', + children: [ + _i1.WidgetbookLeafComponent( + name: 'TextField', + useCase: _i1.WidgetbookUseCase( + name: 'TextField Component', + builder: _i21.buildButtonUseCase, + ), + ) + ], + ), _i1.WidgetbookFolder( name: 'toast', children: [ @@ -270,7 +283,7 @@ final directories = <_i1.WidgetbookNode>[ name: 'Toast', useCase: _i1.WidgetbookUseCase( name: 'Toast Component', - builder: _i21.buildButtonUseCase, + builder: _i22.buildButtonUseCase, ), ) ], diff --git a/packages/remix/lib/remix.dart b/packages/remix/lib/remix.dart index c7287df13..9321a8b2d 100644 --- a/packages/remix/lib/remix.dart +++ b/packages/remix/lib/remix.dart @@ -1,15 +1,15 @@ -/// /\\\\ /\\\\ /\\\\\\\\\\\ /\\\ /\\\ -/// \/\\\\\\ /\\\\\\ \/////\\\/// \///\\\ /\\\/ -/// \/\\\//\\\ /\\\//\\\ \/\\\ \///\\\\\\/ -/// \/\\\\///\\\/\\\/ \/\\\ \/\\\ \//\\\\ -/// \/\\\ \///\\\/ \/\\\ \/\\\ \/\\\\ -/// \/\\\ \/// \/\\\ \/\\\ /\\\\\\ -/// \/\\\ \/\\\ \/\\\ /\\\////\\\ -/// \/\\\ \/\\\ /\\\\\\\\\\\ /\\\/ \///\\\ -/// \/// \/// \/////////// \/// \/// +/// /\\\\ /\\\\ /\\\\\\\\\\\ /\\\ /\\\ +/// \/\\\\\\ /\\\\\\ \/////\\\/// \///\\\ /\\\/ +/// \/\\\//\\\ /\\\//\\\ \/\\\ \///\\\\\\/ +/// \/\\\\///\\\/\\\/ \/\\\ \/\\\ \//\\\\ +/// \/\\\ \///\\\/ \/\\\ \/\\\ \/\\\\ +/// \/\\\ \/// \/\\\ \/\\\ /\\\\\\ +/// \/\\\ \/\\\ \/\\\ /\\\////\\\ +/// \/\\\ \/\\\ /\\\\\\\\\\\ /\\\/ \///\\\ +/// \/// \/// \/////////// \/// \/// +/// +/// https://fluttermix.com /// -/// https://fluttermix.com -/// /// /\///////////////////////////////////////////////////\ /// \/\ ***** GENERATED CODE ***** \ \ /// \/\ ** DO NOT EDIT THIS FILE ** \ \ @@ -20,6 +20,7 @@ library remix; /// APP export 'src/app/remix_app.dart'; + /// COMPONENTS export 'src/components/accordion/accordion.dart'; export 'src/components/avatar/avatar.dart'; @@ -43,6 +44,8 @@ export 'src/components/slider/slider.dart'; export 'src/components/spinner/spinner.dart'; export 'src/components/switch/switch.dart'; export 'src/components/toast/toast.dart'; +export 'src/components/textfield/textfield.dart'; + /// HELPERS export 'src/helpers/color_palette.dart'; export 'src/helpers/color_utils.dart'; @@ -50,6 +53,7 @@ export 'src/helpers/component_builder.dart'; export 'src/helpers/context_ext.dart'; export 'src/helpers/spec/composited_transform_follower_spec.dart'; export 'src/helpers/utility_extension.dart'; + /// THEME export 'src/theme/remix_theme.dart'; export 'src/theme/remix_tokens.dart'; diff --git a/packages/remix/lib/src/components/progress/progress.dart b/packages/remix/lib/src/components/progress/progress.dart index 0516bb4aa..4b6180356 100644 --- a/packages/remix/lib/src/components/progress/progress.dart +++ b/packages/remix/lib/src/components/progress/progress.dart @@ -11,8 +11,6 @@ part 'progress_style.dart'; part 'progress_theme.dart'; part 'progress_widget.dart'; -final $progress = ProgressSpecUtility.self; - @MixableSpec() base class ProgressSpec extends Spec with _$ProgressSpec, Diagnosticable { diff --git a/packages/remix/lib/src/components/textfield/attributes/attributes.dart b/packages/remix/lib/src/components/textfield/attributes/attributes.dart new file mode 100644 index 000000000..1c091bb2d --- /dev/null +++ b/packages/remix/lib/src/components/textfield/attributes/attributes.dart @@ -0,0 +1,24 @@ +import 'dart:ui'; + +import 'package:mix/mix.dart'; +import 'package:mix_annotations/mix_annotations.dart'; + +part 'attributes.g.dart'; + +@MixableEnumUtility() +final class BoxHeightStyleUtility + extends MixUtility with _$BoxHeightStyleUtility { + const BoxHeightStyleUtility(super.builder); +} + +@MixableEnumUtility() +final class BoxWidthStyleUtility + extends MixUtility with _$BoxWidthStyleUtility { + const BoxWidthStyleUtility(super.builder); +} + +@MixableEnumUtility() +final class BrightnessUtility + extends MixUtility with _$BrightnessUtility { + const BrightnessUtility(super.builder); +} diff --git a/packages/remix/lib/src/components/textfield/attributes/attributes.g.dart b/packages/remix/lib/src/components/textfield/attributes/attributes.g.dart new file mode 100644 index 000000000..69cf44a80 --- /dev/null +++ b/packages/remix/lib/src/components/textfield/attributes/attributes.g.dart @@ -0,0 +1,74 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'attributes.dart'; + +// ************************************************************************** +// MixableEnumUtilityGenerator +// ************************************************************************** + +/// {@template box_height_style_utility} +/// A utility class for creating [Attribute] instances from [BoxHeightStyle] values. +/// +/// This class extends [MixUtility] and provides methods to create [Attribute] instances +/// from predefined [BoxHeightStyle] values. +/// {@endtemplate} +mixin _$BoxHeightStyleUtility + on MixUtility { + /// Creates an [Attribute] instance with [BoxHeightStyle.tight] value. + T tight() => builder(BoxHeightStyle.tight); + + /// Creates an [Attribute] instance with [BoxHeightStyle.max] value. + T max() => builder(BoxHeightStyle.max); + + /// Creates an [Attribute] instance with [BoxHeightStyle.includeLineSpacingMiddle] value. + T includeLineSpacingMiddle() => + builder(BoxHeightStyle.includeLineSpacingMiddle); + + /// Creates an [Attribute] instance with [BoxHeightStyle.includeLineSpacingTop] value. + T includeLineSpacingTop() => builder(BoxHeightStyle.includeLineSpacingTop); + + /// Creates an [Attribute] instance with [BoxHeightStyle.includeLineSpacingBottom] value. + T includeLineSpacingBottom() => + builder(BoxHeightStyle.includeLineSpacingBottom); + + /// Creates an [Attribute] instance with [BoxHeightStyle.strut] value. + T strut() => builder(BoxHeightStyle.strut); + + /// Creates an [Attribute] instance with the specified BoxHeightStyle value. + T call(BoxHeightStyle value) => builder(value); +} + +/// {@template box_width_style_utility} +/// A utility class for creating [Attribute] instances from [BoxWidthStyle] values. +/// +/// This class extends [MixUtility] and provides methods to create [Attribute] instances +/// from predefined [BoxWidthStyle] values. +/// {@endtemplate} +mixin _$BoxWidthStyleUtility + on MixUtility { + /// Creates an [Attribute] instance with [BoxWidthStyle.tight] value. + T tight() => builder(BoxWidthStyle.tight); + + /// Creates an [Attribute] instance with [BoxWidthStyle.max] value. + T max() => builder(BoxWidthStyle.max); + + /// Creates an [Attribute] instance with the specified BoxWidthStyle value. + T call(BoxWidthStyle value) => builder(value); +} + +/// {@template brightness_utility} +/// A utility class for creating [Attribute] instances from [Brightness] values. +/// +/// This class extends [MixUtility] and provides methods to create [Attribute] instances +/// from predefined [Brightness] values. +/// {@endtemplate} +mixin _$BrightnessUtility on MixUtility { + /// Creates an [Attribute] instance with [Brightness.dark] value. + T dark() => builder(Brightness.dark); + + /// Creates an [Attribute] instance with [Brightness.light] value. + T light() => builder(Brightness.light); + + /// Creates an [Attribute] instance with the specified Brightness value. + T call(Brightness value) => builder(value); +} diff --git a/packages/remix/lib/src/components/textfield/textfield.dart b/packages/remix/lib/src/components/textfield/textfield.dart new file mode 100644 index 000000000..ec2a8ca93 --- /dev/null +++ b/packages/remix/lib/src/components/textfield/textfield.dart @@ -0,0 +1,132 @@ +import 'dart:ui'; + +import 'package:flutter/cupertino.dart' + show + CupertinoColors, + cupertinoDesktopTextSelectionHandleControls, + cupertinoTextSelectionHandleControls; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' as m; +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:mix/mix.dart'; +import 'package:mix_annotations/mix_annotations.dart'; + +import '../../helpers/component_builder.dart'; +import '../../theme/remix_theme.dart'; +import '../../theme/remix_tokens.dart'; +import 'attributes/attributes.dart'; + +part 'textfield.g.dart'; +part 'textfield_style.dart'; +part 'textfield_theme.dart'; +part 'textfield_widget.dart'; + +@MixableSpec() +class TextFieldSpec extends Spec + with _$TextFieldSpec, Diagnosticable { + final TextStyle style; + final TextAlign textAlign; + + final bool floatingLabel; + + final StrutStyle? strutStyle; + final TextWidthBasis textWidthBasis; + + final double cursorWidth; + final double? cursorHeight; + final Radius? cursorRadius; + final Color cursorColor; + final Offset cursorOffset; + final bool paintCursorAboveText; + final bool cursorOpacityAnimates; + final Color backgroundCursorColor; + final Color? selectionColor; + + final BoxHeightStyle selectionHeightStyle; + final BoxWidthStyle selectionWidthStyle; + + final EdgeInsets scrollPadding; + final Clip clipBehavior; + + final Brightness keyboardAppearance; + final Color? autocorrectionTextRectColor; + + final BoxSpec container; + final FlexSpec containerLayout; + final FlexSpec contentLayout; + final TextStyle? hintTextStyle; + final TextSpec helperText; + final IconSpec icon; + final double floatingLabelHeight; + final TextStyle? floatingLabelStyle; + + @MixableProperty(dto: MixableFieldDto(type: TextHeightBehaviorDto)) + final TextHeightBehavior? textHeightBehavior; + + static const of = _$TextFieldSpec.of; + + static const from = _$TextFieldSpec.from; + + const TextFieldSpec({ + TextStyle? style, + TextAlign? textAlign, + this.strutStyle, + this.textHeightBehavior, + TextWidthBasis? textWidthBasis, + double? cursorWidth, + this.cursorHeight, + this.cursorRadius, + Color? cursorColor, + Offset? cursorOffset, + bool? paintCursorAboveText, + Color? backgroundCursorColor, + this.selectionColor, + BoxHeightStyle? selectionHeightStyle, + BoxWidthStyle? selectionWidthStyle, + EdgeInsets? scrollPadding, + Clip? clipBehavior, + Brightness? keyboardAppearance, + this.autocorrectionTextRectColor, + bool? cursorOpacityAnimates, + BoxSpec? container, + FlexSpec? containerLayout, + this.hintTextStyle, + TextSpec? helperText, + IconSpec? icon, + FlexSpec? contentLayout, + bool? floatingLabel, + double? floatingLabelHeight, + this.floatingLabelStyle, + super.animated, + super.modifiers, + }) : style = style ?? const TextStyle(), + textAlign = textAlign ?? TextAlign.start, + textWidthBasis = textWidthBasis ?? TextWidthBasis.parent, + cursorWidth = cursorWidth ?? 2.0, + cursorColor = cursorColor ?? m.Colors.black54, + cursorOffset = cursorOffset ?? Offset.zero, + paintCursorAboveText = paintCursorAboveText ?? false, + cursorOpacityAnimates = cursorOpacityAnimates ?? false, + backgroundCursorColor = + backgroundCursorColor ?? CupertinoColors.inactiveGray, + selectionHeightStyle = selectionHeightStyle ?? BoxHeightStyle.tight, + selectionWidthStyle = selectionWidthStyle ?? BoxWidthStyle.tight, + scrollPadding = scrollPadding ?? const EdgeInsets.all(20.0), + clipBehavior = clipBehavior ?? Clip.hardEdge, + keyboardAppearance = keyboardAppearance ?? Brightness.light, + container = container ?? const BoxSpec(), + helperText = helperText ?? const TextSpec(), + containerLayout = containerLayout ?? const FlexSpec(), + icon = icon ?? const IconSpec(), + contentLayout = contentLayout ?? const FlexSpec(), + floatingLabel = floatingLabel ?? false, + floatingLabelHeight = floatingLabelHeight ?? 14; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + _debugFillProperties(properties); + } +} diff --git a/packages/remix/lib/src/components/textfield/textfield.g.dart b/packages/remix/lib/src/components/textfield/textfield.g.dart new file mode 100644 index 000000000..1449a1ec2 --- /dev/null +++ b/packages/remix/lib/src/components/textfield/textfield.g.dart @@ -0,0 +1,811 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'textfield.dart'; + +// ************************************************************************** +// MixableSpecGenerator +// ************************************************************************** + +mixin _$TextFieldSpec on Spec { + static TextFieldSpec from(MixData mix) { + return mix.attributeOf()?.resolve(mix) ?? + const TextFieldSpec(); + } + + /// {@template text_field_spec_of} + /// Retrieves the [TextFieldSpec] from the nearest [Mix] ancestor in the widget tree. + /// + /// This method uses [Mix.of] to obtain the [Mix] instance associated with the + /// given [BuildContext], and then retrieves the [TextFieldSpec] from that [Mix]. + /// If no ancestor [Mix] is found, this method returns an empty [TextFieldSpec]. + /// + /// Example: + /// + /// ```dart + /// final textFieldSpec = TextFieldSpec.of(context); + /// ``` + /// {@endtemplate} + static TextFieldSpec of(BuildContext context) { + return _$TextFieldSpec.from(Mix.of(context)); + } + + /// Creates a copy of this [TextFieldSpec] but with the given fields + /// replaced with the new values. + @override + TextFieldSpec copyWith({ + TextStyle? style, + TextAlign? textAlign, + StrutStyle? strutStyle, + TextHeightBehavior? textHeightBehavior, + TextWidthBasis? textWidthBasis, + double? cursorWidth, + double? cursorHeight, + Radius? cursorRadius, + Color? cursorColor, + Offset? cursorOffset, + bool? paintCursorAboveText, + Color? backgroundCursorColor, + Color? selectionColor, + BoxHeightStyle? selectionHeightStyle, + BoxWidthStyle? selectionWidthStyle, + EdgeInsets? scrollPadding, + Clip? clipBehavior, + Brightness? keyboardAppearance, + Color? autocorrectionTextRectColor, + bool? cursorOpacityAnimates, + BoxSpec? container, + FlexSpec? containerLayout, + TextStyle? hintTextStyle, + TextSpec? helperText, + IconSpec? icon, + FlexSpec? contentLayout, + bool? floatingLabel, + double? floatingLabelHeight, + TextStyle? floatingLabelStyle, + AnimatedData? animated, + WidgetModifiersData? modifiers, + }) { + return TextFieldSpec( + style: style ?? _$this.style, + textAlign: textAlign ?? _$this.textAlign, + strutStyle: strutStyle ?? _$this.strutStyle, + textHeightBehavior: textHeightBehavior ?? _$this.textHeightBehavior, + textWidthBasis: textWidthBasis ?? _$this.textWidthBasis, + cursorWidth: cursorWidth ?? _$this.cursorWidth, + cursorHeight: cursorHeight ?? _$this.cursorHeight, + cursorRadius: cursorRadius ?? _$this.cursorRadius, + cursorColor: cursorColor ?? _$this.cursorColor, + cursorOffset: cursorOffset ?? _$this.cursorOffset, + paintCursorAboveText: paintCursorAboveText ?? _$this.paintCursorAboveText, + backgroundCursorColor: + backgroundCursorColor ?? _$this.backgroundCursorColor, + selectionColor: selectionColor ?? _$this.selectionColor, + selectionHeightStyle: selectionHeightStyle ?? _$this.selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle ?? _$this.selectionWidthStyle, + scrollPadding: scrollPadding ?? _$this.scrollPadding, + clipBehavior: clipBehavior ?? _$this.clipBehavior, + keyboardAppearance: keyboardAppearance ?? _$this.keyboardAppearance, + autocorrectionTextRectColor: + autocorrectionTextRectColor ?? _$this.autocorrectionTextRectColor, + cursorOpacityAnimates: + cursorOpacityAnimates ?? _$this.cursorOpacityAnimates, + container: container ?? _$this.container, + containerLayout: containerLayout ?? _$this.containerLayout, + hintTextStyle: hintTextStyle ?? _$this.hintTextStyle, + helperText: helperText ?? _$this.helperText, + icon: icon ?? _$this.icon, + contentLayout: contentLayout ?? _$this.contentLayout, + floatingLabel: floatingLabel ?? _$this.floatingLabel, + floatingLabelHeight: floatingLabelHeight ?? _$this.floatingLabelHeight, + floatingLabelStyle: floatingLabelStyle ?? _$this.floatingLabelStyle, + animated: animated ?? _$this.animated, + modifiers: modifiers ?? _$this.modifiers, + ); + } + + /// Linearly interpolates between this [TextFieldSpec] and another [TextFieldSpec] based on the given parameter [t]. + /// + /// The parameter [t] represents the interpolation factor, typically ranging from 0.0 to 1.0. + /// When [t] is 0.0, the current [TextFieldSpec] is returned. When [t] is 1.0, the [other] [TextFieldSpec] is returned. + /// For values of [t] between 0.0 and 1.0, an interpolated [TextFieldSpec] is returned. + /// + /// If [other] is null, this method returns the current [TextFieldSpec] instance. + /// + /// The interpolation is performed on each property of the [TextFieldSpec] using the appropriate + /// interpolation method: + /// + /// - [MixHelpers.lerpTextStyle] for [style] and [hintTextStyle] and [floatingLabelStyle]. + /// - [MixHelpers.lerpStrutStyle] for [strutStyle]. + /// - [MixHelpers.lerpDouble] for [cursorWidth] and [cursorHeight] and [floatingLabelHeight]. + /// - [Radius.lerp] for [cursorRadius]. + /// - [Color.lerp] for [cursorColor] and [backgroundCursorColor] and [selectionColor] and [autocorrectionTextRectColor]. + /// - [Offset.lerp] for [cursorOffset]. + /// - [EdgeInsets.lerp] for [scrollPadding]. + /// - [BoxSpec.lerp] for [container]. + /// - [FlexSpec.lerp] for [containerLayout] and [contentLayout]. + /// - [TextSpec.lerp] for [helperText]. + /// - [IconSpec.lerp] for [icon]. + + /// For [textAlign] and [textHeightBehavior] and [textWidthBasis] and [paintCursorAboveText] and [selectionHeightStyle] and [selectionWidthStyle] and [clipBehavior] and [keyboardAppearance] and [cursorOpacityAnimates] and [floatingLabel] and [animated] and [modifiers], the interpolation is performed using a step function. + /// If [t] is less than 0.5, the value from the current [TextFieldSpec] is used. Otherwise, the value + /// from the [other] [TextFieldSpec] is used. + /// + /// This method is typically used in animations to smoothly transition between + /// different [TextFieldSpec] configurations. + @override + TextFieldSpec lerp(TextFieldSpec? other, double t) { + if (other == null) return _$this; + + return TextFieldSpec( + style: MixHelpers.lerpTextStyle(_$this.style, other.style, t)!, + textAlign: t < 0.5 ? _$this.textAlign : other.textAlign, + strutStyle: + MixHelpers.lerpStrutStyle(_$this.strutStyle, other.strutStyle, t), + textHeightBehavior: + t < 0.5 ? _$this.textHeightBehavior : other.textHeightBehavior, + textWidthBasis: t < 0.5 ? _$this.textWidthBasis : other.textWidthBasis, + cursorWidth: + MixHelpers.lerpDouble(_$this.cursorWidth, other.cursorWidth, t)!, + cursorHeight: + MixHelpers.lerpDouble(_$this.cursorHeight, other.cursorHeight, t), + cursorRadius: Radius.lerp(_$this.cursorRadius, other.cursorRadius, t), + cursorColor: Color.lerp(_$this.cursorColor, other.cursorColor, t)!, + cursorOffset: Offset.lerp(_$this.cursorOffset, other.cursorOffset, t)!, + paintCursorAboveText: + t < 0.5 ? _$this.paintCursorAboveText : other.paintCursorAboveText, + backgroundCursorColor: Color.lerp( + _$this.backgroundCursorColor, other.backgroundCursorColor, t)!, + selectionColor: + Color.lerp(_$this.selectionColor, other.selectionColor, t), + selectionHeightStyle: + t < 0.5 ? _$this.selectionHeightStyle : other.selectionHeightStyle, + selectionWidthStyle: + t < 0.5 ? _$this.selectionWidthStyle : other.selectionWidthStyle, + scrollPadding: + EdgeInsets.lerp(_$this.scrollPadding, other.scrollPadding, t)!, + clipBehavior: t < 0.5 ? _$this.clipBehavior : other.clipBehavior, + keyboardAppearance: + t < 0.5 ? _$this.keyboardAppearance : other.keyboardAppearance, + autocorrectionTextRectColor: Color.lerp( + _$this.autocorrectionTextRectColor, + other.autocorrectionTextRectColor, + t), + cursorOpacityAnimates: + t < 0.5 ? _$this.cursorOpacityAnimates : other.cursorOpacityAnimates, + container: _$this.container.lerp(other.container, t), + containerLayout: _$this.containerLayout.lerp(other.containerLayout, t), + hintTextStyle: MixHelpers.lerpTextStyle( + _$this.hintTextStyle, other.hintTextStyle, t), + helperText: _$this.helperText.lerp(other.helperText, t), + icon: _$this.icon.lerp(other.icon, t), + contentLayout: _$this.contentLayout.lerp(other.contentLayout, t), + floatingLabel: t < 0.5 ? _$this.floatingLabel : other.floatingLabel, + floatingLabelHeight: MixHelpers.lerpDouble( + _$this.floatingLabelHeight, other.floatingLabelHeight, t)!, + floatingLabelStyle: MixHelpers.lerpTextStyle( + _$this.floatingLabelStyle, other.floatingLabelStyle, t), + animated: t < 0.5 ? _$this.animated : other.animated, + modifiers: other.modifiers, + ); + } + + /// The list of properties that constitute the state of this [TextFieldSpec]. + /// + /// This property is used by the [==] operator and the [hashCode] getter to + /// compare two [TextFieldSpec] instances for equality. + @override + List get props => [ + _$this.style, + _$this.textAlign, + _$this.strutStyle, + _$this.textHeightBehavior, + _$this.textWidthBasis, + _$this.cursorWidth, + _$this.cursorHeight, + _$this.cursorRadius, + _$this.cursorColor, + _$this.cursorOffset, + _$this.paintCursorAboveText, + _$this.backgroundCursorColor, + _$this.selectionColor, + _$this.selectionHeightStyle, + _$this.selectionWidthStyle, + _$this.scrollPadding, + _$this.clipBehavior, + _$this.keyboardAppearance, + _$this.autocorrectionTextRectColor, + _$this.cursorOpacityAnimates, + _$this.container, + _$this.containerLayout, + _$this.hintTextStyle, + _$this.helperText, + _$this.icon, + _$this.contentLayout, + _$this.floatingLabel, + _$this.floatingLabelHeight, + _$this.floatingLabelStyle, + _$this.animated, + _$this.modifiers, + ]; + + TextFieldSpec get _$this => this as TextFieldSpec; + + void _debugFillProperties(DiagnosticPropertiesBuilder properties) { + properties + .add(DiagnosticsProperty('style', _$this.style, defaultValue: null)); + properties.add( + DiagnosticsProperty('textAlign', _$this.textAlign, defaultValue: null)); + properties.add(DiagnosticsProperty('strutStyle', _$this.strutStyle, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'textHeightBehavior', _$this.textHeightBehavior, + defaultValue: null)); + properties.add(DiagnosticsProperty('textWidthBasis', _$this.textWidthBasis, + defaultValue: null)); + properties.add(DiagnosticsProperty('cursorWidth', _$this.cursorWidth, + defaultValue: null)); + properties.add(DiagnosticsProperty('cursorHeight', _$this.cursorHeight, + defaultValue: null)); + properties.add(DiagnosticsProperty('cursorRadius', _$this.cursorRadius, + defaultValue: null)); + properties.add(DiagnosticsProperty('cursorColor', _$this.cursorColor, + defaultValue: null)); + properties.add(DiagnosticsProperty('cursorOffset', _$this.cursorOffset, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'paintCursorAboveText', _$this.paintCursorAboveText, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'backgroundCursorColor', _$this.backgroundCursorColor, + defaultValue: null)); + properties.add(DiagnosticsProperty('selectionColor', _$this.selectionColor, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'selectionHeightStyle', _$this.selectionHeightStyle, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'selectionWidthStyle', _$this.selectionWidthStyle, + defaultValue: null)); + properties.add(DiagnosticsProperty('scrollPadding', _$this.scrollPadding, + defaultValue: null)); + properties.add(DiagnosticsProperty('clipBehavior', _$this.clipBehavior, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'keyboardAppearance', _$this.keyboardAppearance, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'autocorrectionTextRectColor', _$this.autocorrectionTextRectColor, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'cursorOpacityAnimates', _$this.cursorOpacityAnimates, + defaultValue: null)); + properties.add( + DiagnosticsProperty('container', _$this.container, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'containerLayout', _$this.containerLayout, + defaultValue: null)); + properties.add(DiagnosticsProperty('hintTextStyle', _$this.hintTextStyle, + defaultValue: null)); + properties.add(DiagnosticsProperty('helperText', _$this.helperText, + defaultValue: null)); + properties + .add(DiagnosticsProperty('icon', _$this.icon, defaultValue: null)); + properties.add(DiagnosticsProperty('contentLayout', _$this.contentLayout, + defaultValue: null)); + properties.add(DiagnosticsProperty('floatingLabel', _$this.floatingLabel, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'floatingLabelHeight', _$this.floatingLabelHeight, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'floatingLabelStyle', _$this.floatingLabelStyle, + defaultValue: null)); + properties.add( + DiagnosticsProperty('animated', _$this.animated, defaultValue: null)); + properties.add( + DiagnosticsProperty('modifiers', _$this.modifiers, defaultValue: null)); + } +} + +/// Represents the attributes of a [TextFieldSpec]. +/// +/// This class encapsulates properties defining the layout and +/// appearance of a [TextFieldSpec]. +/// +/// Use this class to configure the attributes of a [TextFieldSpec] and pass it to +/// the [TextFieldSpec] constructor. +class TextFieldSpecAttribute extends SpecAttribute + with Diagnosticable { + final TextStyleDto? style; + final TextAlign? textAlign; + final StrutStyleDto? strutStyle; + final TextHeightBehaviorDto? textHeightBehavior; + final TextWidthBasis? textWidthBasis; + final double? cursorWidth; + final double? cursorHeight; + final Radius? cursorRadius; + final ColorDto? cursorColor; + final Offset? cursorOffset; + final bool? paintCursorAboveText; + final ColorDto? backgroundCursorColor; + final ColorDto? selectionColor; + final BoxHeightStyle? selectionHeightStyle; + final BoxWidthStyle? selectionWidthStyle; + final EdgeInsetsDto? scrollPadding; + final Clip? clipBehavior; + final Brightness? keyboardAppearance; + final ColorDto? autocorrectionTextRectColor; + final bool? cursorOpacityAnimates; + final BoxSpecAttribute? container; + final FlexSpecAttribute? containerLayout; + final TextStyleDto? hintTextStyle; + final TextSpecAttribute? helperText; + final IconSpecAttribute? icon; + final FlexSpecAttribute? contentLayout; + final bool? floatingLabel; + final double? floatingLabelHeight; + final TextStyleDto? floatingLabelStyle; + + const TextFieldSpecAttribute({ + this.style, + this.textAlign, + this.strutStyle, + this.textHeightBehavior, + this.textWidthBasis, + this.cursorWidth, + this.cursorHeight, + this.cursorRadius, + this.cursorColor, + this.cursorOffset, + this.paintCursorAboveText, + this.backgroundCursorColor, + this.selectionColor, + this.selectionHeightStyle, + this.selectionWidthStyle, + this.scrollPadding, + this.clipBehavior, + this.keyboardAppearance, + this.autocorrectionTextRectColor, + this.cursorOpacityAnimates, + this.container, + this.containerLayout, + this.hintTextStyle, + this.helperText, + this.icon, + this.contentLayout, + this.floatingLabel, + this.floatingLabelHeight, + this.floatingLabelStyle, + super.animated, + super.modifiers, + }); + + /// Resolves to [TextFieldSpec] using the provided [MixData]. + /// + /// If a property is null in the [MixData], it falls back to the + /// default value defined in the `defaultValue` for that property. + /// + /// ```dart + /// final textFieldSpec = TextFieldSpecAttribute(...).resolve(mix); + /// ``` + @override + TextFieldSpec resolve(MixData mix) { + return TextFieldSpec( + style: style?.resolve(mix), + textAlign: textAlign, + strutStyle: strutStyle?.resolve(mix), + textHeightBehavior: textHeightBehavior?.resolve(mix), + textWidthBasis: textWidthBasis, + cursorWidth: cursorWidth, + cursorHeight: cursorHeight, + cursorRadius: cursorRadius, + cursorColor: cursorColor?.resolve(mix), + cursorOffset: cursorOffset, + paintCursorAboveText: paintCursorAboveText, + backgroundCursorColor: backgroundCursorColor?.resolve(mix), + selectionColor: selectionColor?.resolve(mix), + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + scrollPadding: scrollPadding?.resolve(mix), + clipBehavior: clipBehavior, + keyboardAppearance: keyboardAppearance, + autocorrectionTextRectColor: autocorrectionTextRectColor?.resolve(mix), + cursorOpacityAnimates: cursorOpacityAnimates, + container: container?.resolve(mix), + containerLayout: containerLayout?.resolve(mix), + hintTextStyle: hintTextStyle?.resolve(mix), + helperText: helperText?.resolve(mix), + icon: icon?.resolve(mix), + contentLayout: contentLayout?.resolve(mix), + floatingLabel: floatingLabel, + floatingLabelHeight: floatingLabelHeight, + floatingLabelStyle: floatingLabelStyle?.resolve(mix), + animated: animated?.resolve(mix) ?? mix.animation, + modifiers: modifiers?.resolve(mix), + ); + } + + /// Merges the properties of this [TextFieldSpecAttribute] with the properties of [other]. + /// + /// If [other] is null, returns this instance unchanged. Otherwise, returns a new + /// [TextFieldSpecAttribute] with the properties of [other] taking precedence over + /// the corresponding properties of this instance. + /// + /// Properties from [other] that are null will fall back + /// to the values from this instance. + @override + TextFieldSpecAttribute merge(covariant TextFieldSpecAttribute? other) { + if (other == null) return this; + + return TextFieldSpecAttribute( + style: style?.merge(other.style) ?? other.style, + textAlign: other.textAlign ?? textAlign, + strutStyle: strutStyle?.merge(other.strutStyle) ?? other.strutStyle, + textHeightBehavior: textHeightBehavior?.merge(other.textHeightBehavior) ?? + other.textHeightBehavior, + textWidthBasis: other.textWidthBasis ?? textWidthBasis, + cursorWidth: other.cursorWidth ?? cursorWidth, + cursorHeight: other.cursorHeight ?? cursorHeight, + cursorRadius: other.cursorRadius ?? cursorRadius, + cursorColor: cursorColor?.merge(other.cursorColor) ?? other.cursorColor, + cursorOffset: other.cursorOffset ?? cursorOffset, + paintCursorAboveText: other.paintCursorAboveText ?? paintCursorAboveText, + backgroundCursorColor: + backgroundCursorColor?.merge(other.backgroundCursorColor) ?? + other.backgroundCursorColor, + selectionColor: + selectionColor?.merge(other.selectionColor) ?? other.selectionColor, + selectionHeightStyle: other.selectionHeightStyle ?? selectionHeightStyle, + selectionWidthStyle: other.selectionWidthStyle ?? selectionWidthStyle, + scrollPadding: + scrollPadding?.merge(other.scrollPadding) ?? other.scrollPadding, + clipBehavior: other.clipBehavior ?? clipBehavior, + keyboardAppearance: other.keyboardAppearance ?? keyboardAppearance, + autocorrectionTextRectColor: autocorrectionTextRectColor + ?.merge(other.autocorrectionTextRectColor) ?? + other.autocorrectionTextRectColor, + cursorOpacityAnimates: + other.cursorOpacityAnimates ?? cursorOpacityAnimates, + container: container?.merge(other.container) ?? other.container, + containerLayout: containerLayout?.merge(other.containerLayout) ?? + other.containerLayout, + hintTextStyle: + hintTextStyle?.merge(other.hintTextStyle) ?? other.hintTextStyle, + helperText: helperText?.merge(other.helperText) ?? other.helperText, + icon: icon?.merge(other.icon) ?? other.icon, + contentLayout: + contentLayout?.merge(other.contentLayout) ?? other.contentLayout, + floatingLabel: other.floatingLabel ?? floatingLabel, + floatingLabelHeight: other.floatingLabelHeight ?? floatingLabelHeight, + floatingLabelStyle: floatingLabelStyle?.merge(other.floatingLabelStyle) ?? + other.floatingLabelStyle, + animated: animated?.merge(other.animated) ?? other.animated, + modifiers: modifiers?.merge(other.modifiers) ?? other.modifiers, + ); + } + + /// The list of properties that constitute the state of this [TextFieldSpecAttribute]. + /// + /// This property is used by the [==] operator and the [hashCode] getter to + /// compare two [TextFieldSpecAttribute] instances for equality. + @override + List get props => [ + style, + textAlign, + strutStyle, + textHeightBehavior, + textWidthBasis, + cursorWidth, + cursorHeight, + cursorRadius, + cursorColor, + cursorOffset, + paintCursorAboveText, + backgroundCursorColor, + selectionColor, + selectionHeightStyle, + selectionWidthStyle, + scrollPadding, + clipBehavior, + keyboardAppearance, + autocorrectionTextRectColor, + cursorOpacityAnimates, + container, + containerLayout, + hintTextStyle, + helperText, + icon, + contentLayout, + floatingLabel, + floatingLabelHeight, + floatingLabelStyle, + animated, + modifiers, + ]; + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('style', style, + expandableValue: true, defaultValue: null)); + properties + .add(DiagnosticsProperty('textAlign', textAlign, defaultValue: null)); + properties + .add(DiagnosticsProperty('strutStyle', strutStyle, defaultValue: null)); + properties.add(DiagnosticsProperty('textHeightBehavior', textHeightBehavior, + defaultValue: null)); + properties.add(DiagnosticsProperty('textWidthBasis', textWidthBasis, + defaultValue: null)); + properties.add( + DiagnosticsProperty('cursorWidth', cursorWidth, defaultValue: null)); + properties.add( + DiagnosticsProperty('cursorHeight', cursorHeight, defaultValue: null)); + properties.add( + DiagnosticsProperty('cursorRadius', cursorRadius, defaultValue: null)); + properties.add( + DiagnosticsProperty('cursorColor', cursorColor, defaultValue: null)); + properties.add( + DiagnosticsProperty('cursorOffset', cursorOffset, defaultValue: null)); + properties.add(DiagnosticsProperty( + 'paintCursorAboveText', paintCursorAboveText, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'backgroundCursorColor', backgroundCursorColor, + defaultValue: null)); + properties.add(DiagnosticsProperty('selectionColor', selectionColor, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'selectionHeightStyle', selectionHeightStyle, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'selectionWidthStyle', selectionWidthStyle, + defaultValue: null)); + properties.add(DiagnosticsProperty('scrollPadding', scrollPadding, + defaultValue: null)); + properties.add( + DiagnosticsProperty('clipBehavior', clipBehavior, defaultValue: null)); + properties.add(DiagnosticsProperty('keyboardAppearance', keyboardAppearance, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'autocorrectionTextRectColor', autocorrectionTextRectColor, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'cursorOpacityAnimates', cursorOpacityAnimates, + defaultValue: null)); + properties + .add(DiagnosticsProperty('container', container, defaultValue: null)); + properties.add(DiagnosticsProperty('containerLayout', containerLayout, + defaultValue: null)); + properties.add(DiagnosticsProperty('hintTextStyle', hintTextStyle, + defaultValue: null)); + properties + .add(DiagnosticsProperty('helperText', helperText, defaultValue: null)); + properties.add(DiagnosticsProperty('icon', icon, defaultValue: null)); + properties.add(DiagnosticsProperty('contentLayout', contentLayout, + defaultValue: null)); + properties.add(DiagnosticsProperty('floatingLabel', floatingLabel, + defaultValue: null)); + properties.add(DiagnosticsProperty( + 'floatingLabelHeight', floatingLabelHeight, + defaultValue: null)); + properties.add(DiagnosticsProperty('floatingLabelStyle', floatingLabelStyle, + defaultValue: null)); + properties + .add(DiagnosticsProperty('animated', animated, defaultValue: null)); + properties + .add(DiagnosticsProperty('modifiers', modifiers, defaultValue: null)); + } +} + +/// Utility class for configuring [TextFieldSpec] properties. +/// +/// This class provides methods to set individual properties of a [TextFieldSpec]. +/// Use the methods of this class to configure specific properties of a [TextFieldSpec]. +class TextFieldSpecUtility + extends SpecUtility { + /// Utility for defining [TextFieldSpecAttribute.style] + late final style = TextStyleUtility((v) => only(style: v)); + + /// Utility for defining [TextFieldSpecAttribute.textAlign] + late final textAlign = TextAlignUtility((v) => only(textAlign: v)); + + /// Utility for defining [TextFieldSpecAttribute.strutStyle] + late final strutStyle = StrutStyleUtility((v) => only(strutStyle: v)); + + /// Utility for defining [TextFieldSpecAttribute.textHeightBehavior] + late final textHeightBehavior = + TextHeightBehaviorUtility((v) => only(textHeightBehavior: v)); + + /// Utility for defining [TextFieldSpecAttribute.textWidthBasis] + late final textWidthBasis = + TextWidthBasisUtility((v) => only(textWidthBasis: v)); + + /// Utility for defining [TextFieldSpecAttribute.cursorWidth] + late final cursorWidth = DoubleUtility((v) => only(cursorWidth: v)); + + /// Utility for defining [TextFieldSpecAttribute.cursorHeight] + late final cursorHeight = DoubleUtility((v) => only(cursorHeight: v)); + + /// Utility for defining [TextFieldSpecAttribute.cursorRadius] + late final cursorRadius = RadiusUtility((v) => only(cursorRadius: v)); + + /// Utility for defining [TextFieldSpecAttribute.cursorColor] + late final cursorColor = ColorUtility((v) => only(cursorColor: v)); + + /// Utility for defining [TextFieldSpecAttribute.cursorOffset] + late final cursorOffset = OffsetUtility((v) => only(cursorOffset: v)); + + /// Utility for defining [TextFieldSpecAttribute.paintCursorAboveText] + late final paintCursorAboveText = + BoolUtility((v) => only(paintCursorAboveText: v)); + + /// Utility for defining [TextFieldSpecAttribute.backgroundCursorColor] + late final backgroundCursorColor = + ColorUtility((v) => only(backgroundCursorColor: v)); + + /// Utility for defining [TextFieldSpecAttribute.selectionColor] + late final selectionColor = ColorUtility((v) => only(selectionColor: v)); + + /// Utility for defining [TextFieldSpecAttribute.selectionHeightStyle] + late final selectionHeightStyle = + BoxHeightStyleUtility((v) => only(selectionHeightStyle: v)); + + /// Utility for defining [TextFieldSpecAttribute.selectionWidthStyle] + late final selectionWidthStyle = + BoxWidthStyleUtility((v) => only(selectionWidthStyle: v)); + + /// Utility for defining [TextFieldSpecAttribute.scrollPadding] + late final scrollPadding = EdgeInsetsUtility((v) => only(scrollPadding: v)); + + /// Utility for defining [TextFieldSpecAttribute.clipBehavior] + late final clipBehavior = ClipUtility((v) => only(clipBehavior: v)); + + /// Utility for defining [TextFieldSpecAttribute.keyboardAppearance] + late final keyboardAppearance = + BrightnessUtility((v) => only(keyboardAppearance: v)); + + /// Utility for defining [TextFieldSpecAttribute.autocorrectionTextRectColor] + late final autocorrectionTextRectColor = + ColorUtility((v) => only(autocorrectionTextRectColor: v)); + + /// Utility for defining [TextFieldSpecAttribute.cursorOpacityAnimates] + late final cursorOpacityAnimates = + BoolUtility((v) => only(cursorOpacityAnimates: v)); + + /// Utility for defining [TextFieldSpecAttribute.container] + late final container = BoxSpecUtility((v) => only(container: v)); + + /// Utility for defining [TextFieldSpecAttribute.containerLayout] + late final containerLayout = FlexSpecUtility((v) => only(containerLayout: v)); + + /// Utility for defining [TextFieldSpecAttribute.hintTextStyle] + late final hintTextStyle = TextStyleUtility((v) => only(hintTextStyle: v)); + + /// Utility for defining [TextFieldSpecAttribute.helperText] + late final helperText = TextSpecUtility((v) => only(helperText: v)); + + /// Utility for defining [TextFieldSpecAttribute.icon] + late final icon = IconSpecUtility((v) => only(icon: v)); + + /// Utility for defining [TextFieldSpecAttribute.contentLayout] + late final contentLayout = FlexSpecUtility((v) => only(contentLayout: v)); + + /// Utility for defining [TextFieldSpecAttribute.floatingLabel] + late final floatingLabel = BoolUtility((v) => only(floatingLabel: v)); + + /// Utility for defining [TextFieldSpecAttribute.floatingLabelHeight] + late final floatingLabelHeight = + DoubleUtility((v) => only(floatingLabelHeight: v)); + + /// Utility for defining [TextFieldSpecAttribute.floatingLabelStyle] + late final floatingLabelStyle = + TextStyleUtility((v) => only(floatingLabelStyle: v)); + + /// Utility for defining [TextFieldSpecAttribute.animated] + late final animated = AnimatedUtility((v) => only(animated: v)); + + /// Utility for defining [TextFieldSpecAttribute.modifiers] + late final wrap = SpecModifierUtility((v) => only(modifiers: v)); + + TextFieldSpecUtility(super.builder, {super.mutable}); + + TextFieldSpecUtility get chain => + TextFieldSpecUtility(attributeBuilder, mutable: true); + + static TextFieldSpecUtility get self => + TextFieldSpecUtility((v) => v); + + /// Returns a new [TextFieldSpecAttribute] with the specified properties. + @override + T only({ + TextStyleDto? style, + TextAlign? textAlign, + StrutStyleDto? strutStyle, + TextHeightBehaviorDto? textHeightBehavior, + TextWidthBasis? textWidthBasis, + double? cursorWidth, + double? cursorHeight, + Radius? cursorRadius, + ColorDto? cursorColor, + Offset? cursorOffset, + bool? paintCursorAboveText, + ColorDto? backgroundCursorColor, + ColorDto? selectionColor, + BoxHeightStyle? selectionHeightStyle, + BoxWidthStyle? selectionWidthStyle, + EdgeInsetsDto? scrollPadding, + Clip? clipBehavior, + Brightness? keyboardAppearance, + ColorDto? autocorrectionTextRectColor, + bool? cursorOpacityAnimates, + BoxSpecAttribute? container, + FlexSpecAttribute? containerLayout, + TextStyleDto? hintTextStyle, + TextSpecAttribute? helperText, + IconSpecAttribute? icon, + FlexSpecAttribute? contentLayout, + bool? floatingLabel, + double? floatingLabelHeight, + TextStyleDto? floatingLabelStyle, + AnimatedDataDto? animated, + WidgetModifiersDataDto? modifiers, + }) { + return builder(TextFieldSpecAttribute( + style: style, + textAlign: textAlign, + strutStyle: strutStyle, + textHeightBehavior: textHeightBehavior, + textWidthBasis: textWidthBasis, + cursorWidth: cursorWidth, + cursorHeight: cursorHeight, + cursorRadius: cursorRadius, + cursorColor: cursorColor, + cursorOffset: cursorOffset, + paintCursorAboveText: paintCursorAboveText, + backgroundCursorColor: backgroundCursorColor, + selectionColor: selectionColor, + selectionHeightStyle: selectionHeightStyle, + selectionWidthStyle: selectionWidthStyle, + scrollPadding: scrollPadding, + clipBehavior: clipBehavior, + keyboardAppearance: keyboardAppearance, + autocorrectionTextRectColor: autocorrectionTextRectColor, + cursorOpacityAnimates: cursorOpacityAnimates, + container: container, + containerLayout: containerLayout, + hintTextStyle: hintTextStyle, + helperText: helperText, + icon: icon, + contentLayout: contentLayout, + floatingLabel: floatingLabel, + floatingLabelHeight: floatingLabelHeight, + floatingLabelStyle: floatingLabelStyle, + animated: animated, + modifiers: modifiers, + )); + } +} + +/// A tween that interpolates between two [TextFieldSpec] instances. +/// +/// This class can be used in animations to smoothly transition between +/// different [TextFieldSpec] specifications. +class TextFieldSpecTween extends Tween { + TextFieldSpecTween({ + super.begin, + super.end, + }); + + @override + TextFieldSpec lerp(double t) { + if (begin == null && end == null) { + return const TextFieldSpec(); + } + + if (begin == null) { + return end!; + } + + return begin!.lerp(end!, t); + } +} diff --git a/packages/remix/lib/src/components/textfield/textfield_style.dart b/packages/remix/lib/src/components/textfield/textfield_style.dart new file mode 100644 index 000000000..837b72e26 --- /dev/null +++ b/packages/remix/lib/src/components/textfield/textfield_style.dart @@ -0,0 +1,182 @@ +part of 'textfield.dart'; + +class IsEmptyContextVariant extends ContextVariant { + const IsEmptyContextVariant(); + + @override + bool when(BuildContext context) => + context + .dependOnInheritedWidgetOfExactType<_TextFieldContext>() + ?.isEmpty ?? + false; +} + +class TextFieldSpecConfiguration + extends SpecConfiguration { + const TextFieldSpecConfiguration(super.context, super._utility); + + @override + TextFieldContextVariantUtil get on => TextFieldContextVariantUtil(super.on); +} + +extension type const TextFieldContextVariantUtil( + OnContextVariantUtility _utility) implements OnContextVariantUtility { + ContextVariant get isEmpty => const IsEmptyContextVariant(); + ContextVariant get isNotEmpty => const OnNotVariant(IsEmptyContextVariant()); +} + +class TextFieldStyle extends SpecStyle { + const TextFieldStyle(); + Style platformSettings(SpecConfiguration spec) { + final $ = spec.utilities; + + final iOS = spec.on.ios( + $.chain + ..paintCursorAboveText.on() + ..cursorOpacityAnimates.on() + ..cursorRadius(2) + ..cursorOffset( + m.iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(spec.context), + 0, + ), + ); + + final androidAndFuchsia = (spec.on.android | spec.on.fuchsia)( + $.chain + ..paintCursorAboveText.off() + ..cursorOpacityAnimates.off(), + ); + + final macos = spec.on.macos( + $.chain + ..paintCursorAboveText.on() + ..cursorOpacityAnimates.off() + ..cursorRadius(2) + ..cursorOffset( + m.iOSHorizontalOffset / MediaQuery.devicePixelRatioOf(spec.context), + 0, + ), + ); + + final windows = spec.on.windows( + $.chain + ..paintCursorAboveText.off() + ..cursorOpacityAnimates.off(), + ); + + final linux = spec.on.linux( + $.chain + ..paintCursorAboveText.off() + ..cursorOpacityAnimates.off(), + ); + + return Style(androidAndFuchsia, iOS, macos, windows, linux); + } + + @override + Style makeStyle(SpecConfiguration spec) { + spec as TextFieldSpecConfiguration; + + final $ = spec.utilities; + + final containerStyle = $.container.chain + ..color.white() + ..padding.horizontal(12) + ..padding.vertical(8) + ..borderRadius(6) + ..border.all.color.grey.shade300() + ..shadow.spreadRadius(0) + ..shadow.blurRadius(2) + ..shadow.offset(-1, 1) + ..shadow.color.grey.shade200(); + + final layoutStyle = $.containerLayout.chain + ..direction.vertical() + ..mainAxisSize.min() + ..mainAxisAlignment.start() + ..crossAxisAlignment.start() + ..gap(6); + + final contentLayoutStyle = $.contentLayout.chain + ..direction.horizontal() + ..mainAxisSize.min() + ..mainAxisAlignment.start() + ..crossAxisAlignment.center() + ..gap(8); + + final textStyle = $.chain + ..style.color.black87() + ..style.fontSize(14); + + final hintStyle = [ + $.hintTextStyle.color.black54(), + $.hintTextStyle.fontSize(14), + ]; + + final icon = $.icon.chain + ..color.grey.shade800() + ..size(18); + + final helperStyle = $.helperText.chain + ..style.color.black54() + ..style.fontSize(12) + ..wrap.padding.left(12); + + final focus = spec.on.focus($.container.border.all.color.black()); + + return Style.create([ + platformSettings(spec).call(), + containerStyle, + $.floatingLabel.off(), + $.selectionColor.black12(), + contentLayoutStyle, + layoutStyle, + textStyle, + helperStyle, + ...hintStyle, + icon, + focus, + ]); + } +} + +class TextFieldDarkStyle extends TextFieldStyle { + const TextFieldDarkStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + final cursor = $.cursorColor.grey.shade100(); + + final containerStyle = $.container.chain + ..color.black() + ..border.all.color.grey.shade800() + ..shadow.spreadRadius(0) + ..shadow.blurRadius(0) + ..shadow.offset(0, 0) + ..shadow.color.transparent(); + + final textStyle = $.style.color.white(); + + final hintStyle = [ + $.hintTextStyle.color.grey.shade400(), + $.hintTextStyle.fontSize(14), + ]; + + final helperStyle = $.helperText.style.color.grey.shade400(); + final icon = $.icon.color.grey.shade300(); + + final focus = spec.on.focus($.container.border.all.color.white()); + + return Style.create([ + super.makeStyle(spec).call(), + cursor, + containerStyle, + focus, + textStyle, + helperStyle, + icon, + ...hintStyle, + ]); + } +} diff --git a/packages/remix/lib/src/components/textfield/textfield_theme.dart b/packages/remix/lib/src/components/textfield/textfield_theme.dart new file mode 100644 index 000000000..f76a54ea6 --- /dev/null +++ b/packages/remix/lib/src/components/textfield/textfield_theme.dart @@ -0,0 +1,62 @@ +part of 'textfield.dart'; + +class FortalezaTextFieldStyle extends TextFieldStyle { + const FortalezaTextFieldStyle(); + + @override + Style makeStyle(SpecConfiguration spec) { + final $ = spec.utilities; + + final containerStyle = $.container.chain + ..color.$neutral(1) + ..padding.horizontal.$space(3) + ..padding.vertical.$space(2) + ..borderRadius.all.$radius(2) + ..border.all.color.$neutral(6) + ..border.all.strokeAlign.outside() + ..shadow.color.$neutral(4); + + final textStyle = [$.style.$text(2), $.style.color.$neutral(12)]; + + final layoutStyle = $.containerLayout.gap.$space(2); + + final contentLayoutStyle = $.contentLayout.gap.$space(2); + + final hintStyle = [ + $.hintTextStyle.color.$neutral(9), + $.hintTextStyle.$text(2), + ]; + + final floatingHintStyle = [ + $.floatingLabelStyle.color.$neutral(9), + $.floatingLabelStyle.$text(1), + ]; + + final helperStyle = [ + $.helperText.style.color.$neutral(9), + $.helperText.style.$text(1), + ]; + final icon = $.icon.color.$accent(); + + final focus = spec.on.focus( + $.container.chain + ..border.all.color.$accent() + ..border.all.width(2), + ); + + return Style.create([ + super.makeStyle(spec).call(), + $.floatingLabel.on(), + $.cursorColor.$neutral(12), + containerStyle, + layoutStyle, + ...textStyle, + ...hintStyle, + ...floatingHintStyle, + contentLayoutStyle, + ...helperStyle, + icon, + focus, + ]).animate(); + } +} diff --git a/packages/remix/lib/src/components/textfield/textfield_widget.dart b/packages/remix/lib/src/components/textfield/textfield_widget.dart new file mode 100644 index 000000000..86fced8ad --- /dev/null +++ b/packages/remix/lib/src/components/textfield/textfield_widget.dart @@ -0,0 +1,863 @@ +part of 'textfield.dart'; + +class TextField extends StatefulWidget { + const TextField({ + super.key, + this.controller, + this.maxLength, + this.focusNode, + this.enabled = true, + this.ignorePointers, + this.onTap, + this.maxLengthEnforcement, + TextInputType? keyboardType, + this.textCapitalization = TextCapitalization.none, + this.textInputAction, + this.textDirection, + this.autofocus = false, + this.readOnly = false, + this.showCursor, + this.obscuringCharacter = '•', + this.obscureText = false, + this.hintText, + this.autocorrect = true, + SmartDashesType? smartDashesType, + SmartQuotesType? smartQuotesType, + this.enableSuggestions = true, + this.maxLines = 1, + this.minLines, + this.expands = false, + this.onChanged, + this.onEditingComplete, + this.onSubmitted, + this.inputFormatters, + bool? enableInteractiveSelection, + this.selectionControls, + this.scrollController, + this.scrollPhysics, + this.clipBehavior = Clip.hardEdge, + this.restorationId, + this.scribbleEnabled = true, + this.enableIMEPersonalizedLearning = true, + this.showSelectionHandles = false, + this.autofillClient, + this.autofillHints = const [], + this.contentInsertionConfiguration, + this.dragStartBehavior = DragStartBehavior.start, + this.groupId = EditableText, + this.onAppPrivateCommand, + this.onTapOutside, + this.canRequestFocus = true, + this.onTapAlwaysCalled = false, + this.undoController, + this.magnifierConfiguration, + this.spellCheckConfiguration, + this.contextMenuBuilder, + this.style, + this.variants = const [], + this.error = false, + this.label, + this.helperText, + this.prefixBuilder, + this.suffix, + }) : 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 == TextField.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 TextField.', + ), + keyboardType = keyboardType ?? + (maxLines == 1 ? TextInputType.text : TextInputType.multiline), + enableInteractiveSelection = + enableInteractiveSelection ?? (!readOnly || !obscureText); + + /// If [maxLength] is set to this value, only the "current input length" + /// part of the character counter is shown. + static const int noMaxLength = -1; + + final TextEditingController? controller; + final FocusNode? focusNode; + final TextInputType? keyboardType; + final TextCapitalization textCapitalization; + final TextInputAction? textInputAction; + final TextDirection? textDirection; + final bool enabled; + final bool autofocus; + final bool readOnly; + final bool? showCursor; + final String obscuringCharacter; + final String? label; + final bool obscureText; + final bool autocorrect; + final SmartDashesType? smartDashesType; + final SmartQuotesType? smartQuotesType; + final bool enableSuggestions; + final int? maxLines; + final int? minLines; + final bool expands; + final ValueChanged? onChanged; + final VoidCallback? onEditingComplete; + final ValueChanged? onSubmitted; + final List? inputFormatters; + final bool error; + + /// Determines whether this widget ignores pointer events. + /// + /// Defaults to null, and when null, does nothing. + final bool? ignorePointers; + + /// Determines how the [maxLength] limit should be enforced. + /// + /// {@macro flutter.services.textFormatter.effectiveMaxLengthEnforcement} + /// + /// {@macro flutter.services.textFormatter.maxLengthEnforcement} + final MaxLengthEnforcement? maxLengthEnforcement; + + /// The maximum number of characters (Unicode grapheme clusters) to allow in + /// the text field. + /// + /// 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 [TextField.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 field enforces the length with a [LengthLimitingTextInputFormatter], + /// which is evaluated after the supplied [inputFormatters], if any. + /// + /// This value must be either null, [TextField.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 [TextField.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; + + final String? hintText; + final String? helperText; + final WidgetSpecBuilder? prefixBuilder; + final Widget? suffix; + + /// Determine whether this text field can request the primary focus. + /// + /// Defaults to true. If false, the text field will not request focus + /// when tapped, or when its context menu is displayed. If false it will not + /// be possible to move the focus to the text field with tab key. + final bool canRequestFocus; + + /// Whether [onTap] should be called for every tap. + /// + /// Defaults to false, so [onTap] is only called for each distinct tap. When + /// enabled, [onTap] is called for every tap including consecutive taps. + final bool onTapAlwaysCalled; + + /// {@macro flutter.material.textfield.onTap} + /// If [onTapAlwaysCalled] is enabled, this will also be called for consecutive + /// taps. + final GestureTapCallback? onTap; + + final bool enableInteractiveSelection; + final TextSelectionControls? selectionControls; + final ScrollController? scrollController; + final ScrollPhysics? scrollPhysics; + final Clip clipBehavior; + final String? restorationId; + final bool scribbleEnabled; + final bool enableIMEPersonalizedLearning; + + final bool showSelectionHandles; + + final AutofillClient? autofillClient; + final Iterable? autofillHints; + final ContentInsertionConfiguration? contentInsertionConfiguration; + + final DragStartBehavior dragStartBehavior; + final Object groupId; + final AppPrivateCommandCallback? onAppPrivateCommand; + + final TapRegionCallback? onTapOutside; + final TextMagnifierConfiguration? magnifierConfiguration; + final SpellCheckConfiguration? spellCheckConfiguration; + + final UndoHistoryController? undoController; + final TextFieldStyle? style; + final List variants; + + final Widget Function(BuildContext, EditableTextState)? contextMenuBuilder; + + /// {@macro flutter.widgets.editableText.selectionEnabled} + bool get selectionEnabled => enableInteractiveSelection; + + @override + State createState() => _TextFieldState(); +} + +class _TextFieldState extends State + with RestorationMixin + implements TextSelectionGestureDetectorBuilderDelegate, AutofillClient { + late MixWidgetStateController _statesController; + + late _TextFieldSelectionGestureDetectorBuilder + _selectionGestureDetectorBuilder; + + RestorableTextEditingController? _controller; + TextEditingController get _effectiveController => + widget.controller ?? _controller!.value; + + FocusNode? _focusNode; + FocusNode get _effectiveFocusNode => + widget.focusNode ?? (_focusNode ??= FocusNode()); + + @override + String? get restorationId => widget.restorationId; + + bool get _hasError => widget.error || _hasIntrinsicError; + + @override + final GlobalKey editableTextKey = + GlobalKey(); + + @override + void initState() { + super.initState(); + _statesController = MixWidgetStateController(); + + _selectionGestureDetectorBuilder = + _TextFieldSelectionGestureDetectorBuilder(state: this); + if (widget.controller == null) { + _createLocalController(); + } + _effectiveFocusNode.canRequestFocus = + widget.canRequestFocus && widget.enabled; + _effectiveFocusNode.addListener(_handleFocusChanged); + _initStatesController(); + } + + void _registerController() { + assert(_controller != null); + registerForRestoration(_controller!, 'controller'); + } + + void _createLocalController([TextEditingValue? value]) { + assert(_controller == null); + _controller = value == null + // ignore: avoid-undisposed-instances + ? RestorableTextEditingController() + // ignore: avoid-undisposed-instances + : RestorableTextEditingController.fromValue(value); + if (!restorePending) { + _registerController(); + } + } + + /// Toggle the toolbar when a selection handle is tapped. + void _handleSelectionHandleTapped() { + if (_effectiveController.selection.isCollapsed) { + _editableText!.toggleToolbar(); + } + } + + void _handleSelectionChanged( + TextSelection selection, + SelectionChangedCause? cause, + ) { + final bool willShowSelectionHandles = _shouldShowSelectionHandles(cause); + if (willShowSelectionHandles != _showSelectionHandles) { + setState(() { + _showSelectionHandles = willShowSelectionHandles; + }); + } + + switch (m.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) { + _editableText?.bringIntoView(selection.extent); + } + } + + switch (m.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(); + } + } + } + + bool _shouldShowSelectionHandles(SelectionChangedCause? cause) { + // When the text field 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 (!widget.enabled) { + return false; + } + + // ignore: prefer-switch-with-enums + if (cause == SelectionChangedCause.longPress || + cause == SelectionChangedCause.scribble) { + return true; + } + + if (_effectiveController.text.isNotEmpty) { + return true; + } + + return false; + } + + void _initStatesController() { + _statesController.disabled = !widget.enabled; + _statesController.focused = _effectiveFocusNode.hasFocus; + _statesController.error = _hasError; + } + + void _handleFocusChanged() { + // ignore: avoid-empty-setstate, no-empty-block + setState(() { + // Rebuild the widget on focus change to show/hide the text selection + // highlight. + }); + _statesController.focused = _effectiveFocusNode.hasFocus; + } + + @override + void restoreState(RestorationBucket? oldBucket, bool initialRestore) { + if (_controller != null) { + _registerController(); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + _effectiveFocusNode.canRequestFocus = _canRequestFocus; + } + + @override + void didUpdateWidget(TextField 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 && + widget.enabled) { + if (_effectiveController.selection.isCollapsed) { + _showSelectionHandles = !widget.readOnly; + } + } + + _statesController.error = _hasError; + _statesController.focused = _effectiveFocusNode.hasFocus; + _statesController.disabled = !widget.enabled; + } + + @override + void dispose() { + _effectiveFocusNode.removeListener(_handleFocusChanged); + _focusNode?.dispose(); + _controller?.dispose(); + _statesController.dispose(); + // _statesController.removeListener(_handleStatesControllerChange); + // _internalStatesController?.dispose(); + super.dispose(); + } + + @override + late bool forcePressEnabled; + + @override + void autofill(TextEditingValue newEditingValue) => + _editableText!.autofill(newEditingValue); + + EditableTextState? get _editableText => editableTextKey.currentState; + bool _showSelectionHandles = false; + MaxLengthEnforcement get _effectiveMaxLengthEnforcement => + widget.maxLengthEnforcement ?? + LengthLimitingTextInputFormatter.getDefaultMaxLengthEnforcement( + m.Theme.of(context).platform, + ); + + bool get _hasIntrinsicError => + widget.maxLength != null && + widget.maxLength! > 0 && + (widget.controller == null + ? !restorePending && + _effectiveController.value.text.characters.length > + widget.maxLength! + : _effectiveController.value.text.characters.length > + widget.maxLength!); + + bool get _canRequestFocus { + final NavigationMode mode = + MediaQuery.maybeNavigationModeOf(context) ?? NavigationMode.traditional; + + return switch (mode) { + NavigationMode.traditional => widget.canRequestFocus && widget.enabled, + NavigationMode.directional => true, + }; + } + + @override + Widget build(BuildContext context) { + TextSelectionControls? textSelectionControls = widget.selectionControls; + + switch (MixHelpers.targetPlatform) { + case TargetPlatform.iOS: + forcePressEnabled = true; + textSelectionControls ??= cupertinoTextSelectionHandleControls; + + case TargetPlatform.macOS: + forcePressEnabled = false; + textSelectionControls ??= cupertinoDesktopTextSelectionHandleControls; + + case TargetPlatform.android: + case TargetPlatform.fuchsia: + forcePressEnabled = false; + textSelectionControls ??= m.materialTextSelectionHandleControls; + + case TargetPlatform.windows: + case TargetPlatform.linux: + forcePressEnabled = false; + textSelectionControls ??= m.desktopTextSelectionHandleControls; + } + + final List formatters = [ + ...?widget.inputFormatters, + if (widget.maxLength != null) + LengthLimitingTextInputFormatter( + widget.maxLength, + maxLengthEnforcement: _effectiveMaxLengthEnforcement, + ), + ]; + + // Olhar quando for implementar o semantics + // final int? semanticsMaxValueLength; + // if (_effectiveMaxLengthEnforcement != MaxLengthEnforcement.none && + // widget.maxLength != null && + // widget.maxLength! > 0) { + // semanticsMaxValueLength = widget.maxLength; + // } else { + // semanticsMaxValueLength = null; + // } + + final style = widget.style ?? context.remix.components.textField; + final configuration = + TextFieldSpecConfiguration(context, TextFieldSpecUtility.self); + + final child = SpecBuilder( + controller: _statesController, + style: style.makeStyle(configuration).applyVariants(widget.variants), + builder: (context) { + final spec = TextFieldSpec.of(context); + final isFloating = spec.floatingLabel & + (_effectiveFocusNode.hasFocus || + _effectiveController.value.text.isNotEmpty); + + return spec.containerLayout( + direction: Axis.vertical, + children: [ + spec.container( + child: spec.contentLayout( + direction: Axis.horizontal, + children: [ + if (widget.prefixBuilder != null) + widget.prefixBuilder!(spec.icon), + Expanded( + child: AnimatedBuilder( + animation: _effectiveController, + builder: (context, child) => _HintLabel( + text: widget.hintText ?? '', + style: spec.hintTextStyle ?? spec.style, + float: isFloating, + show: spec.floatingLabel + ? true + : _effectiveController.value.text.isEmpty, + floatingLabelHeight: + spec.floatingLabel ? spec.floatingLabelHeight : 0, + floatingLabelStyle: + spec.floatingLabelStyle ?? spec.style, + child: EditableText( + key: editableTextKey, + controller: _effectiveController, + focusNode: _effectiveFocusNode, + readOnly: widget.readOnly || !widget.enabled, + obscuringCharacter: widget.obscuringCharacter, + obscureText: widget.obscureText, + autocorrect: widget.autocorrect, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + enableSuggestions: widget.enableSuggestions, + style: spec.style, + strutStyle: spec.strutStyle, + cursorColor: spec.cursorColor, + backgroundCursorColor: spec.backgroundCursorColor, + textAlign: spec.textAlign, + textDirection: widget.textDirection, + maxLines: widget.maxLines, + minLines: widget.minLines, + expands: widget.expands, + textHeightBehavior: spec.textHeightBehavior, + textWidthBasis: spec.textWidthBasis, + autofocus: widget.autofocus, + showCursor: widget.showCursor, + showSelectionHandles: _showSelectionHandles, + selectionColor: spec.selectionColor, + selectionControls: textSelectionControls, + keyboardType: widget.keyboardType, + textInputAction: widget.textInputAction, + textCapitalization: widget.textCapitalization, + onChanged: widget.onChanged, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + onAppPrivateCommand: widget.onAppPrivateCommand, + onSelectionChanged: _handleSelectionChanged, + onSelectionHandleTapped: _handleSelectionHandleTapped, + onTapOutside: widget.onTapOutside, + inputFormatters: formatters, + mouseCursor: MouseCursor + .defer, // TextField will handle the cursor + rendererIgnoresPointer: true, + cursorWidth: spec.cursorWidth, + cursorHeight: spec.cursorHeight, + cursorRadius: spec.cursorRadius, + cursorOpacityAnimates: spec.cursorOpacityAnimates, + cursorOffset: spec.cursorOffset, + paintCursorAboveText: spec.paintCursorAboveText, + selectionHeightStyle: spec.selectionHeightStyle, + selectionWidthStyle: spec.selectionWidthStyle, + scrollPadding: spec.scrollPadding, + keyboardAppearance: spec.keyboardAppearance, + dragStartBehavior: widget.dragStartBehavior, + enableInteractiveSelection: + widget.enableInteractiveSelection, + scrollController: widget.scrollController, + scrollPhysics: widget.scrollPhysics, + autocorrectionTextRectColor: + spec.autocorrectionTextRectColor, + autofillClient: this, + clipBehavior: widget.clipBehavior, + restorationId: 'editable', + scribbleEnabled: widget.scribbleEnabled, + enableIMEPersonalizedLearning: + widget.enableIMEPersonalizedLearning, + contentInsertionConfiguration: + widget.contentInsertionConfiguration, + contextMenuBuilder: widget.contextMenuBuilder, + spellCheckConfiguration: + widget.spellCheckConfiguration, + magnifierConfiguration: widget + .magnifierConfiguration ?? + m.TextMagnifier.adaptiveMagnifierConfiguration, + undoController: widget.undoController, + ), + ), + ), + ), + if (widget.suffix != null) widget.suffix!, + ], + ), + ), + spec.helperText(widget.helperText ?? ''), + ], + ); + }, + ); + + return Interactable( + mouseCursor: SystemMouseCursors.text, + controller: _statesController, + child: TextFieldTapRegion( + child: IgnorePointer( + ignoring: widget.ignorePointers ?? !widget.enabled, + child: AnimatedBuilder( + animation: _effectiveController, + builder: (context, child) => _TextFieldContext( + isEmpty: _effectiveController.value.text.isEmpty, + child: child!, + ), + child: _selectionGestureDetectorBuilder.buildGestureDetector( + behavior: HitTestBehavior.translucent, + child: child, + ), + ), + ), + ), + ); + } + + @override + bool get selectionEnabled => widget.selectionEnabled && widget.enabled; + + // AutofillClient implementation start. + @override + String get autofillId => _editableText!.autofillId; + + @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. +} + +class _TextFieldSelectionGestureDetectorBuilder + extends TextSelectionGestureDetectorBuilder { + final _TextFieldState _state; + + _TextFieldSelectionGestureDetectorBuilder({ + required _TextFieldState state, + }) : _state = state, + super(delegate: state); + + @override + void onForcePressStart(ForcePressDetails details) { + super.onForcePressStart(details); + if (delegate.selectionEnabled && shouldShowSelectionToolbar) { + editableText.showToolbar(); + } + } + + @override + // ignore: no-empty-block + void onForcePressEnd(ForcePressDetails details) { + // Not required. + } + + @override + void onUserTap() { + _state.widget.onTap?.call(); + } + + @override + void onSingleLongTapStart(LongPressStartDetails details) { + super.onSingleLongTapStart(details); + if (delegate.selectionEnabled) { + switch (m.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); + } + } + } + + @override + bool get onUserTapAlwaysCalled => _state.widget.onTapAlwaysCalled; +} + +class _TextFieldContext extends InheritedWidget { + const _TextFieldContext({required super.child, required this.isEmpty}); + + final bool isEmpty; + + @override + bool updateShouldNotify(_TextFieldContext oldWidget) => + isEmpty != oldWidget.isEmpty; +} + +class _HintLabel extends StatefulWidget { + const _HintLabel({ + required this.text, + required this.style, + required this.float, + required this.show, + required this.floatingLabelHeight, + required this.floatingLabelStyle, + this.child, + }); + + final String text; + final TextStyle style; + final bool float; + final bool show; + final double floatingLabelHeight; + final TextStyle floatingLabelStyle; + final Widget? child; + + @override + State<_HintLabel> createState() => _HintLabelState(); +} + +class _HintLabelState extends State<_HintLabel> + with SingleTickerProviderStateMixin { + late AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + duration: const Duration(milliseconds: 100), + vsync: this, + ); + } + + @override + void didUpdateWidget(_HintLabel oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.float != oldWidget.float) { + if (widget.float) { + _controller.forward(); + } else { + _controller.reverse(); + } + } + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, child) => CustomPaint( + painter: _FloatingLabelPainter( + text: widget.text, + style: widget.style, + floatingProgress: _controller.value, + show: widget.show, + floatingLabelStyle: widget.floatingLabelStyle, + ), + child: Padding( + padding: EdgeInsets.only(top: widget.floatingLabelHeight), + child: widget.child, + ), + ), + ); + } +} + +class _FloatingLabelPainter extends CustomPainter { + final String text; + final TextStyle style; + final double floatingProgress; + final TextStyle floatingLabelStyle; + final bool show; + + const _FloatingLabelPainter({ + required this.text, + required this.style, + required this.floatingProgress, + required this.show, + required this.floatingLabelStyle, + }); + + @override + void paint(Canvas canvas, Size size) { + if (!show) return; + final style = TextStyle.lerp( + this.style, + floatingLabelStyle, + floatingProgress, + ); + + TextSpan span = TextSpan(text: text, style: style); + + TextPainter tp = TextPainter( + text: span, + textDirection: TextDirection.ltr, + ); + tp.layout(); + + final yCenter = Offset(0, (size.height - tp.height) / 2); + + const floatingPosition = Offset(0.0, -2); + + final offset = Offset.lerp(yCenter, floatingPosition, floatingProgress); + tp.paint(canvas, offset!); + } + + @override + bool shouldRepaint(_FloatingLabelPainter oldDelegate) { + return text != oldDelegate.text || + style != oldDelegate.style || + floatingProgress != oldDelegate.floatingProgress || + show != oldDelegate.show || + floatingLabelStyle != oldDelegate.floatingLabelStyle; + } +} diff --git a/packages/remix/lib/src/helpers/spec/composited_transform_follower_spec.dart b/packages/remix/lib/src/helpers/spec/composited_transform_follower_spec.dart index 6490925ca..d588b7d0c 100644 --- a/packages/remix/lib/src/helpers/spec/composited_transform_follower_spec.dart +++ b/packages/remix/lib/src/helpers/spec/composited_transform_follower_spec.dart @@ -13,7 +13,7 @@ class CompositedTransformFollowerSpec final AlignmentGeometry targetAnchor; final AlignmentGeometry followerAnchor; - /// {@macro button_spec_of} + /// {@macro composited_transform_follower_spec_of} static const of = _$CompositedTransformFollowerSpec.of; static const from = _$CompositedTransformFollowerSpec.from; diff --git a/packages/remix/lib/src/theme/remix_theme.dart b/packages/remix/lib/src/theme/remix_theme.dart index e477250c6..e9622a6ba 100644 --- a/packages/remix/lib/src/theme/remix_theme.dart +++ b/packages/remix/lib/src/theme/remix_theme.dart @@ -22,6 +22,7 @@ import '../components/select/select.dart'; import '../components/slider/slider.dart'; import '../components/spinner/spinner.dart'; import '../components/switch/switch.dart'; +import '../components/textfield/textfield.dart'; import '../components/toast/toast.dart'; import 'remix_tokens.dart'; @@ -45,6 +46,7 @@ class RemixComponentTheme { final SelectStyle select; final SpinnerStyle spinner; final SwitchStyle switchComponent; + final TextFieldStyle textField; final ToastStyle toast; final SliderStyle slider; @@ -68,6 +70,7 @@ class RemixComponentTheme { required this.select, required this.spinner, required this.switchComponent, + required this.textField, required this.toast, required this.slider, }); @@ -93,6 +96,7 @@ class RemixComponentTheme { select: SelectStyle(), spinner: SpinnerStyle(), switchComponent: SwitchStyle(), + textField: TextFieldStyle(), toast: ToastStyle(), slider: SliderStyle(), ); @@ -119,6 +123,7 @@ class RemixComponentTheme { select: const SelectDarkStyle(), spinner: const SpinnerDarkStyle(), switchComponent: const SwitchDarkStyle(), + textField: const TextFieldDarkStyle(), slider: const SliderDarkStyle(), ); } @@ -144,6 +149,7 @@ class RemixComponentTheme { select: FortalezaSelectStyle(), spinner: FortalezaSpinnerStyle(), switchComponent: FortalezaSwitchStyle(), + textField: FortalezaTextFieldStyle(), toast: FortalezaToastStyle(), slider: FortalezaSliderStyle(), ); @@ -179,6 +185,7 @@ class RemixComponentTheme { SelectStyle? select, SpinnerStyle? spinner, SwitchStyle? switchComponent, + TextFieldStyle? textField, ToastStyle? toast, SliderStyle? slider, }) { @@ -202,6 +209,7 @@ class RemixComponentTheme { select: select ?? this.select, spinner: spinner ?? this.spinner, switchComponent: switchComponent ?? this.switchComponent, + textField: textField ?? this.textField, toast: toast ?? this.toast, slider: slider ?? this.slider, );