From 4c4beb95afa1dc9eaefbc55efd185d1bd446163f Mon Sep 17 00:00:00 2001 From: Barret Schloerke Date: Tue, 10 Oct 2023 11:18:59 -0400 Subject: [PATCH] Move fill functions --- shiny/experimental/ui/__init__.py | 38 +- shiny/experimental/ui/_card.py | 2 +- shiny/experimental/ui/_color.py | 3 - shiny/experimental/ui/_deprecated.py | 146 +++++- shiny/experimental/ui/_fill.py | 639 --------------------------- shiny/experimental/ui/_layout.py | 2 +- shiny/experimental/ui/_navs.py | 6 +- shiny/experimental/ui/_output.py | 2 +- shiny/experimental/ui/_page.py | 2 +- shiny/experimental/ui/_sidebar.py | 4 +- shiny/experimental/ui/_valuebox.py | 2 +- shiny/ui/_tooltip.py | 2 - shiny/ui/_x/_fill.py | 44 -- shiny/ui/_x/_sidebar.py | 2 +- shiny/ui/dataframe/_dataframe.py | 2 +- shiny/ui/fill/__init__.py | 19 + shiny/ui/fill/_fill.py | 268 +++++++++++ 17 files changed, 461 insertions(+), 722 deletions(-) delete mode 100644 shiny/experimental/ui/_color.py delete mode 100644 shiny/experimental/ui/_fill.py delete mode 100644 shiny/ui/_x/_fill.py create mode 100644 shiny/ui/fill/__init__.py create mode 100644 shiny/ui/fill/_fill.py diff --git a/shiny/experimental/ui/__init__.py b/shiny/experimental/ui/__init__.py index 1623b88823..c531a2d60d 100644 --- a/shiny/experimental/ui/__init__.py +++ b/shiny/experimental/ui/__init__.py @@ -21,16 +21,6 @@ card_image, card_title, ) -from ._fill import ( # bind_fill_role, - FillingLayout, - as_fill_carrier, - as_fill_item, - as_fillable_container, - is_fill_carrier, - is_fill_item, - is_fillable_container, - remove_all_fill, -) from ._layout import layout_column_wrap from ._navs import ( navset_bar, @@ -86,6 +76,15 @@ # accordion_panel_remove, # accordion_panel_set, # update_accordion_panel, + # Fill + # FillingLayout, + as_fill_carrier, + as_fill_item, + as_fillable_container, + is_fill_carrier, + is_fill_item, + is_fillable_container, + remove_all_fill, ) __all__ = ( @@ -119,16 +118,6 @@ "value_box", "showcase_left_center", "showcase_top_right", - # Fill - "FillingLayout", - # "bind_fill_role", - "as_fill_carrier", - "as_fillable_container", - "as_fill_item", - "remove_all_fill", - "is_fill_carrier", - "is_fillable_container", - "is_fill_item", # Output "output_image", "output_plot", @@ -182,4 +171,13 @@ # "accordion_panel_remove", # "accordion_panel_set", # "update_accordion_panel", + # Fill + # "FillingLayout", + "as_fill_carrier", + "as_fillable_container", + "as_fill_item", + "remove_all_fill", + "is_fill_carrier", + "is_fillable_container", + "is_fill_item", ) diff --git a/shiny/experimental/ui/_card.py b/shiny/experimental/ui/_card.py index 316b3ea269..08bae8ada7 100644 --- a/shiny/experimental/ui/_card.py +++ b/shiny/experimental/ui/_card.py @@ -25,7 +25,7 @@ from ...ui._x._htmldeps import card_dependency from ...ui._x._utils import consolidate_attrs from ...ui.css_unit import CssUnit, as_css_padding, as_css_unit -from ._fill import as_fill_carrier, as_fill_item, as_fillable_container +from ...ui.fill import as_fill_carrier, as_fill_item, as_fillable_container __all__ = ( "CardItem", diff --git a/shiny/experimental/ui/_color.py b/shiny/experimental/ui/_color.py deleted file mode 100644 index d7c75c8da2..0000000000 --- a/shiny/experimental/ui/_color.py +++ /dev/null @@ -1,3 +0,0 @@ -def get_color_contrast(color: str) -> str: - # TODO-future: Implement - return color diff --git a/shiny/experimental/ui/_deprecated.py b/shiny/experimental/ui/_deprecated.py index 36035625cf..f170ba21bc 100644 --- a/shiny/experimental/ui/_deprecated.py +++ b/shiny/experimental/ui/_deprecated.py @@ -1,6 +1,6 @@ from __future__ import annotations -from typing import Any, Literal, Optional, overload +from typing import Any, Literal, Optional, TypeVar, overload from htmltools import Tag, TagAttrs, TagAttrValue, TagChild, TagList @@ -29,6 +29,13 @@ from ...ui.css_unit._css_unit import as_css_padding as main_as_css_padding from ...ui.css_unit._css_unit import as_css_unit as main_as_css_unit from ...ui.css_unit._css_unit import as_width_unit as main_as_width_unit +from ...ui.fill import as_fill_carrier as main_as_fill_carrier +from ...ui.fill import as_fill_item as main_as_fill_item +from ...ui.fill import as_fillable_container as main_as_fillable_container +from ...ui.fill import is_fill_carrier as main_is_fill_carrier +from ...ui.fill import is_fill_item as main_is_fill_item +from ...ui.fill import is_fillable_container as main_is_fillable_container +from ...ui.fill import remove_all_fill as main_remove_all_fill from ._navs import NavSetArg, NavSetCard, navset_card_pill, navset_card_tab from ._sidebar import ( DeprecatedPanelMain, @@ -79,6 +86,14 @@ # "accordion_panel_insert", # "accordion_panel_remove", # "update_accordion_panel", + # Fill + "as_fill_carrier", + "as_fillable_container", + "as_fill_item", + "remove_all_fill", + "is_fill_carrier", + "is_fillable_container", + "is_fill_item", ) ###################### @@ -677,3 +692,132 @@ def update_popover( # icon=icon, # session=session, # ) + + +# ###################### +# # Fill +# ###################### +TagT = TypeVar("TagT", bound="Tag") + + +def as_fill_carrier( + tag: TagT, + *, + min_height: None = None, + max_height: None = None, + gap: None = None, +) -> TagT: + """Deprecated. Please use `shiny.ui.fill.as_fill_carrier()` instead.""" + warn_deprecated( + "`shiny.experimental.ui.as_fill_carrier()` is deprecated. " + "This method will be removed in a future version, " + "please use :func:`shiny.ui.fill.as_fill_carrier` instead." + ) + + if min_height is not None: + raise RuntimeError( + "`min_height` is no longer supported. Please add the attribute directly to the Tag's style." + ) + if max_height is not None: + raise RuntimeError( + "`max_height` is no longer supported. Please add the attribute directly to the Tag's style." + ) + if gap is not None: + raise RuntimeError( + "`gap` is no longer supported. Please add the attribute directly to the Tag's style." + ) + + return main_as_fill_carrier(tag) + + +def as_fillable_container( + tag: TagT, + *, + min_height: None = None, + max_height: None = None, + gap: None = None, +) -> TagT: + """Deprecated. Please use `shiny.ui.fill.as_fillable_container()` instead.""" + warn_deprecated( + "`shiny.experimental.ui.as_fillable_container()` is deprecated. " + "This method will be removed in a future version, " + "please use :func:`shiny.ui.fill.as_fillable_container` instead." + ) + if min_height is not None: + raise RuntimeError( + "`min_height` is no longer supported. Please add the attribute directly to the Tag's style." + ) + if max_height is not None: + raise RuntimeError( + "`max_height` is no longer supported. Please add the attribute directly to the Tag's style." + ) + if gap is not None: + raise RuntimeError( + "`gap` is no longer supported. Please add the attribute directly to the Tag's style." + ) + + return main_as_fillable_container(tag) + + +def as_fill_item( + tag: TagT, + *, + min_height: None = None, + max_height: None = None, +) -> TagT: + """Deprecated. Please use `shiny.ui.fill.as_fill_item()` instead.""" + warn_deprecated( + "`shiny.experimental.ui.as_fill_item()` is deprecated. " + "This method will be removed in a future version, " + "please use :func:`shiny.ui.fill.as_fill_item` instead." + ) + if min_height is not None: + raise RuntimeError( + "`min_height` is no longer supported. Please add the attribute directly to the Tag's style." + ) + if max_height is not None: + raise RuntimeError( + "`max_height` is no longer supported. Please add the attribute directly to the Tag's style." + ) + + return main_as_fill_item(tag) + + +def remove_all_fill(tag: TagT) -> TagT: + """Deprecated. Please use `shiny.ui.fill.remove_all_fill()` instead.""" + warn_deprecated( + "`shiny.experimental.ui.remove_all_fill()` is deprecated. " + "This method will be removed in a future version, " + "please use :func:`shiny.ui.fill.remove_all_fill` instead." + ) + return main_remove_all_fill(tag) + + +def is_fill_carrier(tag: Tag) -> bool: + """Deprecated. Please use `shiny.ui.fill.is_fill_carrier()` instead.""" + warn_deprecated( + "`shiny.experimental.ui.is_fill_carrier()` is deprecated. " + "This method will be removed in a future version, " + "please use :func:`shiny.ui.fill.is_fill_carrier` instead." + ) + return main_is_fill_carrier(tag) + + +def is_fillable_container(tag: TagChild) -> bool: + """Deprecated. Please use `shiny.ui.fill.is_fillable_container()` instead.""" + warn_deprecated( + "`shiny.experimental.ui.is_fillable_container()` is deprecated. " + "This method will be removed in a future version, " + "please use :func:`shiny.ui.fill.is_fillable_container` instead." + ) + return main_is_fillable_container(tag) + + +def is_fill_item(tag: TagChild) -> bool: + """Deprecated. Please use `shiny.ui.fill.is_fill_item()` instead.""" + warn_deprecated( + "`shiny.experimental.ui.is_fill_item()` is deprecated. " + "This method will be removed in a future version, " + "please use :func:`shiny.ui.fill.is_fill_item` instead." + ) + return main_is_fill_item(tag) diff --git a/shiny/experimental/ui/_fill.py b/shiny/experimental/ui/_fill.py deleted file mode 100644 index 300acc95e4..0000000000 --- a/shiny/experimental/ui/_fill.py +++ /dev/null @@ -1,639 +0,0 @@ -from __future__ import annotations - -from typing import Literal, Optional, Protocol, TypeVar, runtime_checkable - -from htmltools import Tag, TagChild, Tagifiable, css - -from ...ui._x._htmldeps import fill_dependency -from ...ui.css_unit import CssUnit, as_css_unit -from ._tag import tag_add_style, tag_prepend_class, tag_remove_class - -""" -examples: - - [ ] remove_all_fill - - [ ] is_fill_carrier - - [ ] is_fillable_container - - [ ] is_fill_item - - [ ] FillingLayout -""" - - -__all__ = ( - "bind_fill_role", - "as_fill_carrier", - "as_fillable_container", - "as_fill_item", - "remove_all_fill", - "is_fill_carrier", - "is_fillable_container", - "is_fill_item", - "FillingLayout", -) - -TagFillingLayoutT = TypeVar("TagFillingLayoutT", bound="Tag | FillingLayout") -""" -A :class:`~htmltools.Tag` object or an object that implements the -:class:`~shiny.experimental.ui.FillingLayout` protocol. -""" - -TagT = TypeVar("TagT", bound="Tag") - - -FILL_ITEM_CLASS = "html-fill-item" -FILL_CONTAINER_CLASS = "html-fill-container" - -# We're currently not supporting the div(as_fillable_container()) API, in order to keep -# the function easier to understand. If we do implement something like that, we should -# use a different name, because the function is conceptually different, and it also is -# behaviorally different (it doesn't include the HTML dependency.) - -# TODO-future-approach: bind_fill_role() should return None? -# From @wch: -# > For functions like this, which modify the original object, I think the Pythonic way -# > of doing things is to return None, to make it clearer that the object is modified in -# > place. -# From @schloerke: -# > It makes for a very clunky interface. Keeping as is for now. -# > Should we copy the tag before modifying it? (If we are not doing that elsewhere, then I am hesitant to start here.) -# > If it is not utilizing `nonlocal foo`, then it should be returned. Even if it is altered in-place - - -def _add_role( - tag: TagT, *, condition: bool | None, class_: str, overwrite: bool = False -) -> TagT: - if condition is None: - return tag - - # Remove the class if it already exists and we're going to add it, - # or if we're requiring it to be removed - if (condition and tag.has_class(class_)) or overwrite: - tag = tag_remove_class(tag, class_) - - if condition: - tag = tag_prepend_class(tag, class_) - tag.append(fill_dependency()) - return tag - - -def bind_fill_role( - tag: TagT, - *, - item: Optional[bool] = None, - container: Optional[bool] = None, - overwrite: bool = False, -) -> TagT: - """ - Allow tags to intelligently fill their container - - Create fill containers and items. If a fill item is a direct child of a fill - container, and that container has an opinionated height, then the item is allowed to - grow and shrink to its container's size. - - Parameters - ---------- - tag - a T object. - item - whether or not to treat `tag` as a fill item. - container - whether or not to treat `x` as a fill container. Note, this will set the CSS - `display` property on the tag to `flex` which can change how its direct children - are rendered. Thus, one should be careful not to mark a tag as a fill container - when it needs to rely on other `display` behavior. - overwrite - whether or not to override previous filling layout calls (e.g., to remove the - item/container role from a tag). - - Returns - ------- - : - The original :class:`~htmltools.Tag` object (`tag`) with additional attributes - (and an :class:`~htmltools.HTMLDependency`). - """ - tag = _add_role( - tag, - condition=item, - class_=FILL_ITEM_CLASS, - overwrite=overwrite, - ) - tag = _add_role( - tag, - condition=container, - class_=FILL_CONTAINER_CLASS, - overwrite=overwrite, - ) - return tag - - -########################################### - - -# Test and/or coerce fill behavior - -# TODO-future; When `css_selector` can be implemented, the three parameters below should be added where appropriate -# @param class A character vector of class names to add to the tag. -# @param style A character vector of CSS properties to add to the tag. -# @param css_selector A character string containing a CSS selector for -# targeting particular (inner) tag(s) of interest. For more details on what -# selector(s) are supported, see [tagAppendAttributes()]. - - -def as_fill_carrier( - tag: TagFillingLayoutT, - *, - min_height: Optional[CssUnit] = None, - max_height: Optional[CssUnit] = None, - gap: Optional[CssUnit] = None, - # class_: Optional[str] = None, - # style: Optional[str] = None, - # css_selector: Optional[str], -) -> TagFillingLayoutT: - """ - Make a tag a fill carrier - - Filling layouts are built on the foundation of _fillable containers_ and _fill - items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is - why most UI components (e.g., :func:`~shiny.experimental.ui.card`, - :func:`~shiny.experimental.ui.card_body`, - :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` - arguments (to control their fill behavior). However, sometimes it's useful to add, - remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, - which these functions are designed to do. - - Parameters - ---------- - tag - a Tag object. - min_height,max_height,gap - Any valid CSS unit (e.g., `150`) to be applied to `tag`. - - Returns - ------- - : - The original :class:`~htmltools.Tag` object (`tag`) with additional attributes - (and an :class:`~htmltools.HTMLDependency`). - - See Also - -------- - * :func:`~shiny.experimental.ui.as_fill_item` - * :func:`~shiny.experimental.ui.as_fillable_container` - * :func:`~shiny.experimental.ui.remove_all_fill` - * :func:`~shiny.experimental.ui.is_fill_carrier` - * :func:`~shiny.experimental.ui.is_fill_item` - * :func:`~shiny.experimental.ui.is_fillable_container` - """ - return _add_filling_attrs( - tag, - item=True, - container=True, - min_height=min_height, - max_height=max_height, - gap=gap, - ) - - -def as_fillable_container( - tag: TagFillingLayoutT, - *, - min_height: Optional[CssUnit] = None, - max_height: Optional[CssUnit] = None, - gap: Optional[CssUnit] = None, - # class_: Optional[str] = None, - # style: Optional[str] = None, - # css_selector: Optional[str] = None, -) -> TagFillingLayoutT: - """ - Coerce a tag to be a fillable container - - Filling layouts are built on the foundation of _fillable containers_ and _fill - items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is - why most UI components (e.g., :func:`~shiny.experimental.ui.card`, - :func:`~shiny.experimental.ui.card_body`, - :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` - arguments (to control their fill behavior). However, sometimes it's useful to add, - remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, - which these functions are designed to do. - - Parameters - ---------- - tag - a Tag object. - min_height,max_height,gap - Any valid CSS unit (e.g., `150`) to be applied to `tag`. - - Returns - ------- - : - The original :class:`~htmltools.Tag` object (`tag`) with additional attributes - (and an :class:`~htmltools.HTMLDependency`). - - See Also - -------- - * :func:`~shiny.experimental.ui.as_fill_carrier` - * :func:`~shiny.experimental.ui.as_fill_item` - * :func:`~shiny.experimental.ui.as_fillable_container` - * :func:`~shiny.experimental.ui.remove_all_fill` - * :func:`~shiny.experimental.ui.is_fill_carrier` - * :func:`~shiny.experimental.ui.is_fill_item` - * :func:`~shiny.experimental.ui.is_fillable_container` - """ - return _add_filling_attrs( - tag, - container=True, - min_height=min_height, - max_height=max_height, - gap=gap, - ) - - -def as_fill_item( - tag: TagFillingLayoutT, - *, - min_height: Optional[CssUnit] = None, - max_height: Optional[CssUnit] = None, - # class_: Optional[str] = None, - # style: Optional[str] = None, - # css_selector: Optional[str] = None, -) -> TagFillingLayoutT: - """ - Coerce a tag to a fill item - - Filling layouts are built on the foundation of _fillable containers_ and _fill - items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is - why most UI components (e.g., :func:`~shiny.experimental.ui.card`, - :func:`~shiny.experimental.ui.card_body`, - :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` - arguments (to control their fill behavior). However, sometimes it's useful to add, - remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, - which these functions are designed to do. - - Parameters - ---------- - tag - a Tag object. - min_height,max_height - Any valid CSS unit (e.g., `150`) to be applied to `tag`. - - Returns - ------- - : - The original :class:`~htmltools.Tag` object (`tag`) with additional attributes - (and an :class:`~htmltools.HTMLDependency`). - - See Also - -------- - * :func:`~shiny.experimental.ui.as_fill_carrier` - * :func:`~shiny.experimental.ui.as_fillable_container` - * :func:`~shiny.experimental.ui.remove_all_fill` - * :func:`~shiny.experimental.ui.is_fill_carrier` - * :func:`~shiny.experimental.ui.is_fill_item` - * :func:`~shiny.experimental.ui.is_fillable_container` - """ - return _add_filling_attrs( - tag, - item=True, - min_height=min_height, - max_height=max_height, - ) - - -def remove_all_fill(tag: TagFillingLayoutT) -> TagFillingLayoutT: - """ - Remove any filling layouts from a tag - - Filling layouts are built on the foundation of _fillable containers_ and _fill - items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is - why most UI components (e.g., :func:`~shiny.experimental.ui.card`, - :func:`~shiny.experimental.ui.card_body`, - :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` - arguments (to control their fill behavior). However, sometimes it's useful to add, - remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, - which these functions are designed to do. - - Parameters - ---------- - tag - a Tag object. - - Returns - ------- - : - The original :class:`~htmltools.Tag` object with filling layout attributes - removed. - - - See Also - -------- - * :func:`~shiny.experimental.ui.as_fill_carrier` - * :func:`~shiny.experimental.ui.as_fill_item` - * :func:`~shiny.experimental.ui.as_fillable_container` - * :func:`~shiny.experimental.ui.is_fill_carrier` - * :func:`~shiny.experimental.ui.is_fill_item` - * :func:`~shiny.experimental.ui.is_fillable_container` - """ - - if isinstance(tag, FillingLayout): - return tag.remove_all_fill() - - return bind_fill_role( - tag, - item=False, - container=False, - overwrite=True, - ) - - -def is_fill_carrier(tag: Tag | FillingLayout) -> bool: - """ - Test a tag for being a fill carrier - - Filling layouts are built on the foundation of _fillable containers_ and _fill - items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is - why most UI components (e.g., :func:`~shiny.experimental.ui.card`, - :func:`~shiny.experimental.ui.card_body`, - :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` - arguments (to control their fill behavior). However, sometimes it's useful to add, - remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, - which these functions are designed to do. - - Parameters - ---------- - tag - a Tag object. - - Returns - ------- - : - Whether or not `tag` is a fill carrier. - - See Also - -------- - * :func:`~shiny.experimental.ui.as_fill_carrier` - * :func:`~shiny.experimental.ui.as_fill_item` - * :func:`~shiny.experimental.ui.as_fillable_container` - * :func:`~shiny.experimental.ui.remove_all_fill` - * :func:`~shiny.experimental.ui.is_fill_item` - * :func:`~shiny.experimental.ui.is_fillable_container` - """ - return is_fillable_container(tag) and is_fill_item(tag) - - -def is_fillable_container(tag: TagChild | FillingLayout) -> bool: - """ - Test a tag for being a fillable container - - Filling layouts are built on the foundation of _fillable containers_ and _fill - items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is - why most UI components (e.g., :func:`~shiny.experimental.ui.card`, - :func:`~shiny.experimental.ui.card_body`, - :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` - arguments (to control their fill behavior). However, sometimes it's useful to add, - remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, - which these functions are designed to do. - - Parameters - ---------- - tag - a Tag object. - - Returns - ------- - : - Whether or not `tag` is a fillable container. - - - See Also - -------- - * :func:`~shiny.experimental.ui.as_fill_carrier` - * :func:`~shiny.experimental.ui.as_fill_item` - * :func:`~shiny.experimental.ui.as_fillable_container` - * :func:`~shiny.experimental.ui.remove_all_fill` - * :func:`~shiny.experimental.ui.is_fill_item` - * :func:`~shiny.experimental.ui.is_fillable_container` - """ - # TODO-future; Handle widgets - # # won't actually work until (htmltools#334) gets fixed - # renders_to_tag_class(x, FILL_CONTAINER_CLASS, ".html-widget") - - return _is_fill_layout(tag, layout="fillable") - - -def is_fill_item(tag: TagChild | FillingLayout) -> bool: - """ - Test a tag for being a fill item - - Filling layouts are built on the foundation of _fillable containers_ and _fill - items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is - why most UI components (e.g., :func:`~shiny.experimental.ui.card`, - :func:`~shiny.experimental.ui.card_body`, - :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` - arguments (to control their fill behavior). However, sometimes it's useful to add, - remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, - which these functions are designed to do. - - Parameters - ---------- - tag - a Tag object. - - Returns - ------- - : - Whether or not `tag` is a fill item. - - See Also - -------- - * :func:`~shiny.experimental.ui.as_fill_carrier` - * :func:`~shiny.experimental.ui.as_fill_item` - * :func:`~shiny.experimental.ui.as_fillable_container` - * :func:`~shiny.experimental.ui.remove_all_fill` - * :func:`~shiny.experimental.ui.is_fill_carrier` - * :func:`~shiny.experimental.ui.is_fillable_container` - """ - # TODO-future; Handle widgets - # # won't actually work until (htmltools#334) gets fixed - # renders_to_tag_class(x, FILL_ITEM_CLASS, ".html-widget") - - return _is_fill_layout(tag, layout="fill") - - -def _is_fill_layout( - tag: TagChild | FillingLayout, - layout: Literal["fill", "fillable"], - # recurse: bool = True, -) -> bool: - if not isinstance(tag, (Tag, Tagifiable, FillingLayout)): - return False - - # tag: Tag | FillingLayout | Tagifiable - - if layout == "fill": - if isinstance(tag, Tag): - return tag.has_class(FILL_ITEM_CLASS) - if isinstance(tag, FillingLayout): - return tag.is_fill_item() - - elif layout == "fillable": - if isinstance(tag, Tag): - return tag.has_class(FILL_CONTAINER_CLASS) - if isinstance(tag, FillingLayout): - return tag.is_fillable_container() - - # tag: Tagifiable and not (Tag or FillingLayout) - raise TypeError( - f"`_is_fill_layout(tag=)` must be a `Tag` or implement the `FillingLayout` protocol methods. Received object of type: `{type(tag).__name__}`" - ) - - -T = TypeVar("T") - - -@runtime_checkable -class FillingLayout(Protocol): - """ - Generic protocol for filling layouts objects - """ - - def add_class( - self: T, - class_: str, - # *, - # # Currently Unused - # css_selector: Optional[str] = None, - # Currently Unused - **kwargs: object, - ) -> T: - """ - Generic method to handle adding a CSS `class` to an object - - Parameters - ---------- - class_ - A character vector of class names to add to the tag. - **kwargs - Possible future arguments - - Returns - ------- - : - The updated object. - """ - ... - - def add_style( - self: T, - style: str, - # *, - # # Currently Unused - # css_selector: Optional[str] = None, - # Currently Unused - **kwargs: object, - ) -> T: - """ - Generic method to handle adding a CSS `style` to an object - - Parameters - ---------- - style - A character vector of CSS properties to add to the tag. - **kwargs - Possible future arguments - - Returns - ------- - : - The updated object. - """ - ... - - def is_fill_item(self) -> bool: - """ - Generic method to handle testing if an object is a fill item - - Returns - ------- - : - Whether or not the object is a fill item - """ - ... - - def is_fillable_container(self) -> bool: - """ - Generic method to handle testing if an object is a fillable container - - Returns - ------- - : - Whether or not the object is a fillable container - """ - ... - - def as_fill_item( - self: T, - ) -> T: - """ - Generic method to handle coercing an object to a fill item - - Returns - ------- - : - The updated object. - """ - ... - - def as_fillable_container( - self: T, - ) -> T: - """ - Generic method to handle coercing an object to a fillable container - - Returns - ------- - : - The updated object. - """ - ... - - def remove_all_fill( - self: T, - ) -> T: - """ - Generic method to handle removing all fill properties from an object - - Returns - ------- - : - The updated object. - """ - ... - - -def _style_units_to_str(**kwargs: CssUnit | None) -> str | None: - style_items: dict[str, CssUnit] = {} - for k, v in kwargs.items(): - if v is not None: - style_items[k] = as_css_unit(v) - - return css(**style_items) - - -def _add_filling_attrs( - tag: TagFillingLayoutT, - item: Optional[bool] = None, - container: Optional[bool] = None, - **kwargs: CssUnit | None, -) -> TagFillingLayoutT: - new_style = _style_units_to_str(**kwargs) - - if isinstance(tag, FillingLayout): - if new_style: - tag.add_style(new_style) - if item: - tag.as_fill_item() - if container: - tag.as_fillable_container() - return tag - - # Tag - tag = tag_add_style(tag, new_style) - return bind_fill_role(tag, item=item, container=container) diff --git a/shiny/experimental/ui/_layout.py b/shiny/experimental/ui/_layout.py index 8b0fbb8ccb..7daf0347a4 100644 --- a/shiny/experimental/ui/_layout.py +++ b/shiny/experimental/ui/_layout.py @@ -7,7 +7,7 @@ from ...ui._x._htmldeps import grid_dependency from ...ui._x._utils import consolidate_attrs from ...ui.css_unit import CssUnit, as_css_unit -from ._fill import as_fill_item, as_fillable_container +from ...ui.fill import as_fill_item, as_fillable_container from ._utils import is_01_scalar diff --git a/shiny/experimental/ui/_navs.py b/shiny/experimental/ui/_navs.py index c249e1ce3b..e75e6611f5 100644 --- a/shiny/experimental/ui/_navs.py +++ b/shiny/experimental/ui/_navs.py @@ -15,11 +15,11 @@ from ..._utils import private_random_int from ...types import NavSetArg from ...ui._html_dependencies import bootstrap_deps +from ...ui._tag import tag_add_style from ...ui.css_unit import CssUnit, as_css_padding, as_css_unit +from ...ui.fill import as_fill_carrier from ._card import CardItem, card, card_body, card_footer, card_header -from ._fill import as_fill_carrier from ._sidebar import Sidebar, layout_sidebar -from ._tag import tag_add_style # ----------------------------------------------------------------------------- @@ -81,7 +81,7 @@ def tagify(self) -> None: class NavSet: - args: tuple[NavSetArg | MetadataNode] + args: tuple[NavSetArg | MetadataNode, ...] ul_class: str id: Optional[str] selected: Optional[str] diff --git a/shiny/experimental/ui/_output.py b/shiny/experimental/ui/_output.py index a83f94b3e8..ad3c02c9a0 100644 --- a/shiny/experimental/ui/_output.py +++ b/shiny/experimental/ui/_output.py @@ -20,7 +20,7 @@ format_opt_names, hover_opts, ) -from ._fill import as_fill_item, as_fillable_container +from ...ui.fill import as_fill_item, as_fillable_container # @add_example() diff --git a/shiny/experimental/ui/_page.py b/shiny/experimental/ui/_page.py index 22c1b1a143..6717cd897c 100644 --- a/shiny/experimental/ui/_page.py +++ b/shiny/experimental/ui/_page.py @@ -20,7 +20,7 @@ from ...ui._x._htmldeps import page_fillable_dependency, page_sidebar_dependency from ...ui._x._utils import consolidate_attrs from ...ui.css_unit import CssUnit, as_css_padding, as_css_unit -from ._fill import as_fillable_container +from ...ui.fill import as_fillable_container from ._navs import navset_bar from ._sidebar import Sidebar, layout_sidebar diff --git a/shiny/experimental/ui/_sidebar.py b/shiny/experimental/ui/_sidebar.py index 0080a9c99e..4b18750838 100644 --- a/shiny/experimental/ui/_sidebar.py +++ b/shiny/experimental/ui/_sidebar.py @@ -13,10 +13,8 @@ from ...ui._x._htmldeps import sidebar_dependency from ...ui._x._utils import consolidate_attrs, trinary from ...ui.css_unit import CssUnit, as_css_padding, as_css_unit - -# from ._color import get_color_contrast +from ...ui.fill import as_fill_item, as_fillable_container from ._card import CardItem -from ._fill import as_fill_item, as_fillable_container class Sidebar: diff --git a/shiny/experimental/ui/_valuebox.py b/shiny/experimental/ui/_valuebox.py index 8ecb053002..6e9e5f5bc0 100644 --- a/shiny/experimental/ui/_valuebox.py +++ b/shiny/experimental/ui/_valuebox.py @@ -7,8 +7,8 @@ from ...ui._x._htmldeps import value_box_dependency from ...ui._x._utils import consolidate_attrs from ...ui.css_unit import CssUnit, as_css_unit, as_width_unit +from ...ui.fill import as_fill_carrier from ._card import CardItem, card, card_body -from ._fill import as_fill_carrier from ._layout import layout_column_wrap from ._utils import is_01_scalar diff --git a/shiny/ui/_tooltip.py b/shiny/ui/_tooltip.py index df9339c3be..6305e11589 100644 --- a/shiny/ui/_tooltip.py +++ b/shiny/ui/_tooltip.py @@ -7,8 +7,6 @@ from .._namespaces import resolve_id_or_none from ._web_component import web_component - -# from ._color import get_color_contrast from ._x._utils import consolidate_attrs diff --git a/shiny/ui/_x/_fill.py b/shiny/ui/_x/_fill.py deleted file mode 100644 index 3be01536b8..0000000000 --- a/shiny/ui/_x/_fill.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import annotations - -from typing import TypeVar - -from htmltools import Tag - -from ...ui._x._htmldeps import fill_dependency -from ._tag import tag_prepend_class - -__all__ = ( - "as_fillable_container", - "as_fill_item", - "as_fill_carrier", -) - -TagT = TypeVar("TagT", bound="Tag") - - -FILL_ITEM_CLASS = "html-fill-item" -FILL_CONTAINER_CLASS = "html-fill-container" - - -def as_fillable_container( - tag: TagT, -) -> TagT: - tag_prepend_class(tag, FILL_CONTAINER_CLASS) - tag.append(fill_dependency()) - return tag - - -def as_fill_item( - tag: TagT, -) -> TagT: - tag_prepend_class(tag, FILL_ITEM_CLASS) - tag.append(fill_dependency()) - return tag - - -def as_fill_carrier( - tag: TagT, -) -> TagT: - as_fillable_container(tag) - as_fill_item(tag) - return tag diff --git a/shiny/ui/_x/_sidebar.py b/shiny/ui/_x/_sidebar.py index 6af92ff3aa..85b30ecbf7 100644 --- a/shiny/ui/_x/_sidebar.py +++ b/shiny/ui/_x/_sidebar.py @@ -11,7 +11,7 @@ # from ._color import get_color_contrast from ...ui.css_unit import CssUnit, as_css_padding, as_css_unit -from ._fill import as_fill_item, as_fillable_container +from ..fill import as_fill_item, as_fillable_container from ._utils import consolidate_attrs, trinary diff --git a/shiny/ui/dataframe/_dataframe.py b/shiny/ui/dataframe/_dataframe.py index d6f5620ab4..f1548ef25a 100644 --- a/shiny/ui/dataframe/_dataframe.py +++ b/shiny/ui/dataframe/_dataframe.py @@ -6,7 +6,7 @@ from ... import __version__ from ..._namespaces import resolve_id -from .._x._fill import as_fill_carrier +from ..fill import as_fill_carrier def data_frame_deps() -> HTMLDependency: diff --git a/shiny/ui/fill/__init__.py b/shiny/ui/fill/__init__.py new file mode 100644 index 0000000000..3025b2e492 --- /dev/null +++ b/shiny/ui/fill/__init__.py @@ -0,0 +1,19 @@ +from ._fill import ( + as_fill_carrier, + as_fill_item, + as_fillable_container, + remove_all_fill, + is_fill_carrier, + is_fill_item, + is_fillable_container, +) + +__all__ = ( + "as_fill_carrier", + "as_fillable_container", + "as_fill_item", + "remove_all_fill", + "is_fill_carrier", + "is_fillable_container", + "is_fill_item", +) diff --git a/shiny/ui/fill/_fill.py b/shiny/ui/fill/_fill.py new file mode 100644 index 0000000000..7b29ab226e --- /dev/null +++ b/shiny/ui/fill/_fill.py @@ -0,0 +1,268 @@ +from __future__ import annotations + +from typing import TypeVar + +from htmltools import Tag + +from .._tag import tag_prepend_class, tag_remove_class +from .._x._htmldeps import fill_dependency + +# TODO-barret; export publically +# TODO-barret; double check documentation; remove experimental from links. update fill location + + +__all__ = ( + "as_fillable_container", + "as_fill_item", + "as_fill_carrier", + "remove_all_fill", + "is_fill_carrier", + "is_fill_item", + "is_fillable_container", +) + +TagT = TypeVar("TagT", bound="Tag") + + +FILL_ITEM_CLASS = "html-fill-item" +FILL_CONTAINER_CLASS = "html-fill-container" + + +def as_fillable_container( + tag: TagT, +) -> TagT: + tag_prepend_class(tag, FILL_CONTAINER_CLASS) + tag.append(fill_dependency()) + return tag + + +def as_fill_item( + tag: TagT, +) -> TagT: + """ + Coerce a tag to a fill item + + Filling layouts are built on the foundation of _fillable containers_ and _fill + items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is + why most UI components (e.g., :func:`~shiny.ui.card`, + :func:`~shiny.ui.layout_sidebar`) possess both `fillable` and `fill` + arguments (to control their fill behavior). However, sometimes it's useful to add, + remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, + which these functions are designed to do. + + Parameters + ---------- + tag + a Tag object. + + Returns + ------- + : + The original :class:`~htmltools.Tag` object (`tag`) with additional attributes + (and an :class:`~htmltools.HTMLDependency`). + + See Also + -------- + * :func:`~shiny.ui.as_fill_carrier` + * :func:`~shiny.ui.as_fillable_container` + * :func:`~shiny.ui.remove_all_fill` + * :func:`~shiny.ui.is_fill_carrier` + * :func:`~shiny.ui.is_fill_item` + * :func:`~shiny.ui.is_fillable_container` + """ + tag_prepend_class(tag, FILL_ITEM_CLASS) + tag.append(fill_dependency()) + return tag + + +def as_fill_carrier( + tag: TagT, +) -> TagT: + """ + Make a tag a fill carrier + + Filling layouts are built on the foundation of _fillable containers_ and _fill + items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is + why most UI components (e.g., :func:`~shiny.ui.card`, + :func:`~shiny.ui.layout_sidebar`) possess both `fillable` and `fill` + arguments (to control their fill behavior). However, sometimes it's useful to add, + remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, + which these functions are designed to do. + + Parameters + ---------- + tag + a Tag object. + + Returns + ------- + : + The original :class:`~htmltools.Tag` object (`tag`) with additional attributes + (and an :class:`~htmltools.HTMLDependency`). + + See Also + -------- + * :func:`~shiny.ui.as_fill_item` + * :func:`~shiny.ui.as_fillable_container` + * :func:`~shiny.ui.remove_all_fill` + * :func:`~shiny.ui.is_fill_carrier` + * :func:`~shiny.ui.is_fill_item` + * :func:`~shiny.ui.is_fillable_container` + """ + as_fillable_container(tag) + as_fill_item(tag) + return tag + + +def remove_all_fill( + tag: TagT, +) -> TagT: + """ + Remove any filling layouts from a tag + + Filling layouts are built on the foundation of _fillable containers_ and _fill + items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is + why most UI components (e.g., :func:`~shiny.ui.card`, + :func:`~shiny.ui.layout_sidebar`) possess both `fillable` and `fill` + arguments (to control their fill behavior). However, sometimes it's useful to add, + remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, + which these functions are designed to do. + + Parameters + ---------- + tag + a Tag object. + + Returns + ------- + : + The original :class:`~htmltools.Tag` object with filling layout attributes + removed. + + + See Also + -------- + * :func:`~shiny.ui.as_fill_carrier` + * :func:`~shiny.ui.as_fill_item` + * :func:`~shiny.ui.as_fillable_container` + * :func:`~shiny.ui.is_fill_carrier` + * :func:`~shiny.ui.is_fill_item` + * :func:`~shiny.ui.is_fillable_container` + """ + + tag_remove_class(tag, FILL_CONTAINER_CLASS) + tag_remove_class(tag, FILL_ITEM_CLASS) + return tag + + +def is_fill_carrier(tag: Tag) -> bool: + """ + Test a tag for being a fill carrier + + Filling layouts are built on the foundation of _fillable containers_ and _fill + items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is + why most UI components (e.g., :func:`~shiny.experimental.ui.card`, + :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` + arguments (to control their fill behavior). However, sometimes it's useful to add, + remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, + which these functions are designed to do. + + Parameters + ---------- + tag + a Tag object. + + Returns + ------- + : + Whether or not `tag` is a fill carrier. + + See Also + -------- + * :func:`~shiny.experimental.ui.as_fill_carrier` + * :func:`~shiny.experimental.ui.as_fill_item` + * :func:`~shiny.experimental.ui.as_fillable_container` + * :func:`~shiny.experimental.ui.remove_all_fill` + * :func:`~shiny.experimental.ui.is_fill_item` + * :func:`~shiny.experimental.ui.is_fillable_container` + """ + return is_fillable_container(tag) and is_fill_item(tag) + + +def is_fillable_container(tag: object) -> bool: + """ + Test a tag for being a fillable container + + Filling layouts are built on the foundation of _fillable containers_ and _fill + items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is + why most UI components (e.g., :func:`~shiny.experimental.ui.card`, + :func:`~shiny.experimental.ui.card_body`, + :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` + arguments (to control their fill behavior). However, sometimes it's useful to add, + remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, + which these functions are designed to do. + + Parameters + ---------- + tag + a Tag object. + + Returns + ------- + : + Whether or not `tag` is a fillable container. + + + See Also + -------- + * :func:`~shiny.experimental.ui.as_fill_carrier` + * :func:`~shiny.experimental.ui.as_fill_item` + * :func:`~shiny.experimental.ui.as_fillable_container` + * :func:`~shiny.experimental.ui.remove_all_fill` + * :func:`~shiny.experimental.ui.is_fill_item` + * :func:`~shiny.experimental.ui.is_fillable_container` + """ + # TODO-future; Handle widgets + # # won't actually work until (htmltools#334) gets fixed + # renders_to_tag_class(x, FILL_CONTAINER_CLASS, ".html-widget") + + return isinstance(tag, Tag) and tag.has_class(FILL_CONTAINER_CLASS) + + +def is_fill_item(tag: object) -> bool: + """ + Test a tag for being a fill item + + Filling layouts are built on the foundation of _fillable containers_ and _fill + items_ (_fill carriers_ are both _fillable containers_ and _fill items_). This is + why most UI components (e.g., :func:`~shiny.experimental.ui.card`, + :func:`~shiny.experimental.ui.card_body`, + :func:`~shiny.experimental.ui.layout_sidebar`) possess both `fillable` and `fill` + arguments (to control their fill behavior). However, sometimes it's useful to add, + remove, and/or test fillable/fill properties on arbitrary :class:`~htmltools.Tag`, + which these functions are designed to do. + + Parameters + ---------- + tag + a Tag object. + + Returns + ------- + : + Whether or not `tag` is a fill item. + + See Also + -------- + * :func:`~shiny.experimental.ui.as_fill_carrier` + * :func:`~shiny.experimental.ui.as_fill_item` + * :func:`~shiny.experimental.ui.as_fillable_container` + * :func:`~shiny.experimental.ui.remove_all_fill` + * :func:`~shiny.experimental.ui.is_fill_carrier` + * :func:`~shiny.experimental.ui.is_fillable_container` + """ + # TODO-future; Handle widgets + # # won't actually work until (htmltools#334) gets fixed + # renders_to_tag_class(x, FILL_ITEM_CLASS, ".html-widget") + + return isinstance(tag, Tag) and tag.has_class(FILL_ITEM_CLASS)