"""
Components/Text fields
======================

.. seealso::

    `Material Design spec, Text fields <https://m3.material.io/components/text-fields/specs>`_

.. rubric:: Text fields let users enter text into a UI.

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields.png
    :align: center

- Make sure text fields look interactive
- Two types: filled and outlined
- The text field’s state (blank, with input, error, etc) should be visible at a glance
- Keep labels and error messages brief and easy to act on
- Text fields commonly appear in forms and dialogs

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/available-fields.png
    :align: center

1. Filled text field
2. Outlined text field

Usage
-----

.. code-block:: kv

    MDTextField:
        mode: "filled"

        MDTextFieldLeadingIcon:
            icon: "magnify"

        MDTextFieldHintText:
            text: "Hint text"

        MDTextFieldHelperText:
            text: "Helper text"
            mode: "persistent"

        MDTextFieldTrailingIcon:
            icon: "information"

        MDTextFieldMaxLengthText:
            max_text_length: 10

Anatomy
-------

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-anatomy.png
    :align: center

Available types of text fields
==============================

Filled mode
-----------

.. code-block:: kv

    MDTextField:
        mode: "filled"

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-filled-mode.png
    :align: center

Outlined mode
-------------

.. code-block:: kv

    MDTextField:
        mode: "outlined"

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-outlined-mode.png
    :align: center

Example
-------

.. tabs::

    .. tab:: Declarative KV style

        .. code-block:: python

            from kivy.lang import Builder

            from kivymd.app import MDApp

            KV = '''
            MDScreen:
                md_bg_color: app.theme_cls.backgroundColor

                MDTextField:
                    mode: "outlined"
                    size_hint_x: None
                    width: "240dp"
                    pos_hint: {"center_x": .5, "center_y": .5}

                    MDTextFieldLeadingIcon:
                        icon: "account"

                    MDTextFieldHintText:
                        text: "Outlined"

                    MDTextFieldHelperText:
                        text: "Helper text"
                        mode: "persistent"

                    MDTextFieldTrailingIcon:
                        icon: "information"

                    MDTextFieldMaxLengthText:
                        max_text_length: 10
            '''


            class Example(MDApp):
                def build(self):
                    self.theme_cls.primary_palette = "Olive"
                    return Builder.load_string(KV)


            Example().run()

    .. tab:: Declarative Python style

        .. code-block:: python

            from kivymd.uix.textfield import (
                MDTextField,
                MDTextFieldLeadingIcon,
                MDTextFieldHintText,
                MDTextFieldHelperText,
                MDTextFieldTrailingIcon,
                MDTextFieldMaxLengthText,
            )

            from kivymd.uix.screen import MDScreen
            from kivymd.app import MDApp


            class Example(MDApp):
                def build(self):
                    self.theme_cls.primary_palette = "Olive"
                    return MDScreen(
                        MDTextField(
                            MDTextFieldLeadingIcon(
                                icon="account",
                            ),
                            MDTextFieldHintText(
                                text="Hint text",
                            ),
                            MDTextFieldHelperText(
                                text="Helper text",
                                mode="persistent",
                            ),
                            MDTextFieldTrailingIcon(
                                icon="information",
                            ),
                            MDTextFieldMaxLengthText(
                                max_text_length=10,
                            ),
                            mode="outlined",
                            size_hint_x=None,
                            width="240dp",
                            pos_hint={"center_x": 0.5, "center_y": 0.5},
                        ),
                        md_bg_color=self.theme_cls.backgroundColor,
                    )


            Example().run()

.. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-example.png
    :align: center

API break
=========

1.2.0 version
-------------

.. code-block:: kv

    MDTextField:
        mode: "rectangle"
        hint_text: "Hint text"
        helper_text: "Helper text"
        helper_text_mode: "persistent"
        max_text_length: 10
        icon_right: "information"

2.0.0 version
-------------

.. note:: The text field with the `round` type was removed in version `2.0.0`.

.. code-block:: kv

    MDTextField:
        mode: "outlined"

        MDTextFieldLeadingIcon:
            icon: "phone"

        MDTextFieldTrailingIcon:
            icon: "information"

        MDTextFieldHintText:
            text: "Hint text"

        MDTextFieldHelperText:
            text: "Helper text"
            mode: "persistent"

        MDTextFieldMaxLengthText:
            max_text_length: 10
"""

from __future__ import annotations

__all__ = (
    "BaseTextFieldIcon",
    "BaseTextFieldLabel",
    "Validator",
    "AutoFormatTelephoneNumber",
    "MDTextField",
    "MDTextFieldHelperText",
    "MDTextFieldMaxLengthText",
    "MDTextFieldHintText",
    "MDTextFieldLeadingIcon",
    "MDTextFieldTrailingIcon",
)

import os
import re
from datetime import date

from kivy.animation import Animation
from kivy.clock import Clock
from kivy.lang import Builder
from kivy.metrics import dp
from kivy.properties import (
    BooleanProperty,
    ColorProperty,
    ListProperty,
    NumericProperty,
    ObjectProperty,
    OptionProperty,
    StringProperty,
    VariableListProperty,
)
from kivy.uix.textinput import TextInput

from kivymd import uix_path
from kivymd.font_definitions import theme_font_styles
from kivymd.theming import ThemableBehavior, ThemeManager
from kivymd.uix.behaviors import DeclarativeBehavior, BackgroundColorBehavior
from kivymd.uix.behaviors.state_layer_behavior import StateLayerBehavior
from kivymd.uix.label import MDIcon, MDLabel

with open(
    os.path.join(uix_path, "textfield", "textfield.kv"), encoding="utf-8"
) as kv_file:
    Builder.load_string(kv_file.read())


# TODO: Add a class to work with the phone number mask.


class AutoFormatTelephoneNumber:
    """
    Implements automatic formatting of the text entered in the text field
    according to the mask, for example '+38 (###) ### ## ##'.

    .. warning:: This class has not yet been implemented and it is not
        recommended to use it yet.
    """

    def __init__(self):
        self._backspace = False

    def isnumeric(self, value) -> bool:
        try:
            int(value)
            return True
        except ValueError:
            return False

    def do_backspace(self, *args) -> None:
        """Do backspace operation from the current cursor position."""

        if self.validator and self.validator == "phone":
            self._backspace = True
            text = self.text
            text = text[:-1]
            self.text = text
            self._backspace = False

    def field_filter(self, value, boolean) -> None:
        if self.validator and self.validator == "phone":
            if len(self.text) == 14:
                return
            if self.isnumeric(value):
                return value
        return value

    def format(self, value) -> None:
        if value != "" and not value.isspace() and not self._backspace:
            if len(value) <= 1 and self.focus:
                self.text = value
                self._check_cursor()
            elif len(value) == 4:
                start = self.text[:-1]
                end = self.text[-1]
                self.text = "%s) %s" % (start, end)
                self._check_cursor()
            elif len(value) == 8:
                self.text += "-"
                self._check_cursor()
            elif len(value) in [12, 16]:
                start = self.text[:-1]
                end = self.text[-1]
                self.text = "%s-%s" % (start, end)
                self._check_cursor()

    def _check_cursor(self):
        def set_pos_cursor(pos_corsor, interval=0.5):
            self.cursor = (pos_corsor, 0)

        if self.focus:
            Clock.schedule_once(lambda x: set_pos_cursor(len(self.text)), 0.1)


class Validator:
    """Container class for various validation methods."""

    datetime_date = ObjectProperty()
    """
    The last valid date as a <class 'datetime.date'> object.

    :attr:`datetime_date` is an :class:`~kivy.properties.ObjectProperty`
    and defaults to `None`.
    """

    date_interval = ListProperty([None, None])
    """
    The date interval that is valid for input.
    Can be entered as <class 'datetime.date'> objects or a string format.
    Both values or just one value can be entered.

    In string format, must follow the current date_format.
    Example: Given date_format -> "mm/dd/yyyy"
    Input examples -> "12/31/1900", "12/31/2100" or "12/31/1900", None.

    :attr:`date_interval` is an :class:`~kivy.properties.ListProperty`
    and defaults to `[None, None]`.
    """

    date_format = OptionProperty(
        None,
        options=[
            "dd/mm/yyyy",
            "mm/dd/yyyy",
            "yyyy/mm/dd",
        ],
    )

    """
    Format of date strings that will be entered.
    Available options are: `'dd/mm/yyyy'`, `'mm/dd/yyyy'`, `'yyyy/mm/dd'`.

    :attr:`date_format` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    def is_email_valid(self, text: str) -> bool:
        """Checks the validity of the email."""

        if not re.match(r"[^@]+@[^@]+\.[^@]+", text):
            return True
        return False

    def is_time_valid(self, text: str) -> bool:
        """Checks the validity of the time."""

        if re.match(r"^(2[0-3]|[01]?[0-9]):([0-5]?[0-9])$", text) or re.match(
            r"^(2[0-3]|[01]?[0-9]):([0-5]?[0-9]):([0-5]?[0-9])$", text
        ):
            return False

        return True

    def is_date_valid(self, text: str) -> bool:
        """Checks the validity of the date."""

        if not self.date_format:
            raise Exception("TextInput date_format was not defined.")

        # Regex strings.
        dd = "[0][1-9]|[1-2][0-9]|[3][0-1]"
        mm = "[0][1-9]|[1][0-2]"
        yyyy = "[0-9][0-9][0-9][0-9]"
        fmt = self.date_format.split("/")
        args = locals()
        # Access  the local variables  dict in the correct format based on
        # date_format split. Example: "mm/dd/yyyy" -> ["mm", "dd", "yyyy"]
        # args[fmt[0]] would be args["mm"] so the month regex string.
        if re.match(
            f"^({args[fmt[0]]})/({args[fmt[1]]})/({args[fmt[2]]})$", text
        ):
            input_split = text.split("/")
            args[fmt[0]] = input_split[0]
            args[fmt[1]] = input_split[1]
            args[fmt[2]] = input_split[2]
            # Organize input  into correct slots and try to convert
            # to datetime  object. This way February exceptions are
            # tested. Also tests with the date_interval are simpler
            # using datetime objects.
            try:
                datetime = date(
                    int(args["yyyy"]), int(args["mm"]), int(args["dd"])
                )
            except ValueError:
                return True

            if self.date_interval:
                if (
                    self.date_interval[0]
                    and not self.date_interval[0] <= datetime
                    or self.date_interval[1]
                    and not datetime <= self.date_interval[1]
                ):
                    return True

            self.datetime_date = datetime
            return False
        return True

    def on_date_interval(self, *args) -> None:
        """Default event handler for date_interval input."""

        def on_date_interval():
            if not self.date_format:
                raise Exception("TextInput date_format was not defined.")

            fmt = self.date_format.split("/")
            args = {}
            # Convert string inputs into datetime.date objects and store
            # them back into self.date_interval.
            try:
                if self.date_interval[0] and not isinstance(
                    self.date_interval[0], date
                ):
                    split = self.date_interval[0].split("/")
                    args[fmt[0]] = split[0]
                    args[fmt[1]] = split[1]
                    args[fmt[2]] = split[2]
                    self.date_interval[0] = date(
                        int(args["yyyy"]), int(args["mm"]), int(args["dd"])
                    )
                if self.date_interval[1] and not isinstance(
                    self.date_interval[1], date
                ):
                    split = self.date_interval[1].split("/")
                    args[fmt[0]] = split[0]
                    args[fmt[1]] = split[1]
                    args[fmt[2]] = split[2]
                    self.date_interval[1] = date(
                        int(args["yyyy"]), int(args["mm"]), int(args["dd"])
                    )

            except Exception:
                raise Exception(
                    r"TextInput date_interval was defined incorrectly, "
                    r"it must be composed of <class 'datetime.date'> objects "
                    r"or strings following current date_format."
                )

            # Test if the interval is valid.
            if isinstance(self.date_interval[0], date) and isinstance(
                self.date_interval[1], date
            ):
                if self.date_interval[0] >= self.date_interval[1]:
                    raise Exception(
                        "TextInput date_interval last date must be greater "
                        "than the first date or set to None."
                    )

        Clock.schedule_once(lambda x: on_date_interval())


class BaseTextFieldLabel(MDLabel):
    """
    Base texture for :class:`~MDTextField` class (helper text, max length,
    hint text).

    For more information, see in the
    :class:`~kivymd.uix.label.label.MDLabel` class documentation.

    .. versionadded:: 2.0.0
    """

    text_color_normal = ColorProperty(None)
    """
    Text color in (r, g, b, a) or string format when text field is out
    of focus.

    .. versionadded:: 1.0.0

    .. versionchanged:: 2.0.0
        The property was moved from class:`~MDTextField` class and renamed
        from `helper_text_color_normal` to `text_color_normal`.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
    
            MDTextFieldHintText:
                text: "Hint text color normal"
                text_color_normal: "brown"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-text-color-normal.png
        :align: center

    :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color_focus = ColorProperty(None)
    """
    Text color in (r, g, b, a) or string format when the text field has
    focus.

    .. versionadded:: 1.0.0

    .. versionchanged:: 2.0.0
        The property was moved from class:`~MDTextField` class and renamed
        from `helper_text_color_focus` to `text_color_focus`.

    .. code-block:: kv

        MDTextField:

            MDTextFieldHelperText:
                text: "Helper text color focus"
                text_color_focus: "brown"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-text-color-focus.png
        :align: center

    :attr:`text_color_focus` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """


class MDTextFieldHelperText(BaseTextFieldLabel):
    """
    Implements the helper text label.

    For more information, see in the :class:`~BaseTextFieldLabel`
    class documentation.

    .. versionadded:: 2.0.0
    """

    mode = OptionProperty(
        "on_focus", options=["on_error", "persistent", "on_focus"]
    )
    """
    Helper text mode. Available options are: `'on_error'`, `'persistent'`,
    `'on_focus'`.

    .. versionchanged:: 2.0.0
        The property was moved from class:`~MDTextField` class and renamed
        from `helper_text_mode` to `mode`.

    On focus
    --------

    .. code-block:: kv

        MDTextField:
            mode: "filled"

            MDTextFieldHelperText:
                text: "Helper text"
                mode: "on_focus"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-helper-text-mode-on-focus.gif
        :align: center

    On error
    --------

    .. code-block:: kv

        MDTextField:
            mode: "filled"

            MDTextFieldHelperText:
                text: "Helper text"
                mode: "on_error"

            MDTextFieldMaxLengthText:
                max_text_length: 5

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-helper-text-mode-on-error.gif
        :align: center

    Persistent
    ----------

    .. code-block:: kv

        MDTextField:
            mode: "filled"

            MDTextFieldHelperText:
                text: "Helper text"
                mode: "persistent"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-helper-text-mode-persistent.gif
        :align: center

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'on_focus'`.
    """


class MDTextFieldMaxLengthText(BaseTextFieldLabel):
    """
    Implements the max length text label.

    For more information, see in the :class:`~BaseTextFieldLabel`
    class documentation.

    .. versionadded:: 2.0.0
    """

    max_text_length = NumericProperty(None)
    """
    Maximum allowed value of characters in a text field.

    .. versionchanged:: 2.0.0
        The property was moved from class:`~MDTextField`.

    .. code-block:: kv

        MDTextField:
            mode: "filled"

            MDTextFieldMaxLengthText:
                max_text_length: 10

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-max-text-length.png
        :align: center

    :attr:`max_text_length` is an :class:`~kivy.properties.NumericProperty`
    and defaults to `None`.
    """


class MDTextFieldHintText(BaseTextFieldLabel):
    """
    Implements the hint text label.

    For more information, see in the :class:`~BaseTextFieldLabel`
    class documentation.

    .. versionadded:: 2.0.0

    .. code-block:: kv

        MDTextField:
            mode: "filled"

            MDTextFieldHintText:
                text: "Hint text"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-hint-text.gif
        :align: center
    """


class BaseTextFieldIcon(MDIcon):
    """
    Base texture for :class:`~MDTextField` class (helper text, max length,
    hint text).

    For more information, see in the :class:`~kivymd.uix.label.label.MDIcon`
    class documentation.

    .. versionchanged:: 2.0.0
    """

    icon_color_normal = ColorProperty(None)
    """
    Icon color in (r, g, b, a) or string format when text field is out
    of focus.

    .. versionadded:: 1.0.0

    .. versionchanged:: 2.0.0
        The property was moved from class:`~MDTextField` class and renamed
        from `icon_right_color_normal/icon_left_color_normal`
        to `icon_color_normal`.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
    
            MDTextFieldLeadingIcon:
                icon: "phone"
                theme_icon_color: "Custom"
                icon_color_normal: "lightgreen"
    
            MDTextFieldHintText:
                text: "Leading icon color normal"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-leading-icon-color-normal.png
        :align: center

    :attr:`icon_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    icon_color_focus = ColorProperty(None)
    """
    Icon color in (r, g, b, a) or string format when the text field has
    focus.

    .. versionadded:: 1.0.0

    .. versionchanged:: 2.0.0
        The property was moved from class:`~MDTextField` class and renamed
        from `icon_right_color_focus/icon_left_color_focus `
        to `icon_color_focus`.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
    
            MDTextFieldLeadingIcon:
                icon: "phone"
                theme_icon_color: "Custom"
                icon_color_focus: "lightgreen"
    
            MDTextFieldHintText:
                text: "Leading icon color focus"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-leading-icon-color-focus.png
        :align: center

    :attr:`icon_color_focus` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """


class MDTextFieldLeadingIcon(BaseTextFieldIcon):
    """
    Implements the leading icon.

    For more information, see in the :class:`~BaseTextFieldIcon`
    class documentation.

    .. versionadded:: 2.0.0

    .. code-block:: kv

        MDTextField:
            mode: "filled"

            MDTextFieldLeadingIcon:
                icon: "phone"

            MDTextFieldHintText:
                text: "Field with leading icon"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-leading-icon.png
        :align: center
    """


class MDTextFieldTrailingIcon(BaseTextFieldIcon):
    """
    Implements the trailing icon.

    For more information, see in the :class:`~BaseTextFieldIcon`
    class documentation.

    .. versionadded:: 2.0.0

    .. code-block:: kv

        MDTextField:
            mode: "filled"

            MDTextFieldTrailingIcon:
                icon: "phone"

            MDTextFieldHintText:
                text: "Field with trailing icon"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-trailing-icon.png
        :align: center
    """


# TODO: Add a custom color for the disabled state of the text field.
class MDTextField(
    DeclarativeBehavior,
    StateLayerBehavior,
    ThemableBehavior,
    TextInput,
    Validator,
    AutoFormatTelephoneNumber,
    BackgroundColorBehavior,
):
    """
    Textfield class.

    For more information, see in the
    :class:`~kivymd.uix.behaviors.declarative_behavior.DeclarativeBehavior` and
    :class:`~kivymd.uix.behaviors.backgroundcolor_behavior.BackgroundColorBehavior` and
    :class:`~kivymd.theming.ThemableBehavior` and
    :class:`~kivy.uix.textinput.TextInput` and
    :class:`~Validator` and
    :class:`~AutoFormatTelephoneNumber` and
    :class:`~kivymd.uix.behaviors.state_layer_behavior.StateLayerBehavior`
    classes documentation.
    """

    font_style = StringProperty("Body")
    """
    Name of the style for the input text.

    .. versionadded:: 2.0.0

    .. seealso::

        `Font style names <https://kivymd.readthedocs.io/en/latest/components/label/#all-styles>`_

    :attr:`font_style` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'Body'`.
    """

    role = StringProperty("large")
    """
    Role of font style.

    .. versionadded:: 2.0.0

    .. seealso::

        `Font style roles <https://kivymd.readthedocs.io/en/latest/components/label/#all-styles>`_

    :attr:`role` is an :class:`~kivy.properties.StringProperty`
    and defaults to `'large'`.
    """

    mode = OptionProperty("outlined", options=["outlined", "filled"])
    """
    Text field mode. Available options are: `'outlined'`, `'filled'`.

    :attr:`mode` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `'outlined'`.
    """

    error_color = ColorProperty(None)
    """
    Error color in (r, g, b, a) or string format for `required = True`
    or when the text field is in `error` state.

    :attr:`error_color` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    error = BooleanProperty(False)
    """
    If True, then the text field goes into `error` mode.

    :attr:`error` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    text_color_normal = ColorProperty(None)
    """
    Text color in (r, g, b, a) or string format when text field is out of focus.

    .. versionadded:: 1.0.0

    .. code-block:: kv

        MDTextField:
            theme_text_color: "Custom"
            text_color_normal: "green"
            text: "Text color normal"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-normal.png
        :align: center

    :attr:`text_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    text_color_focus = ColorProperty(None)
    """
    Text color in (r, g, b, a) or string format when text field has focus.

    .. versionadded:: 1.0.0

    .. code-block:: kv

        MDTextField:
            theme_text_color: "Custom"
            text_color_focus: "green"
            text: "Text color focus"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-field-text-color-focus.png
        :align: center

    :attr:`text_color_focus` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    radius = VariableListProperty([dp(4), dp(4), dp(4), dp(4)])
    """
    The corner radius for a text field in `filled/outlined` mode.

    :attr:`radius` is a :class:`~kivy.properties.VariableListProperty` and
    defaults to `[dp(4), dp(4), 0, 0]`.
    """

    required = BooleanProperty(False)
    """
    Required text. If True then the text field requires text.

    :attr:`required` is an :class:`~kivy.properties.BooleanProperty`
    and defaults to `False`.
    """

    line_color_normal = ColorProperty(None)
    """
    Line color normal (active indicator) in (r, g, b, a) or string format.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
            theme_line_color: "Custom"
            line_color_normal: "green"
    
            MDTextFieldHelperText:
                text: "Line color normal"
                mode: "persistent"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-line-color-normal.png
        :align: center

    :attr:`line_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    line_color_focus = ColorProperty(None)
    """
    Line color focus (active indicator) in (r, g, b, a) or string format.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
            theme_line_color: "Custom"
            line_color_focus: "green"
    
            MDTextFieldHelperText:
                text: "Line color focus"
                mode: "persistent"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-line-color-focus.png
        :align: center

    :attr:`line_color_focus` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    fill_color_normal = ColorProperty(None)
    """
    Fill background color in (r, g, b, a) or string format in 'fill' mode when]
    text field is out of focus.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
            theme_bg_color: "Custom"
            fill_color_normal: 0, 1, 0, .2
    
            MDTextFieldHelperText:
                text: "Fill color normal"
                mode: "persistent"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-fill-color-normal.png
        :align: center

    :attr:`fill_color_normal` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    fill_color_focus = ColorProperty(None)
    """
    Fill background color in (r, g, b, a) or string format in 'fill' mode when
    the text field has focus.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
            theme_bg_color: "Custom"
            fill_color_focus: 0, 1, 0, .2
    
            MDTextFieldHelperText:
                text: "Fill color focus"
                mode: "persistent"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-fill-color-focus.png
        :align: center

    :attr:`fill_color_focus` is an :class:`~kivy.properties.ColorProperty`
    and defaults to `None`.
    """

    # TODO: Add minimum allowed height. Otherwise, if the value is,
    #  for example, 20, the text field will simply be lessened.
    max_height = NumericProperty(0)
    """
    Maximum height of the text box when `multiline = True`.

    .. code-block:: kv

        MDTextField:
            mode: "filled"
            max_height: "200dp"
            multiline: True
    
            MDTextFieldHelperText:
                text: "multiline=True"
                mode: "persistent"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-multiline.gif
        :align: center

    :attr:`max_height` is a :class:`~kivy.properties.NumericProperty` and
    defaults to `0`.
    """

    phone_mask = StringProperty("")
    """
    This property has not yet been implemented and it is not recommended to
    use it yet.

    :attr:`phone_mask` is a :class:`~kivy.properties.StringProperty` and
    defaults to ''.
    """

    validator = OptionProperty(None, options=["date", "email", "time", "phone"])
    """
    The type of text field for entering Email, time, etc.
    Automatically sets the type of the text field as "error" if the user input
    does not match any of the set validation types.
    Available options are: `'date'`, `'email'`, `'time'`.

    When using `'date'`, :attr:`date_format` must be defined.

    .. versionadded:: 1.1.0

    .. code-block:: kv

        MDTextField:
            mode: "filled"
            validator: "email"
    
            MDTextFieldHintText:
                text: "Email"
    
            MDTextFieldHelperText:
                text: "user@gmail.com"
                mode: "persistent"

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-email-validator.png
        :align: center

    .. code-block:: python

        from kivy.lang import Builder

        from kivymd.app import MDApp

        KV = '''
        MDScreen:
            md_bg_color: self.theme_cls.backgroundColor

            MDBoxLayout:
                orientation: "vertical"
                spacing: "20dp"
                adaptive_height: True
                size_hint_x: .8
                pos_hint: {"center_x": .5, "center_y": .5}

                MDTextField:
                    validator: "date"
                    date_format: "dd/mm/yyyy"

                    MDTextFieldHintText:
                        text: "Date dd/mm/yyyy without limits"

                    MDTextFieldHelperText:
                        text: "Enter a valid dd/mm/yyyy date"

                MDTextField:
                    validator: "date"
                    date_format: "mm/dd/yyyy"

                    MDTextFieldHintText:
                        text: "Date mm/dd/yyyy without limits"

                    MDTextFieldHelperText:
                        text: "Enter a valid mm/dd/yyyy date"

                MDTextField:
                    validator: "date"
                    date_format: "yyyy/mm/dd"

                    MDTextFieldHintText:
                        text: "Date yyyy/mm/dd without limits"

                    MDTextFieldHelperText:
                        text: "Enter a valid yyyy/mm/dd date"

                MDTextField:
                    validator: "date"
                    date_format: "dd/mm/yyyy"
                    date_interval: "01/01/1900", "01/01/2100"

                    MDTextFieldHintText:
                        text: "Date dd/mm/yyyy in [01/01/1900, 01/01/2100] interval"

                    MDTextFieldHelperText:
                        text: "Enter a valid dd/mm/yyyy date"

                MDTextField:
                    validator: "date"
                    date_format: "dd/mm/yyyy"
                    date_interval: "01/01/1900", None

                    MDTextFieldHintText:
                        text: "Date dd/mm/yyyy in [01/01/1900, None] interval"

                    MDTextFieldHelperText:
                        text: "Enter a valid dd/mm/yyyy date"

                MDTextField:
                    validator: "date"
                    date_format: "dd/mm/yyyy"
                    date_interval: None, "01/01/2100"

                    MDTextFieldHintText:
                        text: "Date dd/mm/yyyy in [None, 01/01/2100] interval"

                    MDTextFieldHelperText:
                        text: "Enter a valid dd/mm/yyyy date"
        '''


        class Example(MDApp):
            def build(self):
                self.theme_cls.primary_palette = "Olive"
                return Builder.load_string(KV)


        Example().run()

    .. image:: https://github.com/HeaTTheatR/KivyMD-data/raw/master/gallery/kivymddoc/text-fields-validator-date.png
        :align: center

    :attr:`validator` is an :class:`~kivy.properties.OptionProperty`
    and defaults to `None`.
    """

    # Helper text label object.
    _helper_text_label = ObjectProperty()
    # Hint text label object.
    _hint_text_label = ObjectProperty()
    # Leading icon object.
    _leading_icon = ObjectProperty()
    # Trailing icon object.
    _trailing_icon = ObjectProperty()
    # Max length label object.
    _max_length_label = ObjectProperty()
    # Maximum length of characters to be input.
    _max_length = "0"
    # Active indicator height.
    _indicator_height = NumericProperty(dp(1))
    # Outline height.
    _outline_height = NumericProperty(dp(1))
    # The x-axis position of the hint text in the text field.
    _hint_x = NumericProperty(0)
    # The y-axis position of the hint text in the text field.
    _hint_y = NumericProperty(0)
    # The right/left lines coordinates of the text field in 'outlined' mode.
    _left_x_axis_pos = NumericProperty(dp(32))
    _right_x_axis_pos = NumericProperty(dp(32))

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.bind(text=self.set_text)
        self.theme_cls.bind(
            primary_palette=self.update_colors,
            theme_style=self.update_colors,
        )
        Clock.schedule_once(self._check_text)

    def update_colors(
        self, theme_manager: ThemeManager, theme_color: str
    ) -> None:
        """Fired when the `primary_palette` or `theme_style` value changes."""

        def update_colors(*args):
            if not self.disabled:
                self.on_focus(self, self.focus)
            else:
                self.on_disabled(self, self.disabled)

        Clock.schedule_once(update_colors, 1)

    def add_widget(self, widget, index=0, canvas=None):
        if isinstance(widget, MDTextFieldHelperText):
            self._helper_text_label = widget
        if isinstance(widget, MDTextFieldHintText):
            self._hint_text_label = widget
        if isinstance(widget, MDTextFieldLeadingIcon):
            self._leading_icon = widget
        if isinstance(widget, MDTextFieldTrailingIcon):
            self._trailing_icon = widget
        if isinstance(widget, MDTextFieldMaxLengthText):
            self._max_length_label = widget
        else:
            return super().add_widget(widget)

    def set_texture_color(
        self, texture, canvas_group, color: list, error: bool = False
    ) -> None:
        """
        Animates the color of the
        leading/trailing icons/hint/helper/max length text.
        """

        def update_hint_text_rectangle(*args):
            hint_text_rectangle = self.canvas.after.get_group(
                "hint-text-rectangle"
            )[0]
            hint_text_rectangle.texture = None
            texture.texture_update()
            hint_text_rectangle.texture = texture.texture

        if texture:
            Animation(rgba=color, d=0).start(canvas_group)
            a = Animation(color=color, d=0)
            if texture is self._hint_text_label:
                a.bind(on_complete=update_hint_text_rectangle)
            a.start(texture)

    def set_pos_hint_text(self, y: float, x: float) -> None:
        """Animates the x-axis width and y-axis height of the hint text."""

        Animation(_hint_y=y, _hint_x=x, d=0.2, t="out_quad").start(self)

    def set_hint_text_font_size(self) -> None:
        """Animates the font size of the hint text."""

        Animation(
            size=self._hint_text_label.texture_size, d=0.2, t="out_quad"
        ).start(self.canvas.after.get_group("hint-text-rectangle")[0])

    def set_space_in_line(
        self, left_width: float | int, right_width: float | int
    ) -> None:
        """
        Animates the length of the right line of the text field for the
        hint text.
        """

        Animation(_left_x_axis_pos=left_width, d=0.2, t="out_quad").start(self)
        Animation(_right_x_axis_pos=right_width, d=0.2, t="out_quad").start(
            self
        )

    def set_max_text_length(self) -> None:
        """
        Fired when text is entered into a text field.
        Set max length text and updated max length texture.
        """

        if self._max_length_label:
            self._max_length_label.text = ""
            self._max_length_label.text = (
                f"{len(self.text)}/{self._max_length_label.max_text_length}"
            )
            self._max_length_label.texture_update()
            max_length_rect = self.canvas.before.get_group("max-length-rect")[0]
            max_length_rect.texture = None
            max_length_rect.texture = self._max_length_label.texture
            max_length_rect.size = self._max_length_label.texture_size
            max_length_rect.pos = (
                (self.x + self.width)
                - (self._max_length_label.texture_size[0] + dp(16)),
                self.y - dp(18),
            )

    def set_text(self, instance, text: str) -> None:
        """Fired when text is entered into a text field."""

        def set_text(*args):
            self.text = re.sub("\n", " ", text) if not self.multiline else text
            self.set_max_text_length()

            if self.text and self._get_has_error() or self._get_has_error():
                self.error = True
            elif self.text and not self._get_has_error():
                self.error = False

            # Start the appropriate texture animations when programmatically
            # pasting text into a text field.
            if len(self.text) != 0 and not self.focus:
                if self._hint_text_label:
                    self._hint_text_label.font_size = theme_font_styles[
                        self._hint_text_label.font_style
                    ]["small"]["font-size"]
                    self._hint_text_label.texture_update()
                    self.set_hint_text_font_size()

            if (not self.text and not self.focus) or (
                self.text and not self.focus
            ):
                self.on_focus(instance, False)

        set_text()

    def on_focus(self, instance, focus: bool) -> None:
        """Fired when the `focus` value changes."""

        if focus:
            if self.mode == "filled":
                Animation(_indicator_height=dp(1.25), d=0).start(self)
            else:
                Animation(_outline_height=dp(1.25), d=0).start(self)

            if self._trailing_icon:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._trailing_icon,
                        self.canvas.before.get_group("trailing-icons-color")[0],
                        (
                            self.theme_cls.onSurfaceVariantColor
                            if self._trailing_icon.theme_icon_color == "Primary"
                            or not self._trailing_icon.icon_color_focus
                            else self._trailing_icon.icon_color_focus
                        )
                        if not self.error
                        else self._get_error_color(),
                    )
                )
            if self._leading_icon:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._leading_icon,
                        self.canvas.before.get_group("leading-icons-color")[0],
                        self.theme_cls.onSurfaceVariantColor
                        if self._leading_icon.theme_icon_color == "Primary"
                        or not self._leading_icon.icon_color_focus
                        else self._leading_icon.icon_color_focus,
                    )
                )
            if self._max_length_label and not self.error:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._max_length_label,
                        self.canvas.before.get_group("max-length-color")[0],
                        self.theme_cls.onSurfaceVariantColor
                        if not self._max_length_label.text_color_focus
                        else self._max_length_label.text_color_focus,
                    )
                )

            if self._helper_text_label and self._helper_text_label.mode in (
                "on_focus",
                "persistent",
            ):
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._helper_text_label,
                        self.canvas.before.get_group("helper-text-color")[0],
                        (
                            self.theme_cls.onSurfaceVariantColor
                            if not self._helper_text_label.text_color_focus
                            else self._helper_text_label.text_color_focus
                        )
                        if not self.error
                        else self._get_error_color(),
                    )
                )
            if (
                self._helper_text_label
                and self._helper_text_label.mode == "on_error"
                and not self.error
            ):
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._helper_text_label,
                        self.canvas.before.get_group("helper-text-color")[0],
                        self.theme_cls.transparentColor,
                    )
                )
            if self._hint_text_label:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._hint_text_label,
                        self.canvas.after.get_group("hint-text-color")[0],
                        (
                            self.theme_cls.primaryColor
                            if not self._hint_text_label.text_color_focus
                            else self._hint_text_label.text_color_focus
                        )
                        if not self.error
                        else self._get_error_color(),
                    )
                )
                self.set_pos_hint_text(
                    0 if self.mode != "outlined" else dp(-14),
                    (
                        -(
                            (
                                self._leading_icon.texture_size[0]
                                if self._leading_icon
                                else 0
                            )
                            + dp(12)
                        )
                        if self._leading_icon
                        else 0
                    )
                    if self.mode == "outlined"
                    else -(
                        (
                            self._leading_icon.texture_size[0]
                            if self._leading_icon
                            else 0
                        )
                        - dp(24)
                    ),
                )
                self._hint_text_label.font_size = theme_font_styles[
                    self._hint_text_label.font_style
                ]["small"]["font-size"]
                self._hint_text_label.texture_update()
                self.set_hint_text_font_size()
                if self.mode == "outlined":
                    self.set_space_in_line(
                        dp(14), self._hint_text_label.texture_size[0] + dp(18)
                    )
        else:
            if self.mode == "filled":
                Animation(_indicator_height=dp(1), d=0).start(self)
            else:
                Animation(_outline_height=dp(1), d=0).start(self)

            if self._leading_icon:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._leading_icon,
                        self.canvas.before.get_group("leading-icons-color")[0],
                        self.theme_cls.onSurfaceVariantColor
                        if self._leading_icon.theme_icon_color == "Primary"
                        or not self._leading_icon.icon_color_normal
                        else self._leading_icon.icon_color_normal,
                    )
                )
            if self._trailing_icon:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._trailing_icon,
                        self.canvas.before.get_group("trailing-icons-color")[0],
                        (
                            self.theme_cls.onSurfaceVariantColor
                            if self._trailing_icon.theme_icon_color == "Primary"
                            or not self._trailing_icon.icon_color_normal
                            else self._trailing_icon.icon_color_normal
                        )
                        if not self.error
                        else self._get_error_color(),
                    )
                )
            if self._max_length_label and not self.error:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._max_length_label,
                        self.canvas.before.get_group("max-length-color")[0],
                        self.theme_cls.onSurfaceVariantColor
                        if not self._max_length_label.text_color_normal
                        else self._max_length_label.text_color_normal,
                    )
                )
            if (
                self._helper_text_label
                and self._helper_text_label.mode == "on_focus"
            ):
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._helper_text_label,
                        self.canvas.before.get_group("helper-text-color")[0],
                        self.theme_cls.transparentColor,
                    )
                )
            elif (
                self._helper_text_label
                and self._helper_text_label.mode == "persistent"
            ):
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._helper_text_label,
                        self.canvas.before.get_group("helper-text-color")[0],
                        (
                            self.theme_cls.onSurfaceVariantColor
                            if not self._helper_text_label.text_color_normal
                            else self._helper_text_label.text_color_normal
                        )
                        if not self.error
                        else self._get_error_color(),
                    )
                )

            if not self.text:
                if self._hint_text_label:
                    if self.mode == "outlined":
                        self.set_space_in_line(dp(32), dp(32))
                    self._hint_text_label.font_size = theme_font_styles[
                        self._hint_text_label.font_style
                    ]["large"]["font-size"]
                    self._hint_text_label.texture_update()
                    self.set_hint_text_font_size()
                    self.set_pos_hint_text(
                        (self.height / 2)
                        - (self._hint_text_label.texture_size[1] / 2),
                        0,
                    )
            else:
                if self._hint_text_label:
                    if self.mode == "outlined":
                        self.set_space_in_line(
                            dp(14),
                            self._hint_text_label.texture_size[0] + dp(18),
                        )
                    self.set_pos_hint_text(
                        0 if self.mode != "outlined" else dp(-14),
                        (
                            -(
                                (
                                    self._leading_icon.texture_size[0]
                                    if self._leading_icon
                                    else 0
                                )
                                + dp(12)
                            )
                            if self._leading_icon
                            else 0
                        )
                        if self.mode == "outlined"
                        else -(
                            (
                                self._leading_icon.texture_size[0]
                                if self._leading_icon
                                else 0
                            )
                            - dp(24)
                        ),
                    )

            if self._hint_text_label:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._hint_text_label,
                        self.canvas.after.get_group("hint-text-color")[0],
                        (
                            self.theme_cls.onSurfaceVariantColor
                            if not self._hint_text_label.text_color_normal
                            else self._hint_text_label.text_color_normal
                        )
                        if not self.error
                        else self._get_error_color(),
                    ),
                )

    def on_disabled(self, instance, disabled: bool) -> None:
        """Fired when the `disabled` value changes."""

        super().on_disabled(instance, disabled)

        def on_disabled(*args):
            if disabled:
                self._set_disabled_colors()
            else:
                self._set_enabled_colors()

        Clock.schedule_once(on_disabled, 0.2)

    def on_error(self, instance, error: bool) -> None:
        """
        Changes the primary colors of the text box to match the `error` value
        (text field is in an error state or not).
        """

        if error:
            if self._max_length_label:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._max_length_label,
                        self.canvas.before.get_group("max-length-color")[0],
                        self._get_error_color(),
                    )
                )
            if self._hint_text_label:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._hint_text_label,
                        self.canvas.after.get_group("hint-text-color")[0],
                        self._get_error_color(),
                    ),
                )
            if self._helper_text_label and self._helper_text_label.mode in (
                "persistent",
                "on_error",
            ):
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._helper_text_label,
                        self.canvas.before.get_group("helper-text-color")[0],
                        self._get_error_color(),
                    )
                )
            if self._trailing_icon:
                Clock.schedule_once(
                    lambda x: self.set_texture_color(
                        self._trailing_icon,
                        self.canvas.before.get_group("trailing-icons-color")[0],
                        self._get_error_color(),
                    )
                )
        else:
            self.on_focus(self, self.focus)

    def on_height(self, instance, value_height: float) -> None:
        if value_height >= self.max_height and self.max_height:
            self.height = self.max_height

    def _set_enabled_colors(self):
        def schedule_set_texture_color(widget, group_name, color):
            Clock.schedule_once(
                lambda x: self.set_texture_color(widget, group_name, color)
            )

        max_length_label_group = self.canvas.before.get_group(
            "max-length-color"
        )
        helper_text_label_group = self.canvas.before.get_group(
            "helper-text-color"
        )
        hint_text_label_group = self.canvas.after.get_group("hint-text-color")
        leading_icon_group = self.canvas.before.get_group("leading-icons-color")
        trailing_icon_group = self.canvas.before.get_group(
            "trailing-icons-color"
        )

        error_color = self._get_error_color()
        on_surface_variant_color = self.theme_cls.onSurfaceVariantColor

        if self._max_length_label:
            schedule_set_texture_color(
                self._max_length_label,
                max_length_label_group[0],
                self._max_length_label.color[:-1] + [1]
                if not self.error
                else error_color,
            )
        if self._helper_text_label:
            schedule_set_texture_color(
                self._helper_text_label,
                helper_text_label_group[0],
                on_surface_variant_color
                if not self._helper_text_label.text_color_focus
                else self._helper_text_label.text_color_focus
                if not self.error
                else error_color,
            )
        if self._hint_text_label:   
            schedule_set_texture_color(
                self._hint_text_label,
                hint_text_label_group[0],
                on_surface_variant_color
                if not self._hint_text_label.text_color_normal
                else self._hint_text_label.text_color_normal
                if not self.error
                else error_color,
            )
        if self._leading_icon:
            schedule_set_texture_color(
                self._leading_icon,
                leading_icon_group[0],
                on_surface_variant_color
                if self._leading_icon.theme_icon_color == "Primary"
                or not self._leading_icon.icon_color_normal
                else self._leading_icon.icon_color_normal,
            )
        if self._trailing_icon:
            schedule_set_texture_color(
                self._trailing_icon,
                trailing_icon_group[0],
                on_surface_variant_color
                if self._trailing_icon.theme_icon_color == "Primary"
                or not self._trailing_icon.icon_color_normal
                else self._trailing_icon.icon_color_normal
                if not self.error
                else error_color,
            )

    def _set_disabled_colors(self):
        def schedule_set_texture_color(widget, group_name, color, opacity):
            Clock.schedule_once(
                lambda x: self.set_texture_color(
                    widget, group_name, color + [opacity]
                )
            )

        max_length_label_group = self.canvas.before.get_group(
            "max-length-color"
        )
        helper_text_label_group = self.canvas.before.get_group(
            "helper-text-color"
        )
        hint_text_label_group = self.canvas.after.get_group("hint-text-color")
        leading_icon_group = self.canvas.before.get_group("leading-icons-color")
        trailing_icon_group = self.canvas.before.get_group(
            "trailing-icons-color"
        )

        disabled_color = self.theme_cls.disabledTextColor[:-1]

        if self._max_length_label:
            schedule_set_texture_color(
                self._max_length_label,
                max_length_label_group[0],
                disabled_color,
                self.text_field_opacity_value_disabled_max_length_label,
            )
        if self._helper_text_label:
            schedule_set_texture_color(
                self._helper_text_label,
                helper_text_label_group[0],
                disabled_color,
                self.text_field_opacity_value_disabled_helper_text_label,
            )
        if self._hint_text_label:
            schedule_set_texture_color(
                self._hint_text_label,
                hint_text_label_group[0],
                disabled_color,
                self.text_field_opacity_value_disabled_hint_text_label,
            )
        if self._leading_icon:
            schedule_set_texture_color(
                self._leading_icon,
                leading_icon_group[0],
                disabled_color,
                self.text_field_opacity_value_disabled_leading_icon,
            )
        if self._trailing_icon:
            schedule_set_texture_color(
                self._trailing_icon,
                trailing_icon_group[0],
                disabled_color,
                self.text_field_opacity_value_disabled_trailing_icon,
            )

    def _get_has_error(self) -> bool:
        """
        Returns `False` or `True` depending on the state of the text field,
        for example when the allowed character limit has been exceeded or when
        the :attr:`~MDTextField.required` parameter is set to `True`.
        """

        if self.validator and self.validator != "phone":
            has_error = {
                "date": self.is_date_valid,
                "email": self.is_email_valid,
                "time": self.is_time_valid,
            }[self.validator](self.text)
            return has_error
        if (
            self._max_length_label
            and len(self.text) > self._max_length_label.max_text_length
        ):
            has_error = True
        else:
            if all((self.required, len(self.text) == 0)):
                has_error = True
            else:
                has_error = False
        return has_error

    def _get_error_color(self):
        return (
            self.theme_cls.errorColor
            if not self.error_color
            else self.error_color
        )

    def _check_text(self, *args) -> None:
        self.set_text(self, self.text)

    def _refresh_hint_text(self):
        """Method override to avoid duplicate hint text texture."""
