# Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from dataclasses import dataclass
from datetime import timedelta
from typing import TYPE_CHECKING, Final, Literal, NamedTuple, cast, overload

from streamlit.elements.lib.form_utils import current_form_id
from streamlit.elements.lib.layout_utils import (
    Height,
    WidthWithoutContent,
    create_layout_config,
)
from streamlit.elements.lib.policies import (
    check_widget_policies,
    maybe_raise_label_warnings,
)
from streamlit.elements.lib.utils import (
    Key,
    LabelVisibility,
    compute_and_register_element_id,
    get_label_visibility_proto_value,
    to_key,
)
from streamlit.errors import (
    StreamlitIncompatibleParametersError,
    StreamlitInvalidParameterTypeError,
    StreamlitValueError,
    StreamlitValueOutOfRangeError,
)
from streamlit.proto.TextArea_pb2 import TextArea as TextAreaProto
from streamlit.proto.TextInput_pb2 import TextInput as TextInputProto
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
    BindOption,
    OnChangeMode,
    PersistStateOption,
    WidgetArgs,
    WidgetCallback,
    WidgetKwargs,
    get_session_state,
    register_widget,
    validate_on_change_mode,
)
from streamlit.string_util import to_help_str, validate_icon_or_emoji
from streamlit.time_util import time_to_seconds

if TYPE_CHECKING:
    from streamlit.delta_generator import DeltaGenerator
    from streamlit.type_util import SupportsStr


@dataclass
class TextInputSerde:
    value: str | None
    max_chars: int | None = None

    def deserialize(self, ui_value: str | None) -> str | None:
        result = ui_value if ui_value is not None else self.value
        if result is not None and self.max_chars is not None:
            result = result[: self.max_chars]
        return result

    def serialize(self, v: str | None) -> str | None:
        return v


@dataclass
class TextAreaSerde:
    value: str | None
    max_chars: int | None = None

    def deserialize(self, ui_value: str | None) -> str | None:
        result = ui_value if ui_value is not None else self.value
        if result is not None and self.max_chars is not None:
            result = result[: self.max_chars]
        return result

    def serialize(self, v: str | None) -> str | None:
        return v


def _parse_text_input_validate(
    validate: str | tuple[str, str] | None,
) -> tuple[str | None, str | None]:
    if validate is None:
        return None, None

    if isinstance(validate, str):
        return validate, None

    if isinstance(validate, tuple):
        if len(validate) == 2 and all(isinstance(item, str) for item in validate):
            return validate
        raise StreamlitValueError(
            "validate",
            ["a regex string", "a (regex, message) tuple of strings"],
        )

    raise StreamlitInvalidParameterTypeError(
        "validate",
        type(validate).__name__,
        ["None", "str", "tuple"],
    )


# Default pause used when ``live=True``.
_DEFAULT_LIVE_DEBOUNCE_MS: Final = 250
# Cap at 1 minute so a typo like "60m" or "2h" cannot schedule a multi-day
# debounce (uint32 would allow ~49 days).
_MAX_LIVE_DEBOUNCE_MS: Final = 60_000


def _parse_text_input_live(live: object) -> int | None:
    """Normalize ``live`` to a debounce in milliseconds.

    Returns ``None`` when live is off so the proto field stays unset. ``0``
    commits on every accepted change.
    """
    if live is False:
        return None
    if live is True:
        return _DEFAULT_LIVE_DEBOUNCE_MS

    # bool is a subclass of int — True/False must be handled before this.
    if isinstance(live, (int, float, timedelta)):
        raise StreamlitInvalidParameterTypeError(
            "live",
            type(live).__name__,
            ["bool", "str"],
            detail=(
                "A number or `datetime.timedelta` is not accepted. "
                "Use `True` for a 250ms pause, or a duration string like "
                "`'300ms'` or `'0.5s'`. `live=300` is not milliseconds "
                "(and `live=0.3` is not seconds); duration units must be "
                "explicit."
            ),
        )

    if not isinstance(live, str):
        raise StreamlitInvalidParameterTypeError(
            "live",
            type(live).__name__,
            ["bool", "str"],
        )

    # Valid duration strings convert to a finite number of seconds.
    seconds = time_to_seconds(live)
    debounce_ms = round(seconds * 1000.0)
    # Only a true zero-length duration is immediate-commit. A positive
    # sub-millisecond value would otherwise round to 0 and silently become
    # the most expensive rerun mode.
    if seconds > 0 and debounce_ms == 0:
        debounce_ms = 1
    # Use ``seconds < 0`` so a sub-millisecond negative does not round to 0.
    if seconds < 0 or debounce_ms > _MAX_LIVE_DEBOUNCE_MS:
        raise StreamlitValueOutOfRangeError(
            "live",
            live,
            "0ms",
            "1m",
            detail=(
                "Use `True` for the 250ms default, `'0ms'` to commit on every "
                "change, or a duration string up to `'1m'`."
            ),
        )
    return debounce_ms


# Default (regex, message) validation rules for the specialized text input types.
# The regexes are JS-flavored (compiled with the "us" flags on the frontend) and
# anchored, so they flow through the same `validate` channel as a user-supplied
# rule. The email regex requires a dotted domain (accepts `user@host.tld`,
# rejects `user@host`). The url regex requires a dotted host but makes the
# `http(s)://` scheme optional (and case-insensitive, so `HTTPS://` works too),
# so both `example.com` and `https://example.com` pass while obvious non-URLs
# (plain words, values with spaces) are rejected. The first host label excludes
# `/` so a bare path (`path/to/file.txt`) or a malformed scheme
# (`https://.example.com`) does not sneak through, while paths after the host
# (`example.com/a/b`) still pass. It stays intentionally permissive since it only
# needs to catch clear mistakes; users who want stricter rules can pass their own
# `validate`.
_EMAIL_VALIDATE: Final = (
    r"^[^\s@]+@[^\s@]+\.[^\s@]+$",
    "Enter a valid email address.",
)
_URL_VALIDATE: Final = (
    r"^([Hh][Tt][Tt][Pp][Ss]?://)?[^\s/.]+\.[^\s]+$",
    "Enter a valid URL.",
)


class _TextInputTypeDefaults(NamedTuple):
    """Type-derived smart defaults for a ``text_input`` ``type`` value.

    A user value always wins; a ``None`` argument falls back to the value here,
    and an empty string (``""``) forces the corresponding feature off.
    """

    proto_type: TextInputProto.Type.ValueType
    icon: str | None
    placeholder: str | None
    validate: tuple[str, str] | None
    autocomplete: str


# Single source of truth mapping each public `type` string to its native input
# type (proto enum) and its overridable smart defaults. Keeping the whole policy
# here means the frontend only needs the enum -> DOM type mapping.
_TEXT_INPUT_TYPE_DEFAULTS: Final[dict[str, _TextInputTypeDefaults]] = {
    "default": _TextInputTypeDefaults(TextInputProto.DEFAULT, None, None, None, ""),
    "password": _TextInputTypeDefaults(
        TextInputProto.PASSWORD, None, None, None, "new-password"
    ),
    "email": _TextInputTypeDefaults(
        TextInputProto.EMAIL,
        ":material/mail:",
        "you@example.com",
        _EMAIL_VALIDATE,
        "email",
    ),
    "url": _TextInputTypeDefaults(
        TextInputProto.URL,
        ":material/link:",
        "https://example.com",
        _URL_VALIDATE,
        "url",
    ),
    "phone": _TextInputTypeDefaults(
        TextInputProto.PHONE,
        ":material/call:",
        "+1 234 567 8900",
        None,
        "tel",
    ),
    "search": _TextInputTypeDefaults(
        TextInputProto.SEARCH,
        ":material/search:",
        "Search",
        None,
        "off",
    ),
}


class TextWidgetsMixin:
    @overload
    def text_input(
        self,
        label: str,
        value: str = "",
        max_chars: int | None = None,
        key: Key | None = None,
        type: Literal[
            "default", "password", "email", "url", "phone", "search"
        ] = "default",
        help: str | None = None,
        autocomplete: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        validate: str | tuple[str, str] | None = None,
        live: str | bool = False,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> str:
        pass

    @overload
    def text_input(
        self,
        label: str,
        value: SupportsStr | None = None,
        max_chars: int | None = None,
        key: Key | None = None,
        type: Literal[
            "default", "password", "email", "url", "phone", "search"
        ] = "default",
        help: str | None = None,
        autocomplete: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        validate: str | tuple[str, str] | None = None,
        live: str | bool = False,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> str | None:
        pass

    @gather_metrics("text_input")
    def text_input(
        self,
        label: str,
        value: str | SupportsStr | None = "",
        max_chars: int | None = None,
        key: Key | None = None,
        type: Literal[
            "default", "password", "email", "url", "phone", "search"
        ] = "default",
        help: str | None = None,
        autocomplete: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        validate: str | tuple[str, str] | None = None,
        live: str | bool = False,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> str | None:
        r"""Display a single-line text input widget.

        Parameters
        ----------
        label : str
            A short label explaining to the user what this input is for.
            The label can optionally contain GitHub-flavored Markdown of the
            following types: Bold, Italics, Strikethroughs, Inline Code, Links,
            and Images. Images display like icons, with a max height equal to
            the font height.

            Unsupported Markdown elements are unwrapped so only their children
            (text contents) render. Common block-level Markdown (headings,
            lists, blockquotes) is automatically escaped and displays as
            literal text in labels.

            See the ``body`` parameter of |st.markdown|_ for additional,
            supported Markdown directives.

            For accessibility reasons, you should never set an empty label, but
            you can hide it with ``label_visibility`` if needed. In the future,
            we may disallow empty labels by raising an exception.

            .. |st.markdown| replace:: ``st.markdown``
            .. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown

        value : object or None
            The text value of this widget when it first renders. This will be
            cast to str internally. If ``None``, will initialize empty and
            return ``None`` until the user provides input. Defaults to empty string.

        max_chars : int or None
            Max number of characters allowed in text input.

        key : str, int, or None
            An optional string or integer to use as the unique key for
            the widget. If this is ``None`` (default), a key will be
            generated for the widget based on the values of the other
            parameters. No two widgets may have the same key. Assigning
            a key stabilizes the widget's identity and preserves its
            state across reruns even when other parameters change.

            .. note::
               Changing ``max_chars`` or the validation regex resets the
               widget even when a key is provided.

            A key lets you read or update the widget's value via
            ``st.session_state[key]``. For more details, see `Widget
            behavior <https://docs.streamlit.io/develop/concepts/architecture/widget-behavior>`_.

            Additionally, if ``key`` is provided, it will be used as a
            CSS class name prefixed with ``st-key-``.

        type : "default", "password", "email", "url", "phone", or "search"
            The type of the text input. This sets the underlying native HTML
            input type (which controls things like the mobile keyboard and
            browser autofill) and, for the specialized types, applies
            overridable smart defaults for ``icon``, ``placeholder``,
            ``validate``, and ``autocomplete``. Defaults to ``"default"``.

            - ``"default"``: A regular single-line text input. No smart
              defaults are applied.
            - ``"password"``: A text input that masks the user's typed value.
              ``autocomplete`` defaults to ``"new-password"``.
            - ``"email"``: An input for email addresses. Defaults to a mail
              icon, a ``you@example.com`` placeholder, email-format
              validation, and ``autocomplete="email"``.
            - ``"url"``: An input for web addresses. Defaults to a link icon,
              an ``https://example.com`` placeholder, URL-format validation,
              and ``autocomplete="url"``.
            - ``"phone"``: An input for phone numbers (numeric keypad on
              mobile). Defaults to a call icon, a ``+1 234 567 8900``
              placeholder, and ``autocomplete="tel"``. No default validation
              is applied because phone formats vary too widely.
            - ``"search"``: A free-text search input with a clear button that
              empties the field. Defaults to a search icon, a ``Search``
              placeholder, and ``autocomplete="off"`` (so private search terms
              don't leak into the browser's autofill history). No default
              validation is applied.

            The smart defaults are only applied when you don't pass a value
            for ``icon``, ``placeholder``, ``validate``, or ``autocomplete``.
            For each of these, ``None`` (or omission) uses the type's default,
            an explicit value overrides it, and ``""`` forces the feature off
            (for example, ``icon=""`` shows no icon).

            .. note::
               The default email and URL validation runs in the user's browser
               and can be bypassed. If the validation is security-relevant, you
               must also validate the value on the server (in your app code)
               after it is submitted.

        help : str or None
            A tooltip that gets displayed next to the widget label. Streamlit
            only displays the tooltip when ``label_visibility="visible"``. If
            this is ``None`` (default), no tooltip is displayed.

            The tooltip can optionally contain GitHub-flavored Markdown,
            including the Markdown directives described in the ``body``
            parameter of ``st.markdown``.

        autocomplete : str or None
            An optional value that will be passed to the <input> element's
            autocomplete property. If this is ``None`` (default), the value is
            derived from ``type``: ``"new-password"`` for ``"password"``,
            ``"email"`` for ``"email"``, ``"url"`` for ``"url"``, ``"tel"``
            for ``"phone"``, ``"off"`` for ``"search"``, and the empty string
            for ``"default"``. Pass an explicit token to override the default,
            or ``""`` to fall back to the browser's default autofill behavior
            (equivalent to not setting the attribute). For more details, see
            https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/autocomplete

        on_change : callable, "rerun", "ignore", or None
            How the text input should respond to value changes. This controls
            whether or not Streamlit reruns the app when the user interacts
            with the text input. ``on_change`` can be one of the following:

            - ``"rerun"`` (default): Streamlit will rerun the app when the
              user commits a new value (pressing Enter, blurring the field,
              clearing a search input, or, when ``live`` is set, after a
              typing pause).

            - ``"ignore"``: Streamlit will not rerun the app when the user
              commits a new value. The text input still updates in the UI.
              The new value is available on the next rerun triggered by
              something else, such as another widget interaction. Ignored
              commits are held in the browser and are lost if the page is
              refreshed before that rerun, unless ``bind="query-params"``
              is set (see ``bind``). Inside ``st.form``, this has no
              effect: the form already defers all commits until submit.

            - A ``callable``: Streamlit will rerun the app and execute the
              ``callable`` as a callback function before the rest of the app.

            - ``None``: This is the same as ``on_change="rerun"``. This value
              exists for backwards compatibility and shouldn't be used.

        args : list or tuple
            An optional list or tuple of args to pass to the callback.

        kwargs : dict
            An optional dict of kwargs to pass to the callback.

        placeholder : str or None
            An optional string displayed when the text input is empty. If
            ``placeholder`` is ``None`` (default), the placeholder is derived
            from ``type`` (for example, ``you@example.com`` for
            ``type="email"``); for ``type="default"`` and ``type="password"``,
            no placeholder is displayed. Pass ``placeholder=""`` to force no
            placeholder even for a specialized type.

        disabled : bool
            An optional boolean that disables the text input if set to
            ``True``. The default is ``False``.

        required : bool
            An optional boolean that requires a non-empty value if set to
            ``True``. The default is ``False``. If this is ``True``, empty
            and whitespace-only values cannot be submitted.

            Outside a form, clearing the field does not rerun the app, and
            the last committed value is kept. Inside a form, submission is
            blocked until the field has a value. The widget still returns
            its default value until the user provides input.

            When used with ``validate``, empty values fail this check and
            skip validation.

            .. note::
               This check runs in the user's browser and can be bypassed.
               If requiredness is security-relevant, you must also check the
               value on the server (in your app code) after it is submitted.

        label_visibility : "visible", "hidden", or "collapsed"
            The visibility of the label. The default is ``"visible"``. If this
            is ``"hidden"``, Streamlit displays an empty spacer instead of the
            label, which can help keep the widget aligned with other widgets.

        icon : str, None
            An optional emoji or icon to display within the input field to the
            left of the value. If ``icon`` is ``None`` (default), the icon is
            derived from ``type`` (for example, a mail icon for
            ``type="email"``); for ``type="default"`` and ``type="password"``,
            no icon is displayed. Pass ``icon=""`` to force no icon even for a
            specialized type. If ``icon`` is a non-empty string, the following
            options are valid:

            - A single-character emoji. For example, you can set ``icon="🚨"``
              or ``icon="🔥"``. Emoji short codes are not supported.

            - An icon from the Material Symbols library (rounded style) in the
              format ``":material/icon_name:"`` where "icon_name" is the name
              of the icon in snake case.

              For example, ``icon=":material/thumb_up:"`` will display the
              Thumb Up icon. Find additional icons in the `Material Symbols \
              <https://fonts.google.com/icons?icon.set=Material+Symbols&icon.style=Rounded>`_
              font library.

            - ``"spinner"``: Displays a spinner as an icon.

        validate : str, tuple[str, str], or None
            An optional client-side validation rule for the input. If this is
            ``None`` (default), no validation is performed for
            ``type="default"`` and ``type="password"``, while ``type="email"``
            and ``type="url"`` fall back to their built-in format validation.
            Pass ``validate=""`` to turn a specialized type's default
            validation off. If this is a string, it is treated as a
            JavaScript-flavored regular expression that the input must match
            before it can be submitted, and a generic error message is shown
            when validation fails. If this is a ``(regex, message)`` tuple, the
            regex is used for client-side validation and the custom ``message``
            is shown when validation fails. Providing a custom message is
            recommended, since generic validation messages are less helpful to
            users. A user-supplied ``validate`` replaces the type's default
            rule.

            For example, pass ``r"^[^@\s]+@[^@\s]+\.[^@\s]+$"`` to require an
            email-like value, or
            ``(r"^\d{3}-\d{3}-\d{4}$", "Use the format 555-123-4567.")`` to
            require a phone number and show a custom error message. Patterns are
            not implicitly anchored; use ``^`` / ``$`` when the whole value must
            match (same semantics as ``st.column_config.TextColumn``).

            Validation runs when the user tries to submit a value: on blur or
            Enter outside a form, after a typing pause when ``live`` is set,
            and on form submission inside a form. Invalid values are not
            submitted, and empty inputs bypass validation.

            Inside a form with ``bind="query-params"``, keystrokes still stage
            the value into widget state (and therefore the URL) before
            submit-time validation runs. Form submission itself still blocks
            invalid values from reaching the server.

            .. note::
               This validation runs in the user's browser and can be bypassed.
               If the validation is security-relevant, you must also validate
               the value on the server (in your app code) after it is
               submitted.

        live : bool or str
            Whether the widget commits while the user types, after a pause.
            Defaults to ``False``.

            - ``False`` (default): Commit on blur, Enter, or clearing a
              ``type="search"`` input.
            - ``True``: Commit after 250ms without further input.
            - A duration string (same format as ``ttl`` in
              ``st.cache_data``, for example ``"250ms"`` or ``"0.5s"``):
              Commit after that pause. Must be between 0 and 1 minute.
              ``"0ms"``, ``"0s"``, and ``"0"`` commit on every change
              (typing, paste, and so on). Bare numbers and
              ``datetime.timedelta`` raise; use a duration string
              (``live=300`` is not milliseconds).

            The 250ms default suits most cases. Consider ``"200ms"`` for
            inexpensive fragment-scoped filtering and ``"300ms"`` to
            ``"500ms"`` when each update performs expensive computation or
            a remote request.

            Inside ``st.form``, ``live`` has no effect: form widgets only
            commit on submit. ``on_change="ignore"`` still wins: each pause
            stages the value without triggering a rerun. Prefer wrapping
            live search UI in ``@st.fragment`` so typing does not rerun the
            rest of the app. Zero-length and very short delays can cause
            many reruns; use them sparingly.

        width : "stretch" or int
            The width of the text input widget. This can be one of the
            following:

            - ``"stretch"`` (default): The width of the widget matches the
              width of the parent container.
            - An integer specifying the width in pixels: The widget has a
              fixed width. If the specified width is greater than the width of
              the parent container, the width of the widget matches the width
              of the parent container.

        bind : "query-params" or None
            Binding mode for syncing the widget's value with a URL query
            parameter. If this is ``None`` (default), the widget's value
            is not synced to the URL. When this is set to
            ``"query-params"``, changes to the widget update the URL, and
            the widget can be initialized or updated through a query
            parameter in the URL. This requires ``key`` to be set. The
            key is used as the query parameter name.

            When the widget's value equals its default, the query
            parameter is removed from the URL to keep it clean. A bound
            query parameter can't be set or deleted through
            ``st.query_params``; it can only be programmatically changed
            through ``st.session_state``.

            This can't be used with ``type="password"``. An empty
            query parameter (e.g., ``?my_key=``) clears the widget.

            When ``on_change="ignore"``, the URL is updated as soon as the
            value is committed (Enter, blur, search-clear, or a live pause
            when ``live`` is set); typing alone does not update it unless
            ``live`` is set. As with widgets inside a form, the URL can
            show a value that Python hasn't received yet. Python receives
            the new value on the next rerun, so a page load or share uses
            the updated URL value.

        persist_state : "page", "session", or None
            How long to preserve the widget's value when it isn't rendered.
            If this is ``None`` (default), the value is lost when the widget
            stops being rendered or the user switches pages. If this is
            ``"page"``, the value is preserved only while the user stays on the
            page where the widget is defined (for example, while the widget is
            conditionally hidden); it is discarded on a page switch and is not
            restored if the user returns to the page. If this is ``"session"``,
            the value is preserved for the entire session, including across
            page switches, so it returns when the user navigates back. This
            requires ``key`` to be set. If ``bind="query-params"`` is also set,
            the binding takes precedence: the value is stored in the URL, so it
            persists across page switches regardless of the ``persist_state``
            scope. For example,
            ``st.text_input("Name", key="name", persist_state="session")`` keeps
            the entered text when the widget is hidden and shown again, or when
            the user navigates to another page and back.

        Returns
        -------
        str or None
            The current value of the text input widget or ``None`` if no value has been
            provided by the user.

        Examples
        --------
        >>> import streamlit as st
        >>>
        >>> title = st.text_input("Movie title", "Life of Brian")
        >>> st.write("The current movie title is", title)

        .. output::
           https://doc-text-input.streamlit.app/
           height: 260px

        Use a specialized ``type`` to get a matching native input, icon,
        placeholder, and validation with zero extra code:

        >>> import streamlit as st
        >>>
        >>> email = st.text_input("Email", type="email")
        >>> if email:
        ...     st.write("We'll reach you at", email)

        .. output::
           https://doc-text-input-email.streamlit.app/
           height: 260px

        Use ``live=True`` with ``type="search"`` for live search. Prefer a
        fragment around the live UI so typing does not rerun the rest of
        the app:

        >>> import streamlit as st
        >>>
        >>> products = [
        ...     {"Product": "Apple", "Category": "Fruit", "Price": 1.20},
        ...     {"Product": "Banana", "Category": "Fruit", "Price": 0.50},
        ...     {"Product": "Cherry", "Category": "Fruit", "Price": 2.50},
        ...     {"Product": "Date", "Category": "Dried fruit", "Price": 3.00},
        ... ]
        >>>
        >>> @st.fragment
        >>> def product_search():
        ...     query = st.text_input("Search products", type="search", live=True)
        ...     matches = [
        ...         p for p in products if query.lower() in p["Product"].lower()
        ...     ]
        ...     st.dataframe(matches, hide_index=True)
        >>>
        >>> product_search()

        .. output::
           https://doc-text-input-live.streamlit.app/
           height: 450px

        """
        ctx = get_script_run_ctx()
        return self._text_input(
            label=label,
            value=value,
            max_chars=max_chars,
            key=key,
            type=type,
            help=help,
            autocomplete=autocomplete,
            on_change=on_change,
            args=args,
            kwargs=kwargs,
            placeholder=placeholder,
            disabled=disabled,
            required=required,
            label_visibility=label_visibility,
            icon=icon,
            validate=validate,
            live=live,
            width=width,
            bind=bind,
            persist_state=persist_state,
            ctx=ctx,
        )

    def _text_input(
        self,
        label: str,
        value: SupportsStr | None = "",
        max_chars: int | None = None,
        key: Key | None = None,
        type: str = "default",
        help: str | None = None,
        autocomplete: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        required: bool = False,
        label_visibility: LabelVisibility = "visible",
        icon: str | None = None,
        validate: str | tuple[str, str] | None = None,
        live: str | bool = False,
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
        ctx: ScriptRunContext | None = None,
    ) -> str | None:
        key = to_key(key)

        on_change_callback = validate_on_change_mode(
            on_change,
            supported_modes=("rerun", "ignore"),
        )
        live_debounce_ms = _parse_text_input_live(live)

        type_defaults = _TEXT_INPUT_TYPE_DEFAULTS.get(type)
        if type_defaults is None:
            raise StreamlitValueError(
                "type", [repr(t) for t in _TEXT_INPUT_TYPE_DEFAULTS]
            )

        check_widget_policies(
            self.dg,
            key,
            on_change_callback,
            default_value=None if value == "" else value,
        )
        label = maybe_raise_label_warnings(label, label_visibility)

        # Make sure value is always string or None:
        value = str(value) if value is not None else None

        # Hash the raw user-provided values, before type-derived defaults below.
        # `type` is already part of the identity, so those defaults (icon,
        # placeholder, validate, autocomplete) stay out of the hash.
        identity_validate_regex, _ = _parse_text_input_validate(validate)

        element_id = compute_and_register_element_id(
            "text_input",
            user_key=key,
            # Explicitly allowlist max_chars and validate so the ID changes when
            # they change, since the widget value might become invalid based on a
            # different max_chars or validation regex. Only the regex (not the
            # message) is used for identity, since the message is purely cosmetic.
            # `required` is hashed for unkeyed widgets but is not on this
            # allowlist: toggling it cannot make a stored value incompatible.
            key_as_main_identity={"max_chars", "validate"},
            dg=self.dg,
            label=label,
            value=value,
            max_chars=max_chars,
            type=type,
            help=help,
            autocomplete=autocomplete,
            placeholder=str(placeholder),
            icon=icon,
            width=width,
            # Normalized milliseconds so `True` and `"250ms"` share an ID.
            live=live_debounce_ms,
            validate=identity_validate_regex,
            required=required,
        )

        # Resolve the effective values from the type defaults now that the
        # widget identity has been computed from the raw values above.
        # Precedence per property: explicit user value -> type default -> off.
        if icon is None:
            icon = type_defaults.icon

        if placeholder is None:
            placeholder = type_defaults.placeholder

        # `validate=None` falls back to the type default (a no-op for
        # `default`/`password`, which define none); `validate=""` and explicit
        # values pass through unchanged. Identity uses the raw user value above,
        # so the type default never enters the widget ID.
        effective_validate = type_defaults.validate if validate is None else validate
        validate_regex, validate_message = _parse_text_input_validate(
            effective_validate
        )

        if autocomplete is None:
            autocomplete = type_defaults.autocomplete

        session_state = get_session_state().filtered_state
        if key is not None and key in session_state and session_state[key] is None:
            value = None

        text_input_proto = TextInputProto()
        text_input_proto.id = element_id
        text_input_proto.label = label
        if value is not None:
            text_input_proto.default = value
        text_input_proto.form_id = current_form_id(self.dg)
        text_input_proto.disabled = disabled
        text_input_proto.required = required
        text_input_proto.label_visibility.value = get_label_visibility_proto_value(
            label_visibility
        )

        if help is not None:
            text_input_proto.help = to_help_str(help)

        if max_chars is not None:
            text_input_proto.max_chars = max_chars

        if placeholder is not None:
            text_input_proto.placeholder = str(placeholder)

        if icon is not None:
            text_input_proto.icon = validate_icon_or_emoji(icon)

        if validate_regex is not None:
            text_input_proto.validate_regex = validate_regex

        if validate_message is not None:
            text_input_proto.validate_message = validate_message

        text_input_proto.type = type_defaults.proto_type

        text_input_proto.autocomplete = autocomplete

        # Prevent binding password inputs to query params (exposes secrets in URL)
        if bind == "query-params" and type == "password":
            raise StreamlitIncompatibleParametersError(
                "bind='query-params'",
                "type='password'",
                explanation="Password values must not appear in URLs.",
            )

        # Set query param key if bound
        if bind == "query-params" and key is not None:
            text_input_proto.query_param_key = str(key)

        if isinstance(on_change, str) and on_change == "ignore":
            text_input_proto.ignore_rerun = True

        if live_debounce_ms is not None:
            text_input_proto.live_debounce_ms = live_debounce_ms

        serde = TextInputSerde(value, max_chars)

        widget_state = register_widget(
            text_input_proto.id,
            on_change_handler=on_change_callback,
            args=args,
            kwargs=kwargs,
            deserializer=serde.deserialize,
            serializer=serde.serialize,
            ctx=ctx,
            value_type="string_value",
            disabled=disabled,
            bind=bind,
            persist_state=persist_state,
            # Text input is clearable (empty string is a valid value)
            clearable=True,
        )

        if widget_state.value_changed:
            if widget_state.value is not None:
                text_input_proto.value = widget_state.value
            text_input_proto.set_value = True

        layout_config = create_layout_config(width=width)

        self.dg._enqueue(
            "text_input",
            text_input_proto,
            layout_config=layout_config,
            has_one_shot_effect=widget_state.value_changed,
        )
        return widget_state.value

    @overload
    def text_area(
        self,
        label: str,
        value: str = "",
        height: Height | None = None,
        max_chars: int | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        label_visibility: LabelVisibility = "visible",
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> str:
        pass

    @overload
    def text_area(
        self,
        label: str,
        value: SupportsStr | None = None,
        height: Height | None = None,
        max_chars: int | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        label_visibility: LabelVisibility = "visible",
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> str | None:
        pass

    @gather_metrics("text_area")
    def text_area(
        self,
        label: str,
        value: str | SupportsStr | None = "",
        height: Height | None = None,
        max_chars: int | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        label_visibility: LabelVisibility = "visible",
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
    ) -> str | None:
        r"""Display a multi-line text input widget.

        Parameters
        ----------
        label : str
            A short label explaining to the user what this input is for.
            The label can optionally contain GitHub-flavored Markdown of the
            following types: Bold, Italics, Strikethroughs, Inline Code, Links,
            and Images. Images display like icons, with a max height equal to
            the font height.

            Unsupported Markdown elements are unwrapped so only their children
            (text contents) render. Common block-level Markdown (headings,
            lists, blockquotes) is automatically escaped and displays as
            literal text in labels.

            See the ``body`` parameter of |st.markdown|_ for additional,
            supported Markdown directives.

            For accessibility reasons, you should never set an empty label, but
            you can hide it with ``label_visibility`` if needed. In the future,
            we may disallow empty labels by raising an exception.

            .. |st.markdown| replace:: ``st.markdown``
            .. _st.markdown: https://docs.streamlit.io/develop/api-reference/text/st.markdown

        value : object or None
            The text value of this widget when it first renders. This will be
            cast to str internally. If ``None``, will initialize empty and
            return ``None`` until the user provides input. Defaults to empty string.

        height : "content", "stretch", int, or None
            The height of the text area widget. This can be one of the
            following:

            - ``None`` (default): The height of the widget fits three lines.
            - ``"content"``: The height of the widget matches the
              height of its content.
            - ``"stretch"``: The height of the widget matches the height of
              its content or the height of the parent container, whichever is
              larger. If the widget is not in a parent container, the height
              of the widget matches the height of its content.
            - An integer specifying the height in pixels: The widget has a
              fixed height. If the content is larger than the specified
              height, scrolling is enabled.

            The widget's height can't be smaller than the height of two lines.
            When ``label_visibility="collapsed"``, the minimum height is 68
            pixels. Otherwise, the minimum height is 98 pixels.

        max_chars : int or None
            Maximum number of characters allowed in text area.

        key : str, int, or None
            An optional string or integer to use as the unique key for
            the widget. If this is ``None`` (default), a key will be
            generated for the widget based on the values of the other
            parameters. No two widgets may have the same key. Assigning
            a key stabilizes the widget's identity and preserves its
            state across reruns even when other parameters change.

            .. note::
               Changing ``max_chars`` resets the widget even when a key
               is provided.

            A key lets you read or update the widget's value via
            ``st.session_state[key]``. For more details, see `Widget
            behavior <https://docs.streamlit.io/develop/concepts/architecture/widget-behavior>`_.

            Additionally, if ``key`` is provided, it will be used as a
            CSS class name prefixed with ``st-key-``.

        help : str or None
            A tooltip that gets displayed next to the widget label. Streamlit
            only displays the tooltip when ``label_visibility="visible"``. If
            this is ``None`` (default), no tooltip is displayed.

            The tooltip can optionally contain GitHub-flavored Markdown,
            including the Markdown directives described in the ``body``
            parameter of ``st.markdown``.

        on_change : callable, "rerun", "ignore", or None
            How the text area should respond to value changes. This controls
            whether or not Streamlit reruns the app when the user interacts
            with the text area. ``on_change`` can be one of the following:

            - ``"rerun"`` (default): Streamlit will rerun the app when the
              user commits a new value (blurring the field, or pressing
              Ctrl+Enter / Cmd+Enter).

            - ``"ignore"``: Streamlit will not rerun the app when the user
              commits a new value. The text area still updates in the UI.
              The new value is available on the next rerun triggered by
              something else, such as another widget interaction. Ignored
              commits are held in the browser and are lost if the page is
              refreshed before that rerun, unless ``bind="query-params"``
              is set (see ``bind``). Inside ``st.form``, this has no
              effect: the form already defers all commits until submit.

            - A ``callable``: Streamlit will rerun the app and execute the
              ``callable`` as a callback function before the rest of the app.

            - ``None``: This is the same as ``on_change="rerun"``. This value
              exists for backwards compatibility and shouldn't be used.

        args : list or tuple
            An optional list or tuple of args to pass to the callback.

        kwargs : dict
            An optional dict of kwargs to pass to the callback.

        placeholder : str or None
            An optional string displayed when the text area is empty. If None,
            no text is displayed.

        disabled : bool
            An optional boolean that disables the text area if set to ``True``.
            The default is ``False``.

        label_visibility : "visible", "hidden", or "collapsed"
            The visibility of the label. The default is ``"visible"``. If this
            is ``"hidden"``, Streamlit displays an empty spacer instead of the
            label, which can help keep the widget aligned with other widgets.
            If this is ``"collapsed"``, Streamlit displays no label or spacer.

        width : "stretch" or int
            The width of the text area widget. This can be one of the
            following:

            - ``"stretch"`` (default): The width of the widget matches the
              width of the parent container.
            - An integer specifying the width in pixels: The widget has a
              fixed width. If the specified width is greater than the width of
              the parent container, the width of the widget matches the width
              of the parent container.

        bind : "query-params" or None
            Binding mode for syncing the widget's value with a URL query
            parameter. If this is ``None`` (default), the widget's value
            is not synced to the URL. When this is set to
            ``"query-params"``, changes to the widget update the URL, and
            the widget can be initialized or updated through a query
            parameter in the URL. This requires ``key`` to be set. The
            key is used as the query parameter name.

            When the widget's value equals its default, the query
            parameter is removed from the URL to keep it clean. A bound
            query parameter can't be set or deleted through
            ``st.query_params``; it can only be programmatically changed
            through ``st.session_state``.

            An empty query parameter (e.g., ``?my_key=``) clears the
            widget.

            When ``on_change="ignore"``, the URL is updated as soon as the
            value is committed (blurring the field, or pressing Ctrl+Enter /
            Cmd+Enter); typing alone does not update it. As with widgets
            inside a form, the URL can show a value that Python hasn't
            received yet. Python receives the new value on the next rerun,
            so a page load or share uses the updated URL value.

        persist_state : "page", "session", or None
            How long to preserve the widget's value when it isn't rendered.
            If this is ``None`` (default), the value is lost when the widget
            stops being rendered or the user switches pages. If this is
            ``"page"``, the value is preserved only while the user stays on the
            page where the widget is defined (for example, while the widget is
            conditionally hidden); it is discarded on a page switch and is not
            restored if the user returns to the page. If this is ``"session"``,
            the value is preserved for the entire session, including across
            page switches, so it returns when the user navigates back. This
            requires ``key`` to be set. If ``bind="query-params"`` is also set,
            the binding takes precedence: the value is stored in the URL, so it
            persists across page switches regardless of the ``persist_state``
            scope.

        Returns
        -------
        str or None
            The current value of the text area widget or ``None`` if no value has been
            provided by the user.

        Examples
        --------
        >>> import streamlit as st
        >>>
        >>> txt = st.text_area(
        ...     "Text to analyze",
        ...     "It was the best of times, it was the worst of times, it was the age of "
        ...     "wisdom, it was the age of foolishness, it was the epoch of belief, it "
        ...     "was the epoch of incredulity, it was the season of Light, it was the "
        ...     "season of Darkness, it was the spring of hope, it was the winter of "
        ...     "despair, (...)",
        ... )
        >>>
        >>> st.write(f"You wrote {len(txt)} characters.")

        .. output::
           https://doc-text-area.streamlit.app/
           height: 300px

        """
        ctx = get_script_run_ctx()
        return self._text_area(
            label=label,
            value=value,
            height=height,
            max_chars=max_chars,
            key=key,
            help=help,
            on_change=on_change,
            args=args,
            kwargs=kwargs,
            placeholder=placeholder,
            disabled=disabled,
            label_visibility=label_visibility,
            width=width,
            bind=bind,
            persist_state=persist_state,
            ctx=ctx,
        )

    def _text_area(
        self,
        label: str,
        value: SupportsStr | None = "",
        height: Height | None = None,
        max_chars: int | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_change: WidgetCallback | OnChangeMode | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        placeholder: str | None = None,
        disabled: bool = False,
        label_visibility: LabelVisibility = "visible",
        width: WidthWithoutContent = "stretch",
        bind: BindOption = None,
        persist_state: PersistStateOption = None,
        ctx: ScriptRunContext | None = None,
    ) -> str | None:
        key = to_key(key)
        on_change_callback = validate_on_change_mode(
            on_change,
            supported_modes=("rerun", "ignore"),
        )

        check_widget_policies(
            self.dg,
            key,
            on_change_callback,
            default_value=None if value == "" else value,
        )
        label = maybe_raise_label_warnings(label, label_visibility)

        value = str(value) if value is not None else None

        element_id = compute_and_register_element_id(
            "text_area",
            user_key=key,
            # Explicitly whitelist max_chars to make sure the ID changes when it changes
            # since the widget value might become invalid based on a different max_chars
            key_as_main_identity={"max_chars"},
            dg=self.dg,
            label=label,
            value=value,
            height=height,
            max_chars=max_chars,
            help=help,
            placeholder=str(placeholder),
            width=width,
        )

        session_state = get_session_state().filtered_state
        if key is not None and key in session_state and session_state[key] is None:
            value = None

        text_area_proto = TextAreaProto()
        text_area_proto.id = element_id
        text_area_proto.label = label
        if value is not None:
            text_area_proto.default = value
        text_area_proto.form_id = current_form_id(self.dg)
        text_area_proto.disabled = disabled
        text_area_proto.label_visibility.value = get_label_visibility_proto_value(
            label_visibility
        )

        if help is not None:
            text_area_proto.help = to_help_str(help)

        if max_chars is not None:
            text_area_proto.max_chars = max_chars

        if placeholder is not None:
            text_area_proto.placeholder = str(placeholder)

        # Set query param key if bound
        if bind == "query-params" and key is not None:
            text_area_proto.query_param_key = str(key)

        if isinstance(on_change, str) and on_change == "ignore":
            text_area_proto.ignore_rerun = True

        serde = TextAreaSerde(value, max_chars)
        widget_state = register_widget(
            text_area_proto.id,
            on_change_handler=on_change_callback,
            args=args,
            kwargs=kwargs,
            deserializer=serde.deserialize,
            serializer=serde.serialize,
            ctx=ctx,
            value_type="string_value",
            disabled=disabled,
            bind=bind,
            persist_state=persist_state,
            # Text area is clearable (empty string is a valid value)
            clearable=True,
        )

        if widget_state.value_changed:
            if widget_state.value is not None:
                text_area_proto.value = widget_state.value
            text_area_proto.set_value = True

        if height is None:
            # We want to maintain the same approximately three lines of text height
            # for the text input when the label is collapsed.
            # These numbers are for the entire element including the label and
            # padding.
            height = 122 if label_visibility != "collapsed" else 94

        layout_config = create_layout_config(
            width=width, height=height, allow_content_height=True
        )

        self.dg._enqueue(
            "text_area",
            text_area_proto,
            layout_config=layout_config,
            has_one_shot_effect=widget_state.value_changed,
        )
        return widget_state.value

    @property
    def dg(self) -> DeltaGenerator:
        """The associated DeltaGenerator."""
        return cast("DeltaGenerator", self)
