# 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

import io
import mimetypes
import os
from collections.abc import Callable
from dataclasses import dataclass
from pathlib import Path
from textwrap import dedent
from typing import (
    TYPE_CHECKING,
    BinaryIO,
    Final,
    Literal,
    TextIO,
    TypeAlias,
    cast,
    overload,
)

from streamlit import runtime
from streamlit.elements.lib.form_utils import current_form_id, is_in_form
from streamlit.elements.lib.layout_utils import Width, create_layout_config
from streamlit.elements.lib.policies import check_widget_policies
from streamlit.elements.lib.shortcut_utils import normalize_shortcut
from streamlit.elements.lib.utils import (
    Key,
    compute_and_register_element_id,
    save_for_app_testing,
    to_key,
)
from streamlit.errors import (
    StreamlitAPIException,
    StreamlitMissingPageLabelError,
    StreamlitPageNotFoundError,
    StreamlitValueError,
)
from streamlit.file_util import get_main_script_directory, normalize_path_join
from streamlit.navigation.page import Page, _validate_registered_page
from streamlit.proto.Button_pb2 import Button as ButtonProto
from streamlit.proto.ButtonLikeIconPosition_pb2 import (
    ButtonLikeIconPosition as ProtoButtonLikeIconPosition,
)
from streamlit.proto.DownloadButton_pb2 import DownloadButton as DownloadButtonProto
from streamlit.proto.LinkButton_pb2 import LinkButton as LinkButtonProto
from streamlit.proto.PageLink_pb2 import PageLink as PageLinkProto
from streamlit.runtime.download_data_util import convert_data_to_bytes_and_infer_mime
from streamlit.runtime.metrics_util import gather_metrics
from streamlit.runtime.pages_manager import PagesManager
from streamlit.runtime.scriptrunner import ScriptRunContext, get_script_run_ctx
from streamlit.runtime.state import (
    WidgetArgs,
    WidgetCallback,
    WidgetKwargs,
    register_widget,
)
from streamlit.runtime.state.query_params import process_query_params
from streamlit.string_util import validate_icon_or_emoji
from streamlit.url_util import is_url
from streamlit.util import in_sidebar

if TYPE_CHECKING:
    from streamlit.delta_generator import DeltaGenerator
    from streamlit.runtime.state.query_params import QueryParamsInput

FORM_DOCS_INFO: Final = """

For more information, refer to the
[documentation for forms](https://docs.streamlit.io/develop/api-reference/execution-flow/st.form).
"""

DownloadButtonDataType: TypeAlias = (
    str
    | bytes
    | TextIO
    | BinaryIO
    | io.RawIOBase
    | Callable[[], str | bytes | TextIO | BinaryIO | io.RawIOBase]
)

IconPosition: TypeAlias = Literal["left", "right"]

_DEFAULT_ICON_POSITION: Final[IconPosition] = "left"
_VALID_ICON_POSITIONS: Final[tuple[IconPosition, ...]] = ("left", "right")


def _normalize_icon_position(
    icon_position: IconPosition | str | None,
) -> IconPosition:
    if icon_position is None:
        return _DEFAULT_ICON_POSITION

    if icon_position not in _VALID_ICON_POSITIONS:
        raise StreamlitValueError("icon_position", ["'left'", "'right'"])

    return icon_position


def _icon_position_to_proto(
    icon_position: IconPosition,
) -> ProtoButtonLikeIconPosition.ValueType:
    return (
        ProtoButtonLikeIconPosition.RIGHT
        if icon_position == "right"
        else ProtoButtonLikeIconPosition.LEFT
    )


@dataclass
class ButtonSerde:
    def serialize(self, v: bool) -> bool:
        return bool(v)

    def deserialize(self, ui_value: bool | None) -> bool:
        return ui_value or False


class ButtonMixin:
    @gather_metrics("button")
    def button(
        self,
        label: str,
        key: Key | None = None,
        help: str | None = None,
        on_click: WidgetCallback | None = None,
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        use_container_width: bool | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> bool:
        r"""Display a button widget.

        Parameters
        ----------
        label : str
            A short label explaining to the user what this button 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.

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

        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.

            A key lets you access the widget's value via
            ``st.session_state[key]`` (read-only). 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 when the button is hovered over. 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_click : callable
            An optional callback invoked when this button is clicked.

        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.

        type : "primary", "secondary", or "tertiary"
            An optional string that specifies the button type. This can be one
            of the following:

            - ``"primary"``: The button's background is the app's primary color
              for additional emphasis.
            - ``"secondary"`` (default): The button's background coordinates
              with the app's background color for normal emphasis.
            - ``"tertiary"``: The button is plain text without a border or
              background for subtlety.

        icon : str or None
            An optional emoji or icon to display next to the button label. If ``icon``
            is ``None`` (default), no icon is displayed. If ``icon`` is a
            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.

        icon_position : "left" or "right"
            The position of the icon relative to the button label. This
            defaults to ``"left"``.

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

        use_container_width : bool
            Whether to expand the button's width to fill its parent container.
            If ``use_container_width`` is ``False`` (default), Streamlit sizes
            the button to fit its contents. If ``use_container_width`` is
            ``True``, the width of the button matches its parent container.

            In both cases, if the contents of the button are wider than the
            parent container, the contents will line wrap.

            .. deprecated::
                ``use_container_width`` is deprecated and will be removed in a
                future release. For ``use_container_width=True``, use
                ``width="stretch"``. For ``use_container_width=False``, use
                ``width="content"``.

        width : "content", "stretch", or int
            The width of the button. This can be one of the following:

            - ``"content"`` (default): The width of the button matches the
              width of its content, but doesn't exceed the width of the parent
              container.
            - ``"stretch"``: The width of the button matches the width of the
              parent container.
            - An integer specifying the width in pixels: The button has a
              fixed width. If the specified width is greater than the width of
              the parent container, the width of the button matches the width
              of the parent container.

        shortcut : str or None
            An optional keyboard shortcut that triggers the button. This can be
            one of the following strings:

            - A single alphanumeric key like ``"K"`` or ``"4"``.
            - A function key like ``"F11"``.
            - A special key like ``"Enter"``, ``"Esc"``, or ``"Tab"``.
            - Any of the above combined with modifiers. For example, you can use
              ``"Ctrl+K"`` or ``"Cmd+Shift+O"``.

            .. important::
                The keys ``"C"`` and ``"R"`` are reserved and can't be used,
                even with modifiers. Punctuation keys like ``"."`` and ``","``
                aren't currently supported. Some combinations such as
                ``"Ctrl+T"``, ``"Ctrl+W"``, ``"Ctrl+PageUp"``,
                ``"Ctrl+PageDown"``, and ``"F11"`` are reserved by the browser
                or operating system and may never reach Streamlit.

            The following special keys are supported: Backspace, Delete, Down,
            End, Enter, Esc, Home, Left, PageDown, PageUp, Right, Space, Tab,
            and Up.

            The following modifiers are supported: Alt, Ctrl, Cmd, Meta, Mod,
            Option, Shift.

            - Ctrl, Cmd, Meta, and Mod are interchangeable and will display to
              the user to match their platform.
            - Option and Alt are interchangeable and will display to the user
              to match their platform.

        wrap : bool or None
            Whether the button label can wrap onto multiple lines. This can be
            one of the following:

            - ``None`` (default): Streamlit decides based on the surrounding
              layout. Inside a horizontal container, the button keeps its
              standard, single-row height and truncates an overflowing label
              with an ellipsis; in other layouts, the label wraps onto
              additional lines.
            - ``True``: If the label is too wide for the button, it wraps onto
              additional lines and the button grows taller.
            - ``False``: The button keeps its standard, single-row height. A
              label that is too wide is truncated with an ellipsis.

            When the button keeps a single-row label and no ``help`` is set,
            hovering reveals the full label. Icons and keyboard shortcuts
            remain visible.

        Returns
        -------
        bool
            True if the button was clicked on the last run of the app,
            False otherwise.

        Examples
        --------
        **Example 1: Customize your button type**

        >>> import streamlit as st
        >>>
        >>> st.button("Reset", type="primary")
        >>> if st.button("Say hello"):
        ...     st.write("Why hello there")
        ... else:
        ...     st.write("Goodbye")
        >>>
        >>> if st.button("Aloha", type="tertiary"):
        ...     st.write("Ciao")

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

        **Example 2: Add icons to your button**

        Although you can add icons to your buttons through Markdown, the
        ``icon`` parameter is a convenient and consistent alternative.

        >>> import streamlit as st
        >>>
        >>> left, middle, right = st.columns(3)
        >>> if left.button("Plain button", width="stretch"):
        ...     left.markdown("You clicked the plain button.")
        >>> if middle.button("Emoji button", icon="😃", width="stretch"):
        ...     middle.markdown("You clicked the emoji button.")
        >>> if right.button("Material button", icon=":material/mood:", width="stretch"):
        ...     right.markdown("You clicked the Material button.")

        .. output::
           https://doc-button-icons.streamlit.app/
           height: 220px

        **Example 3: Use keyboard shortcuts**

        The following example shows how to use keyboard shortcuts to trigger a
        button. If you use any of the platform-dependent modifiers (Ctrl, Cmd,
        or Mod), they are interpreted interchangeably and always displayed to
        the user to match their platform.

        >>> import streamlit as st
        >>>
        >>> with st.container(horizontal=True, horizontal_alignment="distribute"):
        >>>     "`A`" if st.button("A", shortcut="A") else "` `"
        >>>     "`S`" if st.button("S", shortcut="Ctrl+S") else "` `"
        >>>     "`D`" if st.button("D", shortcut="Cmd+Shift+D") else "` `"
        >>>     "`F`" if st.button("F", shortcut="Mod+Alt+Shift+F") else "` `"

        .. output::
           https://doc-button-shortcuts.streamlit.app/
           height: 220px

        """
        key = to_key(key)
        ctx = get_script_run_ctx()

        if use_container_width is not None:
            width = "stretch" if use_container_width else "content"

        # Checks whether the entered button type is one of the allowed options
        if type not in {"primary", "secondary", "tertiary"}:
            raise StreamlitValueError(
                "type", ["'primary'", "'secondary'", "'tertiary'"]
            )

        normalized_icon_position = _normalize_icon_position(icon_position)

        return self.dg._button(
            label,
            key,
            help,
            is_form_submitter=False,
            on_click=on_click,
            args=args,
            kwargs=kwargs,
            disabled=disabled,
            type=type,
            icon=icon,
            icon_position=normalized_icon_position,
            ctx=ctx,
            width=width,
            shortcut=shortcut,
            wrap=wrap,
        )

    @gather_metrics("download_button")
    def download_button(
        self,
        label: str,
        data: DownloadButtonDataType,
        file_name: str | None = None,
        mime: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_click: WidgetCallback | Literal["rerun", "ignore"] | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        use_container_width: bool | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> bool:
        r"""Display a download button widget.

        This is useful when you would like to provide a way for your users
        to download a file directly from your app.

        If you pass the data directly to the ``data`` parameter, then the data
        is stored in-memory while the user is connected. It's a good idea to
        keep file sizes under a couple hundred megabytes to conserve memory or
        use deferred data generation by passing a callable to the ``data``
        parameter.

        If you want to prevent your app from rerunning when a user clicks the
        download button, wrap the download button in a `fragment
        <https://docs.streamlit.io/develop/concepts/architecture/fragments>`_.

        Parameters
        ----------
        label : str
            A short label explaining to the user what this button 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.

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

        data : str, bytes, file-like, or callable
            The contents of the file to be downloaded or a callable that
            returns the contents of the file.

            File contents can be a string, bytes, or file-like object.
            File-like objects include ``io.BytesIO``, ``io.StringIO``, or any
            class that implements the abstract base class ``io.RawIOBase``.

            If a callable is passed, it is executed when the user clicks
            the download button and runs on a separate thread from the
            resulting script rerun. This deferred generation is helpful for
            large files to avoid blocking the page script. The callable can't
            accept any arguments. If any Streamlit commands are executed inside
            the callable, they will be ignored.

            To prevent unnecessary recomputation, use caching when converting
            your data for download. For more information, see the Example 1
            below.

        file_name: str
            An optional string to use as the name of the file to be downloaded,
            such as ``"my_file.csv"``. If not specified, the name will be
            automatically generated.

        mime : str or None
            The MIME type of the data. If this is ``None`` (default), Streamlit
            sets the MIME type depending on the value of ``data`` as follows:

            - If ``data`` is a file object with a string ``name`` attribute
              (e.g. a file opened with ``open()``), Streamlit first tries to
              guess the MIME type from the file name (``file_name`` if
              specified, otherwise ``data.name``).
            - If ``data`` is a string or textual file (i.e. ``str`` or
              ``io.TextIOWrapper`` object), Streamlit uses the "text/plain"
              MIME type.
            - If ``data`` is a binary file or bytes (i.e. ``bytes``,
              ``io.BytesIO``, ``io.BufferedReader``, or ``io.RawIOBase``
              object), Streamlit uses the "application/octet-stream" MIME type.

            For more information about MIME types, see
            https://www.iana.org/assignments/media-types/media-types.xhtml.

        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.

            A key lets you access the widget's value via
            ``st.session_state[key]`` (read-only). 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 when the button is hovered over. 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_click : callable, "rerun", "ignore", or None
            How the button should respond to user interaction. This controls
            whether or not the button triggers a rerun and if a callback
            function is called. This can be one of the following values:

            - ``"rerun"`` (default): The user downloads the file and the app
              reruns. No callback function is called.
            - ``"ignore"``: The user downloads the file and the app doesn't
              rerun. No callback function is called.
            - A ``callable``: The user downloads the file and app reruns. The
              callable is called before the rest of the app.
            - ``None``: This is same as ``on_click="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.

        type : "primary", "secondary", or "tertiary"
            An optional string that specifies the button type. This can be one
            of the following:

            - ``"primary"``: The button's background is the app's primary color
              for additional emphasis.
            - ``"secondary"`` (default): The button's background coordinates
              with the app's background color for normal emphasis.
            - ``"tertiary"``: The button is plain text without a border or
              background for subtlety.

        icon : str or None
            An optional emoji or icon to display next to the button label. If ``icon``
            is ``None`` (default), no icon is displayed. If ``icon`` is a
            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.

        icon_position : "left" or "right"
            The position of the icon relative to the button label. This
            defaults to ``"left"``.

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

        use_container_width : bool
            Whether to expand the button's width to fill its parent container.
            If ``use_container_width`` is ``False`` (default), Streamlit sizes
            the button to fit its contents. If ``use_container_width`` is
            ``True``, the width of the button matches its parent container.

            In both cases, if the contents of the button are wider than the
            parent container, the contents will line wrap.

            .. deprecated::
                ``use_container_width`` is deprecated and will be removed in a
                future release. For ``use_container_width=True``, use
                ``width="stretch"``. For ``use_container_width=False``, use
                ``width="content"``.

        width : "content", "stretch", or int
            The width of the download button. This can be one of the following:

            - ``"content"`` (default): The width of the button matches the
              width of its content, but doesn't exceed the width of the parent
              container.
            - ``"stretch"``: The width of the button matches the width of the
              parent container.
            - An integer specifying the width in pixels: The button has a
              fixed width. If the specified width is greater than the width of
              the parent container, the width of the button matches the width
              of the parent container.

        shortcut : str or None
            An optional keyboard shortcut that triggers the button. This can be
            one of the following strings:

            - A single alphanumeric key like ``"K"`` or ``"4"``.
            - A function key like ``"F11"``.
            - A special key like ``"Enter"``, ``"Esc"``, or ``"Tab"``.
            - Any of the above combined with modifiers. For example, you can use
              ``"Ctrl+K"`` or ``"Cmd+Shift+O"``.

            .. important::
                The keys ``"C"`` and ``"R"`` are reserved and can't be used,
                even with modifiers. Punctuation keys like ``"."`` and ``","``
                aren't currently supported. Some combinations such as
                ``"Ctrl+T"``, ``"Ctrl+W"``, ``"Ctrl+PageUp"``,
                ``"Ctrl+PageDown"``, and ``"F11"`` are reserved by the browser
                or operating system and may never reach Streamlit.

            For a list of supported keys and modifiers, see the documentation
            for |st.button|_.

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

        wrap : bool or None
            Whether the button label can wrap onto multiple lines. This can be
            one of the following:

            - ``None`` (default): Streamlit decides based on the surrounding
              layout. Inside a horizontal container, the button keeps its
              standard, single-row height and truncates an overflowing label
              with an ellipsis; in other layouts, the label wraps onto
              additional lines.
            - ``True``: If the label is too wide for the button, it wraps onto
              additional lines and the button grows taller.
            - ``False``: The button keeps its standard, single-row height. A
              label that is too wide is truncated with an ellipsis.

            When the button keeps a single-row label and no ``help`` is set,
            hovering reveals the full label. Icons and keyboard shortcuts
            remain visible.

        Returns
        -------
        bool
            True if the button was clicked on the last run of the app,
            False otherwise.

        Examples
        --------
        **Example 1: Download a dataframe as a CSV file**

        When working with a large dataframe, it's recommended to fetch your
        data with a cached function. When working with a download button, it's
        similarly recommended to convert your data into a downloadable format
        with a cached function. Caching ensures that the app reruns
        efficiently.

        >>> import streamlit as st
        >>> import pandas as pd
        >>> import numpy as np
        >>>
        >>> @st.cache_data
        >>> def get_data():
        >>>     df = pd.DataFrame(
        ...         np.random.randn(50, 20), columns=("col %d" % i for i in range(20))
        ...     )
        >>>     return df
        >>>
        >>> @st.cache_data
        >>> def convert_for_download(df):
        >>>     return df.to_csv().encode("utf-8")
        >>>
        >>> df = get_data()
        >>> csv = convert_for_download(df)
        >>>
        >>> st.download_button(
        ...     label="Download CSV",
        ...     data=csv,
        ...     file_name="data.csv",
        ...     mime="text/csv",
        ...     icon=":material/download:",
        ... )

        .. output::
           https://doc-download-button-csv.streamlit.app/
           height: 200px

        **Example 2: Download a string as a text file**

        If you pass a string to the ``data`` argument, Streamlit will
        automatically use the "text/plain" MIME type.

        When you have a widget (like a text area) affecting the value of your
        download, it's recommended to use another button to prepare the
        download. In this case, use ``on_click="ignore"`` in your download
        button to prevent the download button from rerunning your app. This
        turns the download button into a frontend-only element that can be
        nested in another button.

        Without a preparation button, a user can type something into the text
        area and immediately click the download button. Because a download is
        initiated concurrently with the app rerun, this can create a race-like
        condition where the user doesn't see the updated data in their
        download.

        .. important::
           Even when you prevent your download button from triggering a rerun,
           another widget with a pending change can still trigger a rerun. For
           example, if a text area has a pending change when a user clicks a
           download button, the text area will trigger a rerun.

        >>> import streamlit as st
        >>>
        >>> message = st.text_area("Message", value="Lorem ipsum.\nStreamlit is cool.")
        >>>
        >>> if st.button("Prepare download"):
        >>>     st.download_button(
        ...         label="Download text",
        ...         data=message,
        ...         file_name="message.txt",
        ...         on_click="ignore",
        ...         type="primary",
        ...         icon=":material/download:",
        ...     )

        .. output::
           https://doc-download-button-text.streamlit.app/
           height: 250px

        **Example 3: Download a file**

        Use a context manager to open and read a local file on your Streamlit
        server. Pass the ``io.BufferedReader`` object directly to ``data``.
        Remember to specify the MIME type if you don't want the default
        type of ``"application/octet-stream"`` for generic binary data. In the
        example below, the MIME type is set to ``"image/png"`` for a PNG file.

        >>> import streamlit as st
        >>>
        >>> with open("flower.png", "rb") as file:
        ...     st.download_button(
        ...         label="Download image",
        ...         data=file,
        ...         file_name="flower.png",
        ...         mime="image/png",
        ...     )

        .. output::
           https://doc-download-button-file.streamlit.app/
           height: 200px

        **Example 4: Generate the data on-click with a callable**

        Pass a callable to ``data`` to generate the bytes lazily when the user
        clicks the button. Streamlit commands inside this callable are ignored.
        The callable can't accept any arguments and must return a file-like
        object.

        >>> import streamlit as st
        >>> import time
        >>>
        >>> def make_report():
        >>>     time.sleep(1)
        >>>     return "col1,col2\n1,2\n3,4".encode("utf-8")
        >>>
        >>> st.download_button(
        ...     label="Download report",
        ...     data=make_report,
        ...     file_name="report.csv",
        ...     mime="text/csv",
        ... )

        .. output::
           https://doc-download-button-deferred.streamlit.app/
           height: 200px

        """
        ctx = get_script_run_ctx()

        if use_container_width is not None:
            width = "stretch" if use_container_width else "content"

        if type not in {"primary", "secondary", "tertiary"}:
            raise StreamlitValueError(
                "type", ["'primary'", "'secondary'", "'tertiary'"]
            )

        normalized_icon_position = _normalize_icon_position(icon_position)

        return self._download_button(
            label=label,
            data=data,
            file_name=file_name,
            mime=mime,
            key=key,
            help=help,
            on_click=on_click,
            args=args,
            kwargs=kwargs,
            type=type,
            icon=icon,
            icon_position=normalized_icon_position,
            disabled=disabled,
            ctx=ctx,
            width=width,
            shortcut=shortcut,
            wrap=wrap,
        )

    @overload
    def link_button(
        self,
        label: str,
        url: str,
        *,
        key: Key | None = None,
        on_click: Literal["ignore"] = "ignore",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        help: str | None = None,
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        use_container_width: bool | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> DeltaGenerator: ...

    @overload
    def link_button(
        self,
        label: str,
        url: str,
        *,
        key: Key | None = None,
        on_click: Literal["rerun"],
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        help: str | None = None,
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        use_container_width: bool | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> bool: ...

    @overload
    def link_button(
        self,
        label: str,
        url: str,
        *,
        key: Key | None = None,
        on_click: WidgetCallback,
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        help: str | None = None,
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        use_container_width: bool | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> bool: ...

    @gather_metrics("link_button")
    def link_button(
        self,
        label: str,
        url: str,
        *,
        key: Key | None = None,
        on_click: WidgetCallback | Literal["rerun", "ignore"] = "ignore",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        help: str | None = None,
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        use_container_width: bool | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> bool | DeltaGenerator:
        r"""Display a link button element.

        When clicked, a new tab will be opened to the specified URL. This will
        create a new session for the user if directed within the app.

        Parameters
        ----------
        label : str
            A short label explaining to the user what this button 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.

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

        url : str
            The URL to open on user click.

        key : str, int, or None
            An optional string to use for giving this element a stable
            identity. If this is ``None`` (default), the element's identity
            will be determined based on the values of the other parameters.

            If ``on_click`` enables widget behavior and ``key`` is provided,
            Streamlit will register the key in Session State to store the button state.
            The button state is read-only. 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-``.

        on_click : callable, "rerun", or "ignore"
            How the button should respond to user interaction. This controls
            whether or not the button behaves like an input widget. This can
            be one of the following values:

            - ``"ignore"`` (default): Streamlit opens the link in a new tab
              and doesn't rerun the app. The button won't behave like an
              input widget.
            - ``"rerun"``: Streamlit opens the link in a new tab and reruns
              the app. In this case, ``st.link_button`` returns a Boolean value
              like ``st.button``.
            - A ``callable``: Streamlit opens the link in a new tab, reruns
              the app, and executes the callable at the beginning of the rerun.
              In this case, ``st.link_button`` returns a Boolean value.

        args : list or tuple
            An optional list or tuple of args to pass to the callback when
            ``on_click`` is a callable. If ``on_click`` isn't a callable,
            this is ignored.

        kwargs : dict
            An optional dict of kwargs to pass to the callback when
            ``on_click`` is a callable. If ``on_click`` isn't a callable,
            this is ignored.

        help : str or None
            A tooltip that gets displayed when the button is hovered over. 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``.

        type : "primary", "secondary", or "tertiary"
            An optional string that specifies the button type. This can be one
            of the following:

            - ``"primary"``: The button's background is the app's primary color
              for additional emphasis.
            - ``"secondary"`` (default): The button's background coordinates
              with the app's background color for normal emphasis.
            - ``"tertiary"``: The button is plain text without a border or
              background for subtlety.

        icon : str or None
            An optional emoji or icon to display next to the button label. If ``icon``
            is ``None`` (default), no icon is displayed. If ``icon`` is a
            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.

        icon_position : "left" or "right"
            The position of the icon relative to the button label. This
            defaults to ``"left"``.

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

        use_container_width : bool
            Whether to expand the button's width to fill its parent container.
            If ``use_container_width`` is ``False`` (default), Streamlit sizes
            the button to fit its contents. If ``use_container_width`` is
            ``True``, the width of the button matches its parent container.

            In both cases, if the contents of the button are wider than the
            parent container, the contents will line wrap.

            .. deprecated::
                ``use_container_width`` is deprecated and will be removed in a
                future release. For ``use_container_width=True``, use
                ``width="stretch"``. For ``use_container_width=False``, use
                ``width="content"``.

        width : "content", "stretch", or int
            The width of the link button. This can be one of the following:

            - ``"content"`` (default): The width of the button matches the
              width of its content, but doesn't exceed the width of the parent
              container.
            - ``"stretch"``: The width of the button matches the width of the
              parent container.
            - An integer specifying the width in pixels: The button has a
              fixed width. If the specified width is greater than the width of
              the parent container, the width of the button matches the width
              of the parent container.

        shortcut : str or None
            An optional keyboard shortcut that triggers the button. This can be
            one of the following strings:

            - A single alphanumeric key like ``"K"`` or ``"4"``.
            - A function key like ``"F11"``.
            - A special key like ``"Enter"``, ``"Esc"``, or ``"Tab"``.
            - Any of the above combined with modifiers. For example, you can use
              ``"Ctrl+K"`` or ``"Cmd+Shift+O"``.

            .. important::
                The keys ``"C"`` and ``"R"`` are reserved and can't be used,
                even with modifiers. Punctuation keys like ``"."`` and ``","``
                aren't currently supported. Some combinations such as
                ``"Ctrl+T"``, ``"Ctrl+W"``, ``"Ctrl+PageUp"``,
                ``"Ctrl+PageDown"``, and ``"F11"`` are reserved by the browser
                or operating system and may never reach Streamlit.

            For a list of supported keys and modifiers, see the documentation
            for |st.button|_.

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

        wrap : bool or None
            Whether the button label can wrap onto multiple lines. This can be
            one of the following:

            - ``None`` (default): Streamlit decides based on the surrounding
              layout. Inside a horizontal container, the button keeps its
              standard, single-row height and truncates an overflowing label
              with an ellipsis; in other layouts, the label wraps onto
              additional lines.
            - ``True``: If the label is too wide for the button, it wraps onto
              additional lines and the button grows taller.
            - ``False``: The button keeps its standard, single-row height. A
              label that is too wide is truncated with an ellipsis.

            When the button keeps a single-row label and no ``help`` is set,
            hovering reveals the full label. Icons and keyboard shortcuts
            remain visible.

        Returns
        -------
        element or bool
            If ``on_click`` is ``"ignore"`` (default), this command returns an internal
            placeholder for the button element. Otherwise, this command returns a Boolean
            value in the same manner as ``st.button``: ``True`` if the button was clicked
            on the last rerun and ``False`` if it wasn't.

        Examples
        --------
        >>> import streamlit as st
        >>>
        >>> st.link_button("Go to gallery", "https://streamlit.io/gallery")

        .. output::
           https://doc-link-button.streamlit.app/
           height: 200px

        """
        if type not in {"primary", "secondary", "tertiary"}:
            raise StreamlitValueError(
                "type", ["'primary'", "'secondary'", "'tertiary'"]
            )

        ctx = get_script_run_ctx()
        normalized_icon_position = _normalize_icon_position(icon_position)

        if use_container_width is not None:
            width = "stretch" if use_container_width else "content"

        return self._link_button(
            label=label,
            url=url,
            key=key,
            on_click=on_click,
            args=args,
            kwargs=kwargs,
            help=help,
            disabled=disabled,
            type=type,
            icon=icon,
            icon_position=normalized_icon_position,
            width=width,
            shortcut=shortcut,
            wrap=wrap,
            ctx=ctx,
        )

    @gather_metrics("page_link")
    def page_link(
        self,
        page: str | Path | Page,
        *,
        label: str | None = None,
        icon: str | None = None,
        icon_position: IconPosition = "left",
        help: str | None = None,
        disabled: bool = False,
        use_container_width: bool | None = None,
        width: Width = "content",
        query_params: QueryParamsInput | None = None,
    ) -> DeltaGenerator:
        r"""Display a link to another page in a multipage app or to an external page.

        If another page in a multipage app is specified, clicking ``st.page_link``
        stops the current page execution and runs the specified page as if the
        user clicked on it in the sidebar navigation.

        If an external page is specified, clicking ``st.page_link`` opens a new
        tab to the specified page. The current script run will continue if not
        complete.

        Parameters
        ----------
        page : str, Path, or Page
            The page to switch to on user click. This can be one of the
            following values:

            - Path to a Python file: The path can be a string or
              ``pathlib.Path`` object. It can be absolute or relative to the
              entrypoint file. The Python file must be the source of a page in
              ``st.navigation``.

              If you are using the ``pages/`` directory instead of
              ``st.navigation``, the Python file must be your entrypoint file
              or a file in the ``pages/`` directory.

            - ``Page``: The source of the ``Page`` and its
              ``url_path`` must match a page defined in ``st.navigation``.
              Use ``st.Page`` to create a ``Page`` object.

            - URL: The URL must contain an HTTP or HTTPS scheme, like
              ``"https://docs.streamlit.io"``. When a user clicks a
              URL-defined page link, the URL opens in a new tab and the app
              doesn't rerun. If the page link is defined by a URL, then the
              ``label`` parameter is required.

            To link to a page defined by a ``callable``, you must use a
            ``Page`` object.

        label : str
            The label for the page link. Labels are required for external pages.
            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.

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

        icon : str or None
            An optional emoji or icon to display next to the link label. If
            ``icon`` is ``None`` (default), the icon is inferred from the
            ``Page`` object or no icon is displayed. If ``icon`` is a
            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.

        icon_position : "left" or "right"
            The position of the icon relative to the link label. This
            defaults to ``"left"``.

        help : str or None
            A tooltip that gets displayed when the link is hovered over. 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``.

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

        use_container_width : bool
            Whether to expand the link's width to fill its parent container.
            The default is ``True`` for page links in the sidebar and ``False``
            for those in the main app.

            .. deprecated::
                ``use_container_width`` is deprecated and will be removed in a
                future release. For ``use_container_width=True``, use
                ``width="stretch"``. For ``use_container_width=False``, use
                ``width="content"``.

        width : "content", "stretch", or int
            The width of the page-link button. This can be one of the following:

            - ``"content"`` (default): The width of the button matches the
              width of its content, but doesn't exceed the width of the parent
              container.
            - ``"stretch"``: The width of the button matches the width of the
              parent container.
            - An integer specifying the width in pixels: The button has a
              fixed width. If the specified width is greater than the width of
              the parent container, the width of the button matches the width
              of the parent container.

        query_params : dict, list of tuples, or None
            Query parameters to apply when navigating to the target page.
            This can be a dictionary or an iterable of key-value tuples. Values can
            be strings or iterables of strings (for repeated keys). When this is
            ``None`` (default), all non-embed query parameters are cleared during
            navigation.

        Examples
        --------
        **Example 1: Basic usage**

        The following example shows how to create page links in a multipage app
        that uses the ``pages/`` directory:

        .. code-block:: text

            your-repository/
            ├── pages/
            │   ├── page_1.py
            │   └── page_2.py
            └── your_app.py

        >>> import streamlit as st
        >>>
        >>> st.page_link("your_app.py", label="Home", icon="🏠")
        >>> st.page_link("pages/page_1.py", label="Page 1", icon="1️⃣")
        >>> st.page_link("pages/page_2.py", label="Page 2", icon="2️⃣", disabled=True)
        >>> st.page_link("http://www.google.com", label="Google", icon="🌎")

        The default navigation is shown here for comparison, but you can hide
        the default navigation using the |client.showSidebarNavigation|_
        configuration option. This allows you to create custom, dynamic
        navigation menus for your apps!

        .. |client.showSidebarNavigation| replace:: ``client.showSidebarNavigation``
        .. _client.showSidebarNavigation: https://docs.streamlit.io/develop/api-reference/configuration/config.toml#client

        .. output::
            https://doc-page-link.streamlit.app/
            height: 350px

        **Example 2: Passing query parameters**

        The following example shows how to pass query parameters when creating a
        page link in a multipage app:

        .. code-block:: text

            your-repository/
            ├── page_2.py
            └── your_app.py

        >>> import streamlit as st
        >>>
        >>> def page_1():
        >>>     st.title("Page 1")
        >>>     st.page_link("page_2.py", query_params={"utm_source": "page_1"})
        >>>
        >>> pg = st.navigation([page_1, "page_2.py"])
        >>> pg.run()

        .. output::
            https://doc-page-link-query-params.streamlit.app/
            height: 350px

        """
        if use_container_width is not None:
            width = "stretch" if use_container_width else "content"

        if in_sidebar(self.dg):
            # Sidebar page links should always be stretch width.
            width = "stretch"

        normalized_icon_position = _normalize_icon_position(icon_position)

        return self._page_link(
            page=page,
            label=label,
            icon=icon,
            icon_position=normalized_icon_position,
            help=help,
            disabled=disabled,
            width=width,
            query_params=query_params,
        )

    def _download_button(
        self,
        label: str,
        data: DownloadButtonDataType,
        file_name: str | None = None,
        mime: str | None = None,
        key: Key | None = None,
        help: str | None = None,
        on_click: WidgetCallback | Literal["rerun", "ignore"] | None = "rerun",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        ctx: ScriptRunContext | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> bool:
        key = to_key(key)

        on_click_callback: WidgetCallback | None = (
            None
            if on_click is None or on_click in {"ignore", "rerun"}
            else cast("WidgetCallback", on_click)  # ty: ignore[redundant-cast]
        )

        normalized_shortcut: str | None = None
        if shortcut is not None:
            normalized_shortcut = normalize_shortcut(shortcut)

        check_widget_policies(
            self.dg,
            key,
            on_change=on_click_callback,
            default_value=None,
            writes_allowed=False,
        )

        element_id = compute_and_register_element_id(
            "download_button",
            user_key=key,
            key_as_main_identity=True,
            dg=self.dg,
            label=label,
            icon=icon,
            file_name=file_name,
            mime=mime,
            help=help,
            type=type,
            width=width,
            icon_position=icon_position,
            shortcut=normalized_shortcut,
        )

        if is_in_form(self.dg):
            raise StreamlitAPIException(
                f"`st.download_button()` can't be used in an `st.form()`.{FORM_DOCS_INFO}"
            )

        download_button_proto = DownloadButtonProto()
        download_button_proto.id = element_id
        download_button_proto.label = label
        download_button_proto.default = False
        download_button_proto.type = type
        if wrap is not None:
            download_button_proto.wrap = wrap
        marshall_file(
            self.dg._get_delta_path_str(), data, download_button_proto, mime, file_name
        )
        download_button_proto.disabled = disabled

        if help is not None:
            download_button_proto.help = dedent(help)

        if icon is not None:
            download_button_proto.icon = validate_icon_or_emoji(icon)
        download_button_proto.icon_position = _icon_position_to_proto(icon_position)

        if on_click == "ignore":
            download_button_proto.ignore_rerun = True
        else:
            download_button_proto.ignore_rerun = False

        if normalized_shortcut is not None:
            download_button_proto.shortcut = normalized_shortcut

        serde = ButtonSerde()

        button_state = register_widget(
            download_button_proto.id,
            on_change_handler=on_click_callback,
            args=args,
            kwargs=kwargs,
            deserializer=serde.deserialize,
            serializer=serde.serialize,
            ctx=ctx,
            value_type="trigger_value",
            disabled=disabled,
        )

        if ctx:
            save_for_app_testing(ctx, element_id, button_state.value)

        layout_config = create_layout_config(width=width, allow_content_width=True)
        self.dg._enqueue(
            "download_button", download_button_proto, layout_config=layout_config
        )
        return button_state.value

    def _link_button(
        self,
        label: str,
        url: str,
        key: Key | None = None,
        on_click: WidgetCallback | Literal["rerun", "ignore"] = "ignore",
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        help: str | None = None,
        *,  # keyword-only arguments:
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
        ctx: ScriptRunContext | None = None,
    ) -> bool | DeltaGenerator:
        key = to_key(key)
        ignore_rerun = on_click == "ignore"
        is_rerun_mode = not ignore_rerun
        on_click_callback: WidgetCallback | None = (
            None
            if on_click in {"ignore", "rerun"}
            else cast("WidgetCallback", on_click)  # ty: ignore[redundant-cast]
        )

        link_button_proto = LinkButtonProto()
        normalized_shortcut = (
            normalize_shortcut(shortcut) if shortcut is not None else None
        )

        should_check_widget_policies = is_rerun_mode or key is not None
        should_register_element_id = (
            should_check_widget_policies or normalized_shortcut is not None
        )
        if should_check_widget_policies:
            check_widget_policies(
                self.dg,
                key,
                on_change=on_click_callback,
                default_value=None,
                writes_allowed=False,
            )

        if should_register_element_id:
            link_button_proto.id = compute_and_register_element_id(
                "link_button",
                user_key=key,
                key_as_main_identity=should_check_widget_policies,
                dg=self.dg,
                label=label,
                icon=icon,
                url=url,
                help=help,
                type=type,
                width=width,
                icon_position=icon_position,
                shortcut=normalized_shortcut,
            )

        link_button_proto.label = label
        link_button_proto.url = url
        link_button_proto.type = type
        link_button_proto.disabled = disabled
        link_button_proto.ignore_rerun = ignore_rerun
        if wrap is not None:
            link_button_proto.wrap = wrap

        if help is not None:
            link_button_proto.help = dedent(help)

        if icon is not None:
            link_button_proto.icon = validate_icon_or_emoji(icon)
        link_button_proto.icon_position = _icon_position_to_proto(icon_position)

        if normalized_shortcut is not None:
            link_button_proto.shortcut = normalized_shortcut

        button_state = None
        if is_rerun_mode:
            serde = ButtonSerde()
            button_state = register_widget(
                link_button_proto.id,
                on_change_handler=on_click_callback,
                args=args,
                kwargs=kwargs,
                deserializer=serde.deserialize,
                serializer=serde.serialize,
                ctx=ctx,
                value_type="trigger_value",
                disabled=disabled,
            )

        layout_config = create_layout_config(width=width, allow_content_width=True)
        link_button_dg = self.dg._enqueue(
            "link_button", link_button_proto, layout_config=layout_config
        )

        if button_state is not None:
            return button_state.value
        return link_button_dg

    def _page_link(
        self,
        page: str | Path | Page,
        *,  # keyword-only arguments:
        label: str | None = None,
        icon: str | None = None,
        icon_position: IconPosition = "left",
        help: str | None = None,
        disabled: bool = False,
        width: Width = "content",
        query_params: QueryParamsInput | None = None,
    ) -> DeltaGenerator:
        page_link_proto = PageLinkProto()
        if query_params:
            page_link_proto.query_string = process_query_params(query_params)

        layout_config = create_layout_config(width=width, allow_content_width=True)

        # Set icon_position early so it's set even in early return paths
        page_link_proto.icon_position = _icon_position_to_proto(icon_position)

        ctx = get_script_run_ctx()
        if not ctx:
            return self.dg._enqueue(
                "page_link", page_link_proto, layout_config=layout_config
            )

        page_link_proto.disabled = disabled

        if label is not None:
            page_link_proto.label = label

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

        if help is not None:
            page_link_proto.help = dedent(help)

        if isinstance(page, Page):
            if label is None:
                page_link_proto.label = page.title
            if icon is None:
                page_link_proto.icon = page.icon
                # Here the Page's icon is already validated
                # (using validate_icon_or_emoji) during its initialization

            if page.is_external:
                page_link_proto.page = page.external_url or ""
                page_link_proto.external = True
                return self.dg._enqueue(
                    "page_link", page_link_proto, layout_config=layout_config
                )

            _validate_registered_page(page)
            page_link_proto.page_script_hash = page._script_hash
            page_link_proto.page = page.url_path
        else:
            # Convert Path to string if necessary
            if isinstance(page, Path):
                page = str(page)

            # Handle external links:
            if is_url(page):
                if label is None or label == "":
                    raise StreamlitMissingPageLabelError()
                page_link_proto.page = page
                page_link_proto.external = True
                return self.dg._enqueue(
                    "page_link", page_link_proto, layout_config=layout_config
                )

            ctx_main_script = ""
            all_app_pages = {}
            ctx_main_script = ctx.main_script_path
            all_app_pages = ctx.pages_manager.get_pages()

            main_script_directory = get_main_script_directory(ctx_main_script)
            requested_page = os.path.realpath(
                normalize_path_join(main_script_directory, page)
            )

            # Handle retrieving the page_script_hash & page
            for page_data in all_app_pages.values():
                full_path = page_data["script_path"]
                page_name = page_data["page_name"]
                url_pathname = page_data["url_pathname"]
                if requested_page == full_path:
                    if label is None:
                        page_link_proto.label = page_name
                    page_link_proto.page_script_hash = page_data["page_script_hash"]
                    page_link_proto.page = url_pathname
                    break

            if page_link_proto.page_script_hash == "":
                raise StreamlitPageNotFoundError(
                    page=page,
                    main_script_directory=main_script_directory,
                    uses_pages_directory=bool(PagesManager.uses_pages_directory),
                )

        return self.dg._enqueue(
            "page_link", page_link_proto, layout_config=layout_config
        )

    def _button(
        self,
        label: str,
        key: str | None,
        help: str | None,
        is_form_submitter: bool,
        on_click: WidgetCallback | None = None,
        args: WidgetArgs | None = None,
        kwargs: WidgetKwargs | None = None,
        *,  # keyword-only arguments:
        type: Literal["primary", "secondary", "tertiary"] = "secondary",
        icon: str | None = None,
        icon_position: IconPosition = "left",
        disabled: bool = False,
        ctx: ScriptRunContext | None = None,
        width: Width = "content",
        shortcut: str | None = None,
        wrap: bool | None = None,
    ) -> bool:
        key = to_key(key)

        normalized_shortcut: str | None = None
        if shortcut is not None:
            normalized_shortcut = normalize_shortcut(shortcut)

        check_widget_policies(
            self.dg,
            key,
            on_click,
            default_value=None,
            writes_allowed=False,
            enable_check_callback_rules=not is_form_submitter,
        )

        # Only the form submitter button needs a form ID at the moment.
        form_id = current_form_id(self.dg) if is_form_submitter else ""
        element_id = compute_and_register_element_id(
            "form_submit_button" if is_form_submitter else "button",
            user_key=key,
            key_as_main_identity=True,
            dg=self.dg,
            label=label,
            icon=icon,
            help=help,
            is_form_submitter=is_form_submitter,
            type=type,
            width=width,
            icon_position=icon_position,
            shortcut=normalized_shortcut,
        )

        # It doesn't make sense to create a button inside a form (except
        # for the "Form Submitter" button that's automatically created in
        # every form). We throw an error to warn the user about this.
        # We omit this check for scripts running outside streamlit, because
        # they will have no script_run_ctx.
        if runtime.exists():
            if is_in_form(self.dg) and not is_form_submitter:
                raise StreamlitAPIException(
                    "`st.button()` can't be used in an `st.form()`. Use "
                    "`st.form_submit_button()` instead to submit the form."
                    f"{FORM_DOCS_INFO}"
                )
            if not is_in_form(self.dg) and is_form_submitter:
                raise StreamlitAPIException(
                    f"`st.form_submit_button()` must be used inside an `st.form()`.{FORM_DOCS_INFO}"
                )

        button_proto = ButtonProto()
        button_proto.id = element_id
        button_proto.label = label
        button_proto.default = False
        button_proto.is_form_submitter = is_form_submitter
        button_proto.form_id = form_id
        button_proto.type = type
        button_proto.disabled = disabled
        if wrap is not None:
            button_proto.wrap = wrap

        if help is not None:
            button_proto.help = dedent(help)

        if icon is not None:
            button_proto.icon = validate_icon_or_emoji(icon)
        button_proto.icon_position = _icon_position_to_proto(icon_position)

        if normalized_shortcut is not None:
            button_proto.shortcut = normalized_shortcut

        serde = ButtonSerde()

        button_state = register_widget(
            button_proto.id,
            on_change_handler=on_click,
            args=args,
            kwargs=kwargs,
            deserializer=serde.deserialize,
            serializer=serde.serialize,
            ctx=ctx,
            value_type="trigger_value",
            disabled=disabled,
        )

        if ctx:
            save_for_app_testing(ctx, element_id, button_state.value)

        layout_config = create_layout_config(width=width, allow_content_width=True)
        self.dg._enqueue("button", button_proto, layout_config=layout_config)

        return button_state.value

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


def _maybe_infer_file_info(
    data: DownloadButtonDataType,
    file_name: str | None,
    mimetype: str | None,
) -> tuple[str | None, str | None]:
    """Infer a missing ``file_name``/``mime`` from ``data.name`` when it is a
    non-empty string, as on a file object opened from disk (e.g. ``io.FileIO``).

    Explicit user-provided values always take precedence; when nothing can be
    inferred, the values are returned unchanged.
    """
    # `data.name` may be a property that raises (e.g. on a detached
    # TextIOWrapper), so getattr's default alone isn't enough.
    try:
        name = getattr(data, "name", None)
    except (AttributeError, ValueError, OSError):
        return file_name, mimetype
    # FileIO.name is an int when the object was created from a file descriptor.
    if not isinstance(name, str) or not name:
        return file_name, mimetype
    if file_name is None:
        file_name = os.path.basename(name)
    if mimetype is None:
        mimetype = mimetypes.guess_type(file_name)[0]
    return file_name, mimetype


def marshall_file(
    coordinates: str,
    data: DownloadButtonDataType,
    proto_download_button: DownloadButtonProto,
    mimetype: str | None,
    file_name: str | None = None,
) -> None:
    # Check if data is a callable (for deferred downloads)
    if callable(data):
        if not runtime.exists():
            # When running in "raw mode", we can't access the MediaFileManager.
            proto_download_button.url = ""
            return

        # ty needs this cast; mypy already narrows via callable() above.
        data_callable = cast(  # type: ignore[redundant-cast]
            "Callable[[], str | bytes | TextIO | BinaryIO | io.RawIOBase]", data
        )

        # Register the callable for deferred execution
        file_id = runtime.get_instance().media_file_mgr.add_deferred(
            data_callable,
            mimetype,
            coordinates,
            file_name=file_name,
        )
        proto_download_button.deferred_file_id = file_id
        proto_download_button.url = ""  # No URL yet, will be generated on click
        return

    # A file object opened from disk carries a usable path in `name`: use it
    # to fill in a missing file_name/mime. See issue #14159.
    file_name, mimetype = _maybe_infer_file_info(data, file_name, mimetype)

    # Existing logic for non-callable data
    data_as_bytes, inferred_mime_type = convert_data_to_bytes_and_infer_mime(
        data,
        unsupported_error=StreamlitAPIException(
            f"Invalid binary data format: {type(data)}"
        ),
    )
    if mimetype is None:
        mimetype = inferred_mime_type

    if runtime.exists():
        file_url = runtime.get_instance().media_file_mgr.add(
            data_as_bytes,
            mimetype,
            coordinates,
            file_name=file_name,
            is_for_static_download=True,
        )
    else:
        # When running in "raw mode", we can't access the MediaFileManager.
        file_url = ""

    proto_download_button.url = file_url
