diff --git a/py/gooey_gui/components/__init__.py b/py/gooey_gui/components/__init__.py index 80aa080..a9ad1c1 100644 --- a/py/gooey_gui/components/__init__.py +++ b/py/gooey_gui/components/__init__.py @@ -9,5 +9,13 @@ confirm_dialog, button_with_confirm_dialog, ) +from .input_widgets import ( + text_input, + date_input, + number_input, + password_input, + slider, + checkbox, +) from .pills import pill from .url_button import url_button diff --git a/py/gooey_gui/components/common.py b/py/gooey_gui/components/common.py index 065a9d1..17f352f 100644 --- a/py/gooey_gui/components/common.py +++ b/py/gooey_gui/components/common.py @@ -6,12 +6,11 @@ from datetime import datetime, timezone from furl import furl -from loguru import logger - from gooey_gui import core T = typing.TypeVar("T") LabelVisibility = typing.Literal["visible", "collapsed"] +TooltipPlacement = typing.Literal["left", "right", "top", "bottom", "auto"] BLANK_OPTION = "———" @@ -344,7 +343,8 @@ def text_area( value: str = "", height: int = 500, key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, placeholder: str = None, disabled: bool = False, label_visibility: LabelVisibility = "visible", @@ -382,6 +382,7 @@ def text_area( label=dedent(label), defaultValue=value, help=help, + tooltipPlacement=tooltip_placement, placeholder=placeholder, disabled=disabled, **props, @@ -435,7 +436,8 @@ def multiselect( options: typing.Sequence[T], format_func: typing.Callable[[T], typing.Any] = _default_format, key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, allow_none: bool = False, *, disabled: bool = False, @@ -458,6 +460,7 @@ def multiselect( name=key, label=dedent(label), help=help, + tooltipPlacement=tooltip_placement, isDisabled=disabled, isMulti=True, defaultValue=value, @@ -476,7 +479,8 @@ def selectbox( options: typing.Iterable[T], format_func: typing.Callable[[T], typing.Any] = _default_format, key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, *, disabled: bool = False, label_visibility: LabelVisibility = "visible", @@ -502,6 +506,7 @@ def selectbox( name=key, label=dedent(label), help=help, + tooltipPlacement=tooltip_placement, isDisabled=disabled, defaultValue=value, options=[ @@ -518,7 +523,8 @@ def download_button( label: str, url: str, key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, *, type: typing.Literal["primary", "secondary", "tertiary", "link"] = "secondary", disabled: bool = False, @@ -531,6 +537,7 @@ def download_button( label=label, key=key, help=help, + tooltip_placement=tooltip_placement, type=type, disabled=disabled, **props, @@ -540,7 +547,8 @@ def download_button( def button( label: str, key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, *, type: typing.Literal["primary", "secondary", "tertiary", "link"] = "secondary", disabled: bool = False, @@ -565,6 +573,7 @@ def button( name=key, label=dedent(label), help=help, + tooltipPlacement=tooltip_placement, disabled=disabled, className=className, **props, @@ -617,7 +626,8 @@ def file_uploader( key: str = None, value: str | list[str] = None, upload_key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, *, disabled: bool = False, label_visibility: LabelVisibility = "visible", @@ -657,6 +667,7 @@ def file_uploader( name=key, label=dedent(label), help=help, + tooltipPlacement=tooltip_placement, disabled=disabled, accept=accept, multiple=accept_multiple_files, @@ -709,7 +720,8 @@ def horizontal_radio( format_func: typing.Callable[[T], typing.Any] = _default_format, *, key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, value: T = None, disabled: bool = False, checked_by_default: bool = True, @@ -749,7 +761,8 @@ def radio( format_func: typing.Callable[[T], typing.Any] = _default_format, key: str = None, value: T = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, *, disabled: bool = False, checked_by_default: bool = True, @@ -776,219 +789,13 @@ def radio( value=option, defaultChecked=bool(value == option), help=help, + tooltipPlacement=tooltip_placement, disabled=disabled, ), ).mount() return value -def text_input( - label: str, - value: str = "", - max_chars: str = None, - key: str = None, - help: str = None, - *, - placeholder: str = None, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> str: - value = _input_widget( - input_type="text", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - maxLength=max_chars, - placeholder=placeholder, - **props, - ) - return value or "" - - -def date_input( - label: str, - value: str | None = None, - key: str = None, - help: str = None, - *, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> datetime | None: - value = _input_widget( - input_type="date", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - style=dict( - border="1px solid hsl(0, 0%, 80%)", - padding="0.375rem 0.75rem", - borderRadius="0.25rem", - margin="0 0.5rem 0 0.5rem", - ), - **props, - ) - try: - return datetime.strptime(value, "%Y-%m-%d") if value else None - except ValueError: - return None - - -def password_input( - label: str, - value: str = "", - max_chars: str = None, - key: str = None, - help: str = None, - *, - placeholder: str = None, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> str: - value = _input_widget( - input_type="password", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - maxLength=max_chars, - placeholder=placeholder, - **props, - ) - return value or "" - - -def slider( - label: str, - min_value: float = None, - max_value: float = None, - value: float = None, - step: float = None, - key: str = None, - help: str = None, - *, - disabled: bool = False, -) -> float: - value = _input_widget( - input_type="range", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - min=min_value, - max=max_value, - step=_step_value(min_value, max_value, step), - ) - return value or 0 - - -def number_input( - label: str, - min_value: float = None, - max_value: float = None, - value: float = None, - step: float = None, - key: str = None, - help: str = None, - *, - disabled: bool = False, -) -> float: - value = _input_widget( - input_type="number", - inputMode="decimal", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - min=min_value, - max=max_value, - step=_step_value(min_value, max_value, step), - ) - return value or 0 - - -def _step_value( - min_value: float | None, max_value: float | None, step: float | None -) -> float: - if step: - return step - elif isinstance(min_value, float) or isinstance(max_value, float): - return 0.1 - else: - return 1 - - -def checkbox( - label: str, - value: bool = False, - key: str = None, - help: str = None, - *, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - **props, -) -> bool: - value = _input_widget( - input_type="checkbox", - label=label, - value=value, - key=key, - help=help, - disabled=disabled, - label_visibility=label_visibility, - default_value_attr="defaultChecked", - **props, - ) - return bool(value) - - -def _input_widget( - *, - input_type: str, - label: str, - value: typing.Any = None, - key: str = None, - help: str = None, - disabled: bool = False, - label_visibility: LabelVisibility = "visible", - default_value_attr: str = "defaultValue", - **kwargs, -) -> typing.Any: - # if key: - # assert not value, "only one of value or key can be provided" - # else: - if not key: - key = core.md5_values("input", input_type, label, help, label_visibility) - value = core.session_state.setdefault(key, value) - if label_visibility != "visible": - label = None - core.RenderTreeNode( - name="input", - props={ - "type": input_type, - "name": key, - "label": dedent(label), - default_value_attr: value, - "help": help, - "disabled": disabled, - **kwargs, - }, - ).mount() - return value - - def breadcrumbs(divider: str = "/", **props) -> core.NestingCtx: style = props.pop("style", {}) | {"--bs-breadcrumb-divider": f"'{divider}'"} with tag("nav", style=style, **props): @@ -1051,7 +858,8 @@ def switch( label: str, value: bool = False, key: str = None, - help: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, *, disabled: bool = False, size: typing.Literal["small", "large"] = "large", @@ -1070,6 +878,7 @@ def switch( "label": dedent(label), "defaultChecked": value, "help": help, + "tooltipPlacement": tooltip_placement, "disabled": disabled, "size": size, **props, @@ -1081,7 +890,7 @@ def switch( def tooltip( content: str = "", *, - placement: typing.Literal["left", "right", "top", "bottom", "auto"] = "auto", + placement: TooltipPlacement | None = None, **props, ) -> core.NestingCtx: tooltip = core.RenderTreeNode( diff --git a/py/gooey_gui/components/input_widgets.py b/py/gooey_gui/components/input_widgets.py new file mode 100644 index 0000000..51792b1 --- /dev/null +++ b/py/gooey_gui/components/input_widgets.py @@ -0,0 +1,231 @@ +import base64 +import html as html_lib +import math +import textwrap +import typing +from datetime import datetime, timezone + +from furl import furl +from gooey_gui import core +from loguru import logger + +from .common import LabelVisibility, TooltipPlacement, dedent + + +def text_input( + label: str, + value: str = "", + max_chars: str = None, + key: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, + *, + placeholder: str = None, + disabled: bool = False, + label_visibility: LabelVisibility = "visible", + **props, +) -> str: + value = _html_input( + "text", + label=label, + value=value, + key=key, + help=help, + tooltip_placement=tooltip_placement, + disabled=disabled, + label_visibility=label_visibility, + maxLength=max_chars, + placeholder=placeholder, + **props, + ) + return value or "" + + +def date_input( + label: str, + value: str | None = None, + key: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, + *, + disabled: bool = False, + label_visibility: LabelVisibility = "visible", + **props, +) -> datetime | None: + value = _html_input( + "date", + label=label, + value=value, + key=key, + help=help, + tooltip_placement=tooltip_placement, + disabled=disabled, + label_visibility=label_visibility, + style=dict( + border="1px solid hsl(0, 0%, 80%)", + padding="0.375rem 0.75rem", + borderRadius="0.25rem", + margin="0 0.5rem 0 0.5rem", + ), + **props, + ) + try: + return datetime.strptime(value, "%Y-%m-%d") if value else None + except ValueError: + return None + + +def password_input( + label: str, + value: str = "", + max_chars: str = None, + key: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, + *, + placeholder: str = None, + disabled: bool = False, + label_visibility: LabelVisibility = "visible", + **props, +) -> str: + value = _html_input( + "password", + label=label, + value=value, + key=key, + help=help, + tooltip_placement=tooltip_placement, + disabled=disabled, + label_visibility=label_visibility, + maxLength=max_chars, + placeholder=placeholder, + **props, + ) + return value or "" + + +def slider( + label: str, + min_value: float = None, + max_value: float = None, + value: float = None, + step: float = None, + key: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, + *, + disabled: bool = False, +) -> float: + value = _html_input( + "range", + label=label, + value=value, + key=key, + help=help, + tooltip_placement=tooltip_placement, + disabled=disabled, + min=min_value, + max=max_value, + step=_step_value(min_value, max_value, step), + ) + return value or 0 + + +def number_input( + label: str, + min_value: float = None, + max_value: float = None, + value: float = None, + step: float = None, + key: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, + *, + disabled: bool = False, +) -> float: + value = _html_input( + "number", + inputMode="decimal", + label=label, + value=value, + key=key, + help=help, + tooltip_placement=tooltip_placement, + disabled=disabled, + min=min_value, + max=max_value, + step=_step_value(min_value, max_value, step), + ) + return value or 0 + + +def _step_value( + min_value: float | None, max_value: float | None, step: float | None +) -> float: + if step: + return step + elif isinstance(min_value, float) or isinstance(max_value, float): + return 0.1 + else: + return 1 + + +def checkbox( + label: str, + value: bool = False, + key: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, + *, + disabled: bool = False, + label_visibility: LabelVisibility = "visible", + **props, +) -> bool: + value = _html_input( + "checkbox", + label=label, + value=value, + key=key, + help=help, + tooltip_placement=tooltip_placement, + disabled=disabled, + label_visibility=label_visibility, + default_value_attr="defaultChecked", + **props, + ) + return bool(value) + + +def _html_input( + input_type: str, + /, + *, + label: str, + value: typing.Any = None, + key: str = None, + help: str | None = None, + tooltip_placement: TooltipPlacement | None = None, + disabled: bool = False, + label_visibility: LabelVisibility = "visible", + default_value_attr: str = "defaultValue", + **kwargs, +) -> typing.Any: + if not key: + key = core.md5_values("input", input_type, label, help, label_visibility) + value = core.session_state.setdefault(key, value) + if label_visibility != "visible": + label = None + core.RenderTreeNode( + name="input", + props={ + "type": input_type, + "name": key, + "label": dedent(label), + default_value_attr: value, + "help": help, + "tooltipPlacement": tooltip_placement, + "disabled": disabled, + **kwargs, + }, + ).mount() + return value