From 1716b1d9ab66d10ed4f7e1d100812b11376c1891 Mon Sep 17 00:00:00 2001 From: Carson Date: Mon, 11 Dec 2023 14:30:45 -0600 Subject: [PATCH 01/26] Add TopLevelRecallContextManager which inspects args to infer page default. Gut layout.page_*() for now. --- shiny/express/_recall_context.py | 93 +++++++++++++-- shiny/express/_run.py | 7 +- shiny/express/layout.py | 199 ------------------------------- 3 files changed, 86 insertions(+), 213 deletions(-) diff --git a/shiny/express/_recall_context.py b/shiny/express/_recall_context.py index 3317ff2a1..72c437771 100644 --- a/shiny/express/_recall_context.py +++ b/shiny/express/_recall_context.py @@ -3,11 +3,13 @@ import functools import sys from types import TracebackType -from typing import Callable, Generic, Mapping, Optional, Type, TypeVar +from typing import Callable, Generic, Mapping, Optional, Type, TypeVar, cast from htmltools import HTML, Tag, Tagifiable, TagList, tags +from .. import ui from .._typing_extensions import ParamSpec +from ..ui._navs import Nav, NavMenu P = ParamSpec("P") R = TypeVar("R") @@ -19,12 +21,10 @@ def __init__( self, fn: Callable[..., R], *, - default_page: RecallContextManager[Tag] | None = None, args: tuple[object, ...] | None = None, kwargs: Mapping[str, object] | None = None, ): - self.fn = fn - self.default_page = default_page + self._fn = fn if args is None: args = tuple() if kwargs is None: @@ -46,11 +46,6 @@ def append_arg(self, value: object): self.args.append(tags.pre(repr(value))) def __enter__(self) -> None: - if self.default_page is not None: - from . import _run - - _run.replace_top_level_recall_context_manager(self.default_page) - self._prev_displayhook = sys.displayhook # Collect each of the "printed" values in the args list. sys.displayhook = self.append_arg @@ -67,6 +62,86 @@ def __exit__( sys.displayhook(res) return False + @property + def fn(self) -> Callable[..., R]: + return self._fn + + +class TopLevelRecallContextManager(RecallContextManager[Tag]): + def __init__( + self, + *, + args: tuple[object, ...] | None = None, + kwargs: Mapping[str, object] | None = None, + ): + super().__init__(lambda x: x, args=args, kwargs=kwargs) + + @property + def fn(self) -> Callable[..., Tag]: + # Presence of a top-level nav items and/or sidebar determines the page function + navs = [x for x in self.args if isinstance(x, (Nav, NavMenu))] + sidebars = [x for x in self.args if isinstance(x, ui.Sidebar)] + + nNavs = len(navs) + nSidebars = len(sidebars) + + # TODO: How should this work with .set_page_*()/.set_title()? + if nNavs == 0: + if nSidebars == 0: + return _DEFAULT_PAGE_FUNCTION + + if nSidebars == 1: + # page_sidebar() needs sidebar to be the first arg + self.args = sidebars + [x for x in self.args if x not in sidebars] + return ui.page_sidebar + + # If multiple sidebars(), wrap them in layout_sidebar() + # TODO: + # 1. Maybe this logic be should handled by non-top-level ctx managers? + # That is, if we're not in a top-level ctx manager, automatically wrap + # Sidebar() into layout_sidebar()? + # 2. Provide a way to exit the layout.sidebar() context? Maybe '---'? + if nSidebars > 1: + new_args: object = [] + sidebar_idx = [ + i for i, x in enumerate(self.args) if isinstance(x, ui.Sidebar) + ] + new_args.append(*self.args[0 : sidebar_idx[0]]) + for i, x in enumerate(sidebar_idx): + j = ( + sidebar_idx[i + 1] + if i < len(sidebar_idx) - 1 + else len(self.args) + ) + s = ui.layout_sidebar( + cast(ui.Sidebar, self.args[x]), + *self.args[x + 1 : j], # type: ignore + ) + new_args.append(s) + + self.args = new_args + return _DEFAULT_PAGE_FUNCTION + + # At least one nav + else: + if nSidebars == 0: + # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? + return ui.page_navbar + + if nSidebars == 1: + self.kwargs["sidebar"] = self.kwargs.get("sidebar", sidebars[0]) + return ui.page_navbar + + if nSidebars > 1: + raise NotImplementedError( + "Multiple top-level sidebars not allowed in combination with top-level navs" + ) + + return _DEFAULT_PAGE_FUNCTION + + +_DEFAULT_PAGE_FUNCTION = ui.page_fixed + def wrap_recall_context_manager( fn: Callable[P, R] diff --git a/shiny/express/_run.py b/shiny/express/_run.py index 9d776ded3..94ebeb7f6 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -8,10 +8,9 @@ from htmltools import Tag, TagList -from .. import ui from .._app import App from ..session import Inputs, Outputs, Session -from ._recall_context import RecallContextManager +from ._recall_context import RecallContextManager, TopLevelRecallContextManager from .display_decorator._func_displayhook import _display_decorator_function_def from .display_decorator._node_transformers import ( DisplayFuncsTransformer, @@ -20,8 +19,6 @@ __all__ = ("wrap_express_app",) -_DEFAULT_PAGE_FUNCTION = ui.page_fixed - def wrap_express_app(file: Path) -> App: """Wrap a Shiny Express mode app into a Shiny `App` object. @@ -139,7 +136,7 @@ def set_result(x: object): def reset_top_level_recall_context_manager(): global _top_level_recall_context_manager global _top_level_recall_context_manager_has_been_replaced - _top_level_recall_context_manager = RecallContextManager(_DEFAULT_PAGE_FUNCTION) + _top_level_recall_context_manager = TopLevelRecallContextManager() _top_level_recall_context_manager_has_been_replaced = False diff --git a/shiny/express/layout.py b/shiny/express/layout.py index f25ea803b..e8bc1a936 100644 --- a/shiny/express/layout.py +++ b/shiny/express/layout.py @@ -8,11 +8,9 @@ from .. import ui from ..types import MISSING, MISSING_TYPE from ..ui.css import CssUnit -from . import _run from ._recall_context import RecallContextManager, wrap_recall_context_manager __all__ = ( - "set_page", "p", "div", "span", @@ -27,21 +25,9 @@ "navset", "navset_card", "nav", - "page_fluid", - "page_fixed", - "page_fillable", - "page_sidebar", ) -# ====================================================================================== -# Page functions -# ====================================================================================== -def set_page(page_fn: RecallContextManager[Tag]): - """Set the page function for the current Shiny express app.""" - _run.replace_top_level_recall_context_manager(page_fn, force=True) - - # ====================================================================================== # htmltools Tag functions # ====================================================================================== @@ -126,7 +112,6 @@ def sidebar( """ return RecallContextManager( ui.sidebar, - default_page=page_sidebar(), kwargs=dict( width=width, position=position, @@ -209,7 +194,6 @@ def layout_column_wrap( """ return RecallContextManager( ui.layout_column_wrap, - default_page=page_fillable(), kwargs=dict( width=width, fixed_width=fixed_width, @@ -591,186 +575,3 @@ def nav( icon=icon, ), ) - - -# ====================================================================================== -# Page components -# ====================================================================================== -def page_fluid( - *, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: str, -) -> RecallContextManager[Tag]: - """ - Create a fluid page. - - This function wraps :func:`~shiny.ui.page_fluid`. - - Parameters - ---------- - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Attributes on the page level container. - - Returns - ------- - : - A UI element. - """ - return RecallContextManager( - ui.page_fluid, - kwargs=dict( - title=title, - lang=lang, - **kwargs, - ), - ) - - -def page_fixed( - *, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: str, -) -> RecallContextManager[Tag]: - """ - Create a fixed page. - - This function wraps :func:`~shiny.ui.page_fixed`. - - Parameters - ---------- - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Attributes on the page level container. - - Returns - ------- - : - A UI element. - """ - return RecallContextManager( - ui.page_fixed, - kwargs=dict( - title=title, - lang=lang, - **kwargs, - ), - ) - - -def page_fillable( - *, - padding: Optional[CssUnit | list[CssUnit]] = None, - gap: Optional[CssUnit] = None, - fillable_mobile: bool = False, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: TagAttrValue, -): - """ - Creates a fillable page. - - This function wraps :func:`~shiny.ui.page_fillable`. - - Parameters - ---------- - padding - Padding to use for the body. See :func:`~shiny.ui.css_unit.as_css_padding` - for more details. - fillable_mobile - Whether or not the page should fill the viewport's height on mobile devices - (i.e., narrow windows). - gap - A CSS length unit passed through :func:`~shiny.ui.css_unit.as_css_unit` - defining the `gap` (i.e., spacing) between elements provided to `*args`. - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - - Returns - ------- - : - A UI element. - """ - return RecallContextManager( - ui.page_fillable, - kwargs=dict( - padding=padding, - gap=gap, - fillable_mobile=fillable_mobile, - title=title, - lang=lang, - **kwargs, - ), - ) - - -def page_sidebar( - *, - title: Optional[str | Tag | TagList] = None, - fillable: bool = True, - fillable_mobile: bool = False, - window_title: str | MISSING_TYPE = MISSING, - lang: Optional[str] = None, - **kwargs: TagAttrValue, -): - """ - Create a page with a sidebar and a title. - - This function wraps :func:`~shiny.ui.page_sidebar`. - - Parameters - ---------- - sidebar - Content to display in the sidebar. - title - A title to display at the top of the page. - fillable - Whether or not the main content area should be considered a fillable - (i.e., flexbox) container. - fillable_mobile - Whether or not ``fillable`` should apply on mobile devices. - window_title - The browser's window title (defaults to the host URL of the page). Can also be - set as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Additional attributes passed to :func:`~shiny.ui.layout_sidebar`. - - Returns - ------- - : - A UI element. - """ - return RecallContextManager( - ui.page_sidebar, - kwargs=dict( - title=title, - fillable=fillable, - fillable_mobile=fillable_mobile, - window_title=window_title, - lang=lang, - **kwargs, - ), - ) From 5fa9a52d30c56aee96ee996d8bf2e9f01634cea0 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 11 Dec 2023 23:45:51 -0600 Subject: [PATCH 02/26] WIP convert layout to ui --- examples/express/nav_app.py | 18 ++++----- examples/express/sidebar_app.py | 6 +-- shiny/express/__init__.py | 4 +- shiny/express/{layout.py => ui.py} | 62 ++++++++++++++++++++++-------- 4 files changed, 61 insertions(+), 29 deletions(-) rename shiny/express/{layout.py => ui.py} (97%) diff --git a/examples/express/nav_app.py b/examples/express/nav_app.py index e42769167..8e35b061b 100644 --- a/examples/express/nav_app.py +++ b/examples/express/nav_app.py @@ -1,15 +1,15 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.layout_column_wrap(width=1 / 2): - with layout.navset(): - with layout.nav(title="One"): +with ui.layout_column_wrap(width=1 / 2): + with ui.navset(): + with ui.nav(title="One"): ui.input_slider("n", "N", 1, 100, 50) - with layout.nav(title="Two"): + with ui.nav(title="Two"): @render.plot def histogram(): @@ -17,11 +17,11 @@ def histogram(): x = 100 + 15 * np.random.randn(437) plt.hist(x, input.n(), density=True) - with layout.navset_card(): - with layout.nav(title="One"): + with ui.navset_card(): + with ui.nav(title="One"): ui.input_slider("n2", "N", 1, 100, 50) - with layout.nav(title="Two"): + with ui.nav(title="Two"): @render.plot def histogram2(): diff --git a/examples/express/sidebar_app.py b/examples/express/sidebar_app.py index c229051d6..ac1e97844 100644 --- a/examples/express/sidebar_app.py +++ b/examples/express/sidebar_app.py @@ -1,10 +1,10 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.sidebar(): +with ui.sidebar(): ui.input_slider("n", "N", 1, 100, 50) diff --git a/shiny/express/__init__.py b/shiny/express/__init__.py index 4cc54de50..9b318c8c6 100644 --- a/shiny/express/__init__.py +++ b/shiny/express/__init__.py @@ -2,7 +2,7 @@ from ..session import Inputs, Outputs, Session from ..session import _utils as _session_utils -from . import app, layout +from . import app, ui from ._is_express import is_express_app from ._output import output_args, suspend_display from ._run import wrap_express_app @@ -17,7 +17,7 @@ "suspend_display", "wrap_express_app", "app", - "layout", + "ui", "display_body", ) diff --git a/shiny/express/layout.py b/shiny/express/ui.py similarity index 97% rename from shiny/express/layout.py rename to shiny/express/ui.py index f25ea803b..e516de915 100644 --- a/shiny/express/layout.py +++ b/shiny/express/ui.py @@ -2,21 +2,62 @@ from typing import Literal, Optional -import htmltools -from htmltools import Tag, TagAttrValue, TagChild, TagList +from htmltools import ( + Tag, + TagAttrValue, + TagChild, + TagList, + a, + br, + code, + div, + em, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + img, + p, + pre, + span, + strong, + tags, +) from .. import ui from ..types import MISSING, MISSING_TYPE +from ..ui import input_slider from ..ui.css import CssUnit from . import _run -from ._recall_context import RecallContextManager, wrap_recall_context_manager +from ._recall_context import RecallContextManager __all__ = ( - "set_page", - "p", + # htmltools imports + "a", + "br", + "code", "div", - "span", + "em", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "img", + "p", "pre", + "span", + "strong", + "tags", + # Imports from ..ui + "input_slider", + # Locally-defined functions + "set_page", "sidebar", "layout_column_wrap", "column", @@ -42,15 +83,6 @@ def set_page(page_fn: RecallContextManager[Tag]): _run.replace_top_level_recall_context_manager(page_fn, force=True) -# ====================================================================================== -# htmltools Tag functions -# ====================================================================================== -p = wrap_recall_context_manager(htmltools.p) -div = wrap_recall_context_manager(htmltools.div) -span = wrap_recall_context_manager(htmltools.span) -pre = wrap_recall_context_manager(htmltools.pre) - - # ====================================================================================== # Shiny layout components # ====================================================================================== From 19b8f447915a88b8863983d9abd7f210bb0bd659 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 12 Dec 2023 17:32:50 -0600 Subject: [PATCH 03/26] Add remaining components to shiny.express.ui --- examples/express/basic_app.py | 4 +- examples/express/column_wrap_app.py | 12 +- shiny/express/ui/__init__.py | 300 ++++++++++ shiny/express/{ui.py => ui/_cm_components.py} | 540 ++++++++++++++---- tests/pytest/test_express_ui.py | 23 + 5 files changed, 765 insertions(+), 114 deletions(-) create mode 100644 shiny/express/ui/__init__.py rename shiny/express/{ui.py => ui/_cm_components.py} (61%) diff --git a/examples/express/basic_app.py b/examples/express/basic_app.py index 9c6827839..c81e218aa 100644 --- a/examples/express/basic_app.py +++ b/examples/express/basic_app.py @@ -1,5 +1,5 @@ -from shiny import render, ui -from shiny.express import input +from shiny import render +from shiny.express import input, ui ui.input_slider("n", "N", 1, 100, 50) diff --git a/examples/express/column_wrap_app.py b/examples/express/column_wrap_app.py index 75bf3fd47..5e5c44f06 100644 --- a/examples/express/column_wrap_app.py +++ b/examples/express/column_wrap_app.py @@ -1,14 +1,14 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.layout_column_wrap(width=1 / 2): - with layout.card(): +with ui.layout_column_wrap(width=1 / 2): + with ui.card(): ui.input_slider("n", "N", 1, 100, 50) - with layout.card(): + with ui.card(): @render.plot def histogram(): @@ -16,7 +16,7 @@ def histogram(): x = 100 + 15 * np.random.randn(437) plt.hist(x, input.n(), density=True) - with layout.card(): + with ui.card(): @render.plot def histogram2(): diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py new file mode 100644 index 000000000..6c76ef725 --- /dev/null +++ b/shiny/express/ui/__init__.py @@ -0,0 +1,300 @@ +from __future__ import annotations + + +from htmltools import ( + TagList, + Tag, + TagChild, + TagAttrs, + TagAttrValue, + tags, + HTML, + head_content, + a, + br, + code, + div, + em, + h1, + h2, + h3, + h4, + h5, + h6, + hr, + img, + p, + pre, + span, + strong, +) + +from ...ui import ( + AccordionPanel, + AnimationOptions, + CardItem, + card_header, + card_footer, + ShowcaseLayout, + Sidebar, + SliderStepArg, + SliderValueArg, + ValueBoxTheme, + download_button, + download_link, + brush_opts, + click_opts, + dblclick_opts, + help_text, + hover_opts, + include_css, + include_js, + input_action_button, + input_action_link, + input_checkbox, + input_checkbox_group, + input_switch, + input_radio_buttons, + input_date, + input_date_range, + input_file, + input_numeric, + input_password, + input_select, + input_selectize, + input_slider, + input_text, + input_text_area, + insert_accordion_panel, + remove_accordion_panel, + update_accordion, + update_accordion_panel, + update_sidebar, + update_action_button, + update_action_link, + update_checkbox, + update_switch, + update_checkbox_group, + update_radio_buttons, + update_date, + update_date_range, + update_numeric, + update_select, + update_selectize, + update_slider, + update_text, + update_text_area, + update_navs, + update_tooltip, + update_popover, + insert_ui, + remove_ui, + markdown, + modal_button, + modal, + modal_show, + modal_remove, + notification_show, + notification_remove, + output_plot, + output_image, + output_text, + output_text_verbatim, + output_table, + output_ui, + Progress, + output_data_frame, + value_box_theme, +) + +from ._cm_components import ( + set_page, + sidebar, + layout_sidebar, + layout_column_wrap, + column, + row, + card, + accordion, + accordion_panel, + navset, + navset_card, + nav, + value_box, + panel_well, + panel_conditional, + panel_fixed, + panel_absolute, + page_fluid, + page_fixed, + page_fillable, + page_sidebar, + page_navbar, +) + +__all__ = ( + # Imports from htmltools + "TagList", + "Tag", + "TagChild", + "TagAttrs", + "TagAttrValue", + "tags", + "HTML", + "head_content", + "a", + "br", + "code", + "div", + "em", + "h1", + "h2", + "h3", + "h4", + "h5", + "h6", + "hr", + "img", + "p", + "pre", + "span", + "strong", + "tags", + # Imports from ...ui + "AccordionPanel", + "AnimationOptions", + "CardItem", + "ShowcaseLayout", + "Sidebar", + "SliderStepArg", + "SliderValueArg", + "ValueBoxTheme", + "card_header", + "card_footer", + "download_button", + "download_link", + "brush_opts", + "click_opts", + "dblclick_opts", + "help_text", + "hover_opts", + "include_css", + "include_js", + "input_action_button", + "input_action_link", + "input_checkbox", + "input_checkbox_group", + "input_switch", + "input_radio_buttons", + "input_date", + "input_date_range", + "input_file", + "input_numeric", + "input_password", + "input_select", + "input_selectize", + "input_slider", + "input_text", + "input_text_area", + "insert_accordion_panel", + "remove_accordion_panel", + "update_accordion", + "update_accordion_panel", + "update_sidebar", + "update_action_button", + "update_action_link", + "update_checkbox", + "update_switch", + "update_checkbox_group", + "update_radio_buttons", + "update_date", + "update_date_range", + "update_numeric", + "update_select", + "update_selectize", + "update_slider", + "update_text", + "update_text_area", + "update_navs", + "update_tooltip", + "update_popover", + "insert_ui", + "remove_ui", + "markdown", + "modal_button", + "modal", + "modal_show", + "modal_remove", + "notification_show", + "notification_remove", + "output_plot", + "output_image", + "output_text", + "output_text_verbatim", + "output_table", + "output_ui", + "Progress", + "output_data_frame", + "value_box_theme", + # Imports from ._cm_components + "set_page", + "sidebar", + "layout_sidebar", + "layout_column_wrap", + "column", + "row", + "card", + "accordion", + "accordion_panel", + "navset", + "navset_card", + "nav", + "value_box", + "panel_well", + "panel_conditional", + "panel_fixed", + "panel_absolute", + "page_fluid", + "page_fixed", + "page_fillable", + "page_sidebar", + "page_navbar", +) + + +_known_missing = { + # Items from shiny.ui that don't have a counterpart in shiny.express.ui + "shiny.ui": ( + "nav_control", + "nav_menu", + "nav_spacer", + "navset_bar", + "navset_card_pill", + "navset_card_tab", + "navset_card_underline", + "navset_hidden", + "navset_pill", + "navset_pill_card", + "navset_pill_list", + "navset_tab", + "navset_tab_card", + "navset_underline", + "page_bootstrap", + "panel_main", + "panel_sidebar", + "panel_title", + "page_output", + "popover", + "showcase_bottom", + "showcase_left_center", + "showcase_top_right", + "tooltip", + ), + # Items from shiny.express.ui that don't have a counterpart in shiny.ui + "shiny.express.ui": ( + "set_page", + # TODO: Migrate these to shiny.ui + "navset", + "navset_card", + ), +} diff --git a/shiny/express/ui.py b/shiny/express/ui/_cm_components.py similarity index 61% rename from shiny/express/ui.py rename to shiny/express/ui/_cm_components.py index e516de915..1d5cae375 100644 --- a/shiny/express/ui.py +++ b/shiny/express/ui/_cm_components.py @@ -1,64 +1,21 @@ +"Context manager components for Shiny Express" + from __future__ import annotations from typing import Literal, Optional -from htmltools import ( - Tag, - TagAttrValue, - TagChild, - TagList, - a, - br, - code, - div, - em, - h1, - h2, - h3, - h4, - h5, - h6, - hr, - img, - p, - pre, - span, - strong, - tags, -) +from htmltools import Tag, TagAttrValue, TagChild, TagList -from .. import ui -from ..types import MISSING, MISSING_TYPE -from ..ui import input_slider -from ..ui.css import CssUnit -from . import _run -from ._recall_context import RecallContextManager +from ... import ui +from ...types import MISSING, MISSING_TYPE +from ...ui.css import CssUnit +from .. import _run +from .._recall_context import RecallContextManager __all__ = ( - # htmltools imports - "a", - "br", - "code", - "div", - "em", - "h1", - "h2", - "h3", - "h4", - "h5", - "h6", - "hr", - "img", - "p", - "pre", - "span", - "strong", - "tags", - # Imports from ..ui - "input_slider", - # Locally-defined functions "set_page", "sidebar", + "layout_sidebar", "layout_column_wrap", "column", "row", @@ -68,10 +25,15 @@ "navset", "navset_card", "nav", + "panel_well", + "panel_conditional", + "panel_fixed", + "panel_absolute", "page_fluid", "page_fixed", "page_fillable", "page_sidebar", + "page_navbar", ) @@ -150,11 +112,6 @@ def sidebar( be used for top, the second will be left and right, and the third will be bottom. If four, then the values will be interpreted as top, right, bottom, and left respectively. - - Returns - ------- - : - A :class:`~shiny.ui.Sidebar` object. """ return RecallContextManager( ui.sidebar, @@ -175,6 +132,82 @@ def sidebar( ) +# TODO: Figure out sidebar arg for ui.layout_sidebar +def layout_sidebar( + *, + fillable: bool = True, + fill: bool = True, + bg: Optional[str] = None, + fg: Optional[str] = None, + border: Optional[bool] = None, + border_radius: Optional[bool] = None, + border_color: Optional[str] = None, + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, + height: Optional[CssUnit] = None, + **kwargs: TagAttrValue, +): + """ + Sidebar layout + + Create a sidebar layout component which can be dropped inside any Shiny UI page + method (e.g. :func:`~shiny.shiny.ui.page_fillable`) or :func:`~shiny.ui.card` + context. + + The first child needs to be of class :class:`~shiny.ui.Sidebar` object created by + :func:`~shiny.express.ui.sidebar`. The remaining arguments will contain the contents + to the main content area. Or tag attributes that are supplied to the resolved + :class:`~htmltools.Tag` object. + + Parameters + ---------- + fillable + Whether or not the main content area should be wrapped in a fillable container. + See :func:`~shiny.ui.as_fillable_container` for details. + fill + Whether or not the sidebar layout should be wrapped in a fillable container. See + :func:`~shiny.ui.as_fill_item` for details. + bg,fg + A background or foreground color. + border + Whether or not to show a border around the sidebar layout. + border_radius + Whether or not to round the corners of the sidebar layout. + border_color + A border color. + gap + A CSS length unit defining the vertical `gap` (i.e., spacing) between elements + provided to `*args`. This value will only be used if `fillable` is `True`. + padding + Padding within the sidebar itself. This can be a numeric vector (which will be + interpreted as pixels) or a character vector with valid CSS lengths. `padding` + may be one to four values. If one, then that value will be used for all four + sides. If two, then the first value will be used for the top and bottom, while + the second value will be used for left and right. If three, then the first will + be used for top, the second will be left and right, and the third will be + bottom. If four, then the values will be interpreted as top, right, bottom, and + left respectively. + height + Any valid CSS unit to use for the height. + """ + return RecallContextManager( + ui.layout_sidebar, + kwargs=dict( + fillable=fillable, + fill=fill, + bg=bg, + fg=fg, + border=border, + border_radius=border_radius, + border_color=border_color, + gap=gap, + padding=padding, + height=height, + **kwargs, + ), + ) + + def layout_column_wrap( *, width: CssUnit | None | MISSING_TYPE = MISSING, @@ -233,11 +266,6 @@ def layout_column_wrap( A CSS class to apply to the containing element. **kwargs Additional attributes to apply to the containing element. - - Returns - ------- - : - A :class:`~htmltools.Tag` element. """ return RecallContextManager( ui.layout_column_wrap, @@ -273,11 +301,6 @@ def column(width: int, *, offset: int = 0, **kwargs: TagAttrValue): **kwargs Attributes to place on the column tag. - Returns - ------- - : - A UI element. - See Also ------- :func:`~shiny.ui.row` @@ -309,11 +332,6 @@ def row(**kwargs: TagAttrValue): **kwargs Attributes to place on the row tag. - Returns - ------- - : - A UI element. - See Also ------- :func:`~shiny.ui.column` @@ -361,11 +379,6 @@ def card( `"b"` and once with `"d"`). **kwargs HTML attributes on the returned Tag. - - Returns - ------- - : - An :func:`~shiny.ui.tags.div` tag. """ return RecallContextManager( ui.card, @@ -420,11 +433,6 @@ def accordion( Any valid CSS unit; for example, height="100%". **kwargs Attributes to this tag. - - Returns - ------- - : - Accordion panel Tag object. """ return RecallContextManager( ui.accordion, @@ -463,11 +471,6 @@ def accordion_panel( A :class:`~htmltools.Tag` which is positioned just before the `title`. **kwargs Tag attributes to the `accordion-body` div Tag. - - Returns - ------- - : - `AccordionPanel` object. """ return RecallContextManager( ui.accordion_panel, @@ -625,6 +628,244 @@ def nav( ) +# ====================================================================================== +# Value boxes +# ====================================================================================== +def value_box( + title: TagChild, + value: TagChild, + *, + showcase: Optional[TagChild] = None, + showcase_layout: ui._valuebox.SHOWCASE_LAYOUTS_STR + | ui.ShowcaseLayout = "left center", + full_screen: bool = False, + theme: Optional[str | ui.ValueBoxTheme] = None, + height: Optional[CssUnit] = None, + max_height: Optional[CssUnit] = None, + fill: bool = True, + class_: Optional[str] = None, + **kwargs: TagAttrValue, +): + return RecallContextManager( + ui.value_box, + args=(title, value), + kwargs=dict( + showcase=showcase, + showcase_layout=showcase_layout, + full_screen=full_screen, + theme=theme, + height=height, + max_height=max_height, + fill=fill, + class_=class_, + **kwargs, + ), + ) + + +# ====================================================================================== +# Panels +# ====================================================================================== + + +def panel_well(**kwargs: TagAttrValue): + """ + Create a well panel + + This function wraps :func:`~shiny.ui.panel_well`. + + A well panel is a simple container with a border and some padding. It's useful for + grouping related content together. + """ + return RecallContextManager( + ui.panel_well, + kwargs=dict( + **kwargs, + ), + ) + + +def panel_conditional( + *, + condition: str, + **kwargs: TagAttrValue, +): + """ + Create a conditional panel + + This function wraps :func:`~shiny.ui.panel_conditional`. + + Show UI elements only if a ``JavaScript`` condition is ``true``. + + Parameters + ---------- + condition + A JavaScript expression that will be evaluated repeatedly to determine whether + the panel should be displayed. + **kwargs + Attributes to place on the panel tag. + + Note + ---- + In the JS expression, you can refer to input and output JavaScript objects that + contain the current values of input and output. For example, if you have an input + with an id of foo, then you can use input.foo to read its value. (Be sure not to + modify the input/output objects, as this may cause unpredictable behavior.) + + You are not recommended to use special JavaScript characters such as a period . in + the input id's, but if you do use them anyway, for example, ``id = "foo.bar"``, you + will have to use ``input["foo.bar"]`` instead of ``input.foo.bar`` to read the input + value. + + Tip + --- + A more powerful (but slower) way to conditionally show UI content is to use + :func:`~shiny.render.ui`. + """ + return RecallContextManager( + ui.panel_conditional, + kwargs=dict( + condition=condition, + **kwargs, + ), + ) + + +def panel_fixed( + *, + top: Optional[str] = None, + left: Optional[str] = None, + right: Optional[str] = None, + bottom: Optional[str] = None, + width: Optional[str] = None, + height: Optional[str] = None, + draggable: bool = False, + cursor: Literal["auto", "move", "default", "inherit"] = "auto", + **kwargs: TagAttrValue, +): + """ + Create a panel of absolutely positioned content. + + This function wraps :func:`~shiny.ui.panel_fixed`. + + This function is equivalent to calling :func:`~shiny.ui.panel_absolute` with + ``fixed=True`` (i.e., the panel does not scroll with the rest of the page). See + :func:`~shiny.ui.panel_absolute` for more information. + + Parameters + ---------- + **kwargs + Arguments passed along to :func:`~shiny.ui.panel_absolute`. + + See Also + ------- + :func:`~shiny.ui.panel_absolute` + """ + return RecallContextManager( + ui.panel_fixed, + kwargs=dict( + top=top, + left=left, + right=right, + bottom=bottom, + width=width, + height=height, + draggable=draggable, + cursor=cursor, + **kwargs, + ), + ) + + +def panel_absolute( + *, + top: Optional[str] = None, + left: Optional[str] = None, + right: Optional[str] = None, + bottom: Optional[str] = None, + width: Optional[str] = None, + height: Optional[str] = None, + draggable: bool = False, + fixed: bool = False, + cursor: Literal["auto", "move", "default", "inherit"] = "auto", + **kwargs: TagAttrValue, +): + """ + Create a panel of absolutely positioned content. + + This function wraps :func:`~shiny.ui.panel_absolute`. + + Creates a ``
`` tag whose CSS position is set to absolute (or fixed if ``fixed = + True``). The way absolute positioning works in HTML is that absolute coordinates are + specified relative to its nearest parent element whose position is not set to static + (which is the default), and if no such parent is found, then relative to the page + borders. If you're not sure what that means, just keep in mind that you may get + strange results if you use this function from inside of certain types of panels. + + Parameters + ---------- + top + Distance between the top of the panel, and the top of the page or parent + container. + left + Distance between the left side of the panel, and the left of the page or parent + container. + right + Distance between the right side of the panel, and the right of the page or + parent container. + bottom + Distance between the bottom of the panel, and the bottom of the page or parent + container. + width + Width of the panel. + height + Height of the panel. + draggable + If ``True``, allows the user to move the panel by clicking and dragging. + fixed + Positions the panel relative to the browser window and prevents it from being + scrolled with the rest of the page. + cursor + The type of cursor that should appear when the user mouses over the panel. Use + ``"move"`` for a north-east-south-west icon, ``"default"`` for the usual cursor + arrow, or ``"inherit"`` for the usual cursor behavior (including changing to an + I-beam when the cursor is over text). The default is ``"auto"``, which is + equivalent to ``"move" if draggable else "inherit"``. + **kwargs + Attributes added to the content's container tag. + + Tip + ---- + The position (``top``, ``left``, ``right``, ``bottom``) and size (``width``, + ``height``) parameters are all optional, but you should specify exactly two of top, + bottom, and height and exactly two of left, right, and width for predictable + results. + + Like most other distance parameters in Shiny, the position and size parameters take + a number (interpreted as pixels) or a valid CSS size string, such as ``"100px"`` + (100 pixels) or ``"25%"``. + + For arcane HTML reasons, to have the panel fill the page or parent you should + specify 0 for ``top``, ``left``, ``right``, and ``bottom`` rather than the more + obvious ``width = "100%"`` and ``height = "100%"``. + """ + return RecallContextManager( + ui.panel_absolute, + kwargs=dict( + top=top, + left=left, + right=right, + bottom=bottom, + width=width, + height=height, + draggable=draggable, + fixed=fixed, + cursor=cursor, + **kwargs, + ), + ) + + # ====================================================================================== # Page components # ====================================================================================== @@ -650,11 +891,6 @@ def page_fluid( default, `None`, results in an empty string. **kwargs Attributes on the page level container. - - Returns - ------- - : - A UI element. """ return RecallContextManager( ui.page_fluid, @@ -688,11 +924,6 @@ def page_fixed( default, `None`, results in an empty string. **kwargs Attributes on the page level container. - - Returns - ------- - : - A UI element. """ return RecallContextManager( ui.page_fixed, @@ -737,10 +968,6 @@ def page_fillable( will be used as the lang in the ```` tag, as in ````. The default, `None`, results in an empty string. - Returns - ------- - : - A UI element. """ return RecallContextManager( ui.page_fillable, @@ -789,11 +1016,6 @@ def page_sidebar( default, `None`, results in an empty string. **kwargs Additional attributes passed to :func:`~shiny.ui.layout_sidebar`. - - Returns - ------- - : - A UI element. """ return RecallContextManager( ui.page_sidebar, @@ -806,3 +1028,109 @@ def page_sidebar( **kwargs, ), ) + + +# TODO: Figure out sidebar arg for ui.page_navbar +def page_navbar( + *, + title: Optional[str | Tag | TagList] = None, + id: Optional[str] = None, + selected: Optional[str] = None, + fillable: bool | list[str] = True, + fillable_mobile: bool = False, + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, + position: Literal["static-top", "fixed-top", "fixed-bottom"] = "static-top", + header: Optional[TagChild] = None, + footer: Optional[TagChild] = None, + bg: Optional[str] = None, + inverse: bool = False, + underline: bool = True, + collapsible: bool = True, + fluid: bool = True, + window_title: str | MISSING_TYPE = MISSING, + lang: Optional[str] = None, +): + """ + Create a page with a navbar and a title. + + This function wraps :func:`~shiny.ui.page_navbar`. + + Parameters + ---------- + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :func:`~shiny.ui.sidebar` component to display on every page. + fillable + Whether or not the main content area should be considered a fillable + (i.e., flexbox) container. + fillable_mobile + Whether or not ``fillable`` should apply on mobile devices. + position + Determines whether the navbar should be displayed at the top of the page with + normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or + pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or + "fixed-bottom" will cause the navbar to overlay your body content, unless you + add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + bg + Background color of the navbar (a CSS color). + inverse + Either ``True`` for a light text color or ``False`` for a dark text color. + collapsible + ``True`` to automatically collapse the elements into an expandable menu on mobile devices or narrow window widths. + fluid + ``True`` to use fluid layout; ``False`` to use fixed layout. + window_title + The browser's window title (defaults to the host URL of the page). Can also be + set as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + + See Also + ------- + * :func:`~shiny.ui.nav` + * :func:`~shiny.ui.nav_menu` + * :func:`~shiny.ui.navset_bar` + * :func:`~shiny.ui.page_fluid` + + Example + ------- + See :func:`~shiny.ui.nav`. + """ + + return RecallContextManager( + ui.page_navbar, + kwargs=dict( + title=title, + id=id, + selected=selected, + fillable=fillable, + fillable_mobile=fillable_mobile, + gap=gap, + padding=padding, + position=position, + header=header, + footer=footer, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + fluid=fluid, + window_title=window_title, + lang=lang, + ), + ) diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index b06fb3972..8b3c875ed 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -7,6 +7,29 @@ from shiny.express import output_args, suspend_display +def test_express_ui_is_complete(): + """ + Make sure shiny.express.ui covers everything that shiny.ui does, or explicitly lists + the item in _known_missing. + """ + + from shiny import ui + from shiny.express import ui as xui + + ui_all = set(ui.__all__) + xui_all = set(xui.__all__) + ui_known_missing = set(xui._known_missing["shiny.ui"]) + xui_known_missing = set(xui._known_missing["shiny.express.ui"]) + + # Make sure that in shiny.express.ui, there's no overlap between __all__ and + # _known_missing. + assert xui_all.isdisjoint(ui_known_missing) + + # Make sure that everything from shiny.ui is either exported by shiny.express.ui, or + # explicitly listed in shiny.express.ui._known_missing. + assert ui_all.union(xui_known_missing) == xui_all.union(ui_known_missing) + + def test_render_output_controls(): @render.text def text1(): From fac3ca25a3ba98a70d55318551135c75b3386a68 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 12 Dec 2023 19:26:48 -0600 Subject: [PATCH 04/26] Install htmltools from github --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 079301384..62ac13493 100644 --- a/setup.cfg +++ b/setup.cfg @@ -37,7 +37,7 @@ install_requires = starlette>=0.17.1 websockets>=10.0 python-multipart - htmltools>=0.4.1.9001 + htmltools @ git+https://github.com/posit-dev/py-htmltools.git click>=8.1.4 markdown-it-py>=1.1.0 # This is needed for markdown-it-py. Without it, when loading shiny/ui/_markdown.py, From d9361d21c523686c2c7f7e40fbb7feec032bc4a2 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Tue, 12 Dec 2023 19:30:08 -0600 Subject: [PATCH 05/26] Update examples --- examples/express/accordion_app.py | 10 +++++----- examples/express/shared_app.py | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/express/accordion_app.py b/examples/express/accordion_app.py index 7dc6d691f..fdd722c34 100644 --- a/examples/express/accordion_app.py +++ b/examples/express/accordion_app.py @@ -1,14 +1,14 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input, layout +from shiny import render +from shiny.express import input, ui -with layout.accordion(open=["Panel 1", "Panel 2"]): - with layout.accordion_panel("Panel 1"): +with ui.accordion(open=["Panel 1", "Panel 2"]): + with ui.accordion_panel("Panel 1"): ui.input_slider("n", "N", 1, 100, 50) - with layout.accordion_panel("Panel 2"): + with ui.accordion_panel("Panel 2"): @render.text def txt(): diff --git a/examples/express/shared_app.py b/examples/express/shared_app.py index b16e5005e..d5f874341 100644 --- a/examples/express/shared_app.py +++ b/examples/express/shared_app.py @@ -6,8 +6,8 @@ import numpy as np import shared -from shiny import reactive, render, ui -from shiny.express import input +from shiny import reactive, render +from shiny.express import input, ui @render.plot From f03bb44b11c5c7cc870a8850e781e6c319067a1f Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 13 Dec 2023 14:47:16 -0600 Subject: [PATCH 06/26] Better test error messages --- tests/pytest/test_express_ui.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index 8b3c875ed..ee99effdc 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -21,9 +21,18 @@ def test_express_ui_is_complete(): ui_known_missing = set(xui._known_missing["shiny.ui"]) xui_known_missing = set(xui._known_missing["shiny.express.ui"]) - # Make sure that in shiny.express.ui, there's no overlap between __all__ and - # _known_missing. - assert xui_all.isdisjoint(ui_known_missing) + # Make sure that there's no overlap between shiny.express.ui.__all__ and + # _known_missing["shiny.ui"]; and same for other combinations. Note that the use of + # `.intersection() == set()` instead of `disjoint()` is intentional, because if the + # test fails, the first form provides an error message that shows the difference, + # while the second form does note. + assert xui_all.intersection(ui_known_missing) == set() + assert ui_all.intersection(xui_known_missing) == set() + + # Similar to above, use .difference() instead of .issubset() to get better error + # messages. + assert xui_known_missing.difference(xui_all) == set() + assert ui_known_missing.difference(ui_all) == set() # Make sure that everything from shiny.ui is either exported by shiny.express.ui, or # explicitly listed in shiny.express.ui._known_missing. From 74412af81b95b58dae1f57837f15386a0bfeb08a Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 13 Dec 2023 14:52:53 -0600 Subject: [PATCH 07/26] Update express UI components --- shiny/express/ui/__init__.py | 15 +++++--- shiny/express/ui/_cm_components.py | 60 ++++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index 6c76ef725..3f0cf47ee 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -96,6 +96,7 @@ modal_remove, notification_show, notification_remove, + nav_spacer, output_plot, output_image, output_text, @@ -120,6 +121,8 @@ navset, navset_card, nav, + nav_control, + nav_menu, value_box, panel_well, panel_conditional, @@ -227,6 +230,7 @@ "modal_remove", "notification_show", "notification_remove", + "nav_spacer", "output_plot", "output_image", "output_text", @@ -249,6 +253,8 @@ "navset", "navset_card", "nav", + "nav_control", + "nav_menu", "value_box", "panel_well", "panel_conditional", @@ -265,9 +271,6 @@ _known_missing = { # Items from shiny.ui that don't have a counterpart in shiny.express.ui "shiny.ui": ( - "nav_control", - "nav_menu", - "nav_spacer", "navset_bar", "navset_card_pill", "navset_card_tab", @@ -280,10 +283,10 @@ "navset_tab_card", "navset_underline", "page_bootstrap", - "panel_main", - "panel_sidebar", - "panel_title", "page_output", + "panel_main", # Deprecated + "panel_sidebar", # Deprecated + "panel_title", "popover", "showcase_bottom", "showcase_left_center", diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index 1d5cae375..ecfc73be1 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -25,6 +25,8 @@ "navset", "navset_card", "nav", + "nav_control", + "nav_menu", "panel_well", "panel_conditional", "panel_fixed", @@ -628,6 +630,64 @@ def nav( ) +def nav_control(): + """ + Place a control in the navigation container. + + This function wraps :func:`~shiny.ui.nav_control`. + + Parameters + ---------- + *args + UI elements to display as the nav item. + """ + return RecallContextManager(ui.nav_control) + + +def nav_menu( + title: TagChild, + *, + value: Optional[str] = None, + icon: TagChild = None, + align: Literal["left", "right"] = "left", +): + """ + Create a menu of nav items. + + This function wraps :func:`~shiny.ui.nav_menu`. + + Parameters + ---------- + title + A title to display. Can be a character string or UI elements (i.e., tags). + *args + A collection of nav items (e.g., :func:`~shiny.ui.nav`) and/or strings. + Strings will be rendered as a section header unless the string is a set + of two or more hyphens (e.g., ``---``), in which case it will be rendered + as a divider. + value + The value of the item. This is used to determine whether the item is active + (when an ``id`` is provided to the nav container), programmatically select the + item (e.g., :func:`~shiny.ui.update_navs`), and/or be provided to the + ``selected`` argument of the navigation container (e.g., + :func:`~shiny.ui.navset_tab`). + icon + An icon to appear inline with the button/link. + align + Horizontal alignment of the dropdown menu relative to dropdown toggle. + """ + + return RecallContextManager( + ui.nav_menu, + args=(title,), + kwargs=dict( + value=value, + icon=icon, + align=align, + ), + ) + + # ====================================================================================== # Value boxes # ====================================================================================== From 9d75b26374a3a9eb18ee6933a4133afda1ddfadb Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 14 Dec 2023 15:42:23 -0600 Subject: [PATCH 08/26] Remove row and column from express.ui --- shiny/express/ui/__init__.py | 8 ++--- shiny/express/ui/_cm_components.py | 56 ------------------------------ 2 files changed, 3 insertions(+), 61 deletions(-) diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index c574c6a1e..8003d97a4 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -114,8 +114,6 @@ layout_sidebar, layout_column_wrap, layout_columns, - column, - row, card, accordion, accordion_panel, @@ -247,8 +245,6 @@ "layout_sidebar", "layout_column_wrap", "layout_columns", - "column", - "row", "card", "accordion", "accordion_panel", @@ -273,7 +269,9 @@ _known_missing = { # Items from shiny.ui that don't have a counterpart in shiny.express.ui "shiny.ui": ( - "nav", # Deprecated + "column", # Deprecated in favor of layout_columns + "row", # Deprecated in favor of layout_columns + "nav", # Deprecated in favor of nav_panel "navset_bar", "navset_card_pill", "navset_card_tab", diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index c87857a80..3f0932257 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -19,8 +19,6 @@ "layout_sidebar", "layout_column_wrap", "layout_columns", - "column", - "row", "card", "accordion", "accordion_panel", @@ -393,60 +391,6 @@ def layout_columns( ) -def column(width: int, *, offset: int = 0, **kwargs: TagAttrValue): - """ - Responsive row-column based layout - - This function wraps :func:`~shiny.ui.column`. See :func:`~shiny.ui.row` for more - information. - - Parameters - ---------- - width - The width of the column (an integer between 1 and 12). - offset - The number of columns to offset this column from the end of the previous column. - **kwargs - Attributes to place on the column tag. - - See Also - ------- - :func:`~shiny.ui.row` - """ - return RecallContextManager( - ui.column, - args=(width,), - kwargs=dict( - offset=offset, - **kwargs, - ), - ) - - -def row(**kwargs: TagAttrValue): - """ - Responsive row-column based layout - - This function wraps :func:`~shiny.ui.row`. Layout UI components using Bootstrap's - grid layout system. Use ``row()`` to group elements that should appear on the same - line (if the browser has adequate width) and :func:`~shiny.ui.column` to define how - much horizontal space within a 12-unit wide grid each on of these elements should - occupy. See the [layout guide](https://shiny.posit.co/articles/layout-guide.html>) - for more context and examples. (The article is about Shiny for R, but the general - principles are the same.) - - Parameters - ---------- - **kwargs - Attributes to place on the row tag. - - See Also - ------- - :func:`~shiny.ui.column` - """ - return RecallContextManager(ui.row, kwargs=kwargs) - - def card( *, full_screen: bool = False, From b5520677819a323e6c8e314bb28c74c6f5eef665 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 14 Dec 2023 22:05:53 -0600 Subject: [PATCH 09/26] Move TopLevelRecallContextManager code to page_auto --- shiny/express/_page.py | 78 ++++++++++++++++++++++++++++ shiny/express/_recall_context.py | 88 ++------------------------------ shiny/express/_run.py | 7 +-- 3 files changed, 85 insertions(+), 88 deletions(-) create mode 100644 shiny/express/_page.py diff --git a/shiny/express/_page.py b/shiny/express/_page.py new file mode 100644 index 000000000..d53ea7ccb --- /dev/null +++ b/shiny/express/_page.py @@ -0,0 +1,78 @@ +from __future__ import annotations + +from typing import cast + +from htmltools import Tag + +from .. import ui +from ..ui._navs import NavMenu, NavPanel +from ._recall_context import RecallContextManager + + +def page_auto_cm() -> RecallContextManager[Tag]: + return RecallContextManager(page_auto) + + +def page_auto(*args: object) -> Tag: + # Presence of a top-level nav items and/or sidebar determines the page function + navs = [x for x in args if isinstance(x, (NavPanel, NavMenu))] + sidebars = [x for x in args if isinstance(x, ui.Sidebar)] + + nNavs = len(navs) + nSidebars = len(sidebars) + + # TODO: How should this work with .set_page_*()/.set_title()? + if nNavs == 0: + if nSidebars == 0: + return _DEFAULT_PAGE_FUNCTION( + *args # pyright: ignore[reportGeneralTypeIssues] + ) + + if nSidebars == 1: + # page_sidebar() needs sidebar to be the first arg + new_args = sidebars + [x for x in args if x not in sidebars] + return ui.page_sidebar( + *new_args # pyright: ignore[reportGeneralTypeIssues] + ) + + # If multiple sidebars(), wrap them in layout_sidebar() + # TODO: + # 1. Maybe this logic be should handled by non-top-level ctx managers? + # That is, if we're not in a top-level ctx manager, automatically wrap + # Sidebar() into layout_sidebar()? + # 2. Provide a way to exit the layout.sidebar() context? Maybe '---'? + if nSidebars > 1: + new_args: object = [] + sidebar_idx = [i for i, x in enumerate(args) if isinstance(x, ui.Sidebar)] + new_args.append(*args[0 : sidebar_idx[0]]) + for i, x in enumerate(sidebar_idx): + j = sidebar_idx[i + 1] if i < len(sidebar_idx) - 1 else len(args) + s = ui.layout_sidebar( + cast(ui.Sidebar, args[x]), + *self.args[x + 1 : j], # type: ignore + ) + new_args.append(s) + + return _DEFAULT_PAGE_FUNCTION(*new_args) + + # At least one nav + else: + if nSidebars == 0: + # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? + return ui.page_navbar(*args) # pyright: ignore[reportGeneralTypeIssues] + + if nSidebars == 1: + return ui.page_navbar( + *args, # pyright: ignore[reportGeneralTypeIssues] + sidebar=sidebars[0], + ) + + if nSidebars > 1: + raise NotImplementedError( + "Multiple top-level sidebars not allowed in combination with top-level navs" + ) + + return _DEFAULT_PAGE_FUNCTION(*args) # pyright: ignore[reportGeneralTypeIssues] + + +_DEFAULT_PAGE_FUNCTION = ui.page_fixed diff --git a/shiny/express/_recall_context.py b/shiny/express/_recall_context.py index 6e48a93c9..2bac6e078 100644 --- a/shiny/express/_recall_context.py +++ b/shiny/express/_recall_context.py @@ -3,13 +3,11 @@ import functools import sys from types import TracebackType -from typing import Callable, Generic, Mapping, Optional, Type, TypeVar, cast +from typing import Callable, Generic, Mapping, Optional, Type, TypeVar -from htmltools import Tag, wrap_displayhook_handler +from htmltools import wrap_displayhook_handler -from .. import ui from .._typing_extensions import ParamSpec -from ..ui._navs import NavMenu, NavPanel P = ParamSpec("P") R = TypeVar("R") @@ -24,7 +22,7 @@ def __init__( args: tuple[object, ...] | None = None, kwargs: Mapping[str, object] | None = None, ): - self._fn = fn + self.fn = fn if args is None: args = tuple() if kwargs is None: @@ -49,86 +47,6 @@ def __exit__( sys.displayhook(res) return False - @property - def fn(self) -> Callable[..., R]: - return self._fn - - -class TopLevelRecallContextManager(RecallContextManager[Tag]): - def __init__( - self, - *, - args: tuple[object, ...] | None = None, - kwargs: Mapping[str, object] | None = None, - ): - super().__init__(lambda x: x, args=args, kwargs=kwargs) - - @property - def fn(self) -> Callable[..., Tag]: - # Presence of a top-level nav items and/or sidebar determines the page function - navs = [x for x in self.args if isinstance(x, (NavPanel, NavMenu))] - sidebars = [x for x in self.args if isinstance(x, ui.Sidebar)] - - nNavs = len(navs) - nSidebars = len(sidebars) - - # TODO: How should this work with .set_page_*()/.set_title()? - if nNavs == 0: - if nSidebars == 0: - return _DEFAULT_PAGE_FUNCTION - - if nSidebars == 1: - # page_sidebar() needs sidebar to be the first arg - self.args = sidebars + [x for x in self.args if x not in sidebars] - return ui.page_sidebar - - # If multiple sidebars(), wrap them in layout_sidebar() - # TODO: - # 1. Maybe this logic be should handled by non-top-level ctx managers? - # That is, if we're not in a top-level ctx manager, automatically wrap - # Sidebar() into layout_sidebar()? - # 2. Provide a way to exit the layout.sidebar() context? Maybe '---'? - if nSidebars > 1: - new_args: object = [] - sidebar_idx = [ - i for i, x in enumerate(self.args) if isinstance(x, ui.Sidebar) - ] - new_args.append(*self.args[0 : sidebar_idx[0]]) - for i, x in enumerate(sidebar_idx): - j = ( - sidebar_idx[i + 1] - if i < len(sidebar_idx) - 1 - else len(self.args) - ) - s = ui.layout_sidebar( - cast(ui.Sidebar, self.args[x]), - *self.args[x + 1 : j], # type: ignore - ) - new_args.append(s) - - self.args = new_args - return _DEFAULT_PAGE_FUNCTION - - # At least one nav - else: - if nSidebars == 0: - # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? - return ui.page_navbar - - if nSidebars == 1: - self.kwargs["sidebar"] = self.kwargs.get("sidebar", sidebars[0]) - return ui.page_navbar - - if nSidebars > 1: - raise NotImplementedError( - "Multiple top-level sidebars not allowed in combination with top-level navs" - ) - - return _DEFAULT_PAGE_FUNCTION - - -_DEFAULT_PAGE_FUNCTION = ui.page_fixed - def wrap_recall_context_manager( fn: Callable[P, R] diff --git a/shiny/express/_run.py b/shiny/express/_run.py index 94ebeb7f6..5892755eb 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -10,7 +10,8 @@ from .._app import App from ..session import Inputs, Outputs, Session -from ._recall_context import RecallContextManager, TopLevelRecallContextManager +from ._page import page_auto_cm +from ._recall_context import RecallContextManager from .display_decorator._func_displayhook import _display_decorator_function_def from .display_decorator._node_transformers import ( DisplayFuncsTransformer, @@ -136,7 +137,7 @@ def set_result(x: object): def reset_top_level_recall_context_manager(): global _top_level_recall_context_manager global _top_level_recall_context_manager_has_been_replaced - _top_level_recall_context_manager = TopLevelRecallContextManager() + _top_level_recall_context_manager = page_auto_cm() _top_level_recall_context_manager_has_been_replaced = False @@ -162,7 +163,7 @@ def replace_top_level_recall_context_manager( The RecallContextManager to replace the previous one. force If `False` (the default) and the top level RecallContextManager has already been - replaced, return with no chnages. If `True`, this will aways replace. + replaced, return with no changes. If `True`, this will aways replace. Returns ------- From 1a8935d6ebafa36e68cac3b09692e89d648fbec9 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 14 Dec 2023 22:34:45 -0600 Subject: [PATCH 10/26] For multiple sidebars, require use of layout_sidebar() --- shiny/express/_page.py | 24 +++--------------------- 1 file changed, 3 insertions(+), 21 deletions(-) diff --git a/shiny/express/_page.py b/shiny/express/_page.py index d53ea7ccb..2ce54f55d 100644 --- a/shiny/express/_page.py +++ b/shiny/express/_page.py @@ -1,7 +1,5 @@ from __future__ import annotations -from typing import cast - from htmltools import Tag from .. import ui @@ -21,7 +19,6 @@ def page_auto(*args: object) -> Tag: nNavs = len(navs) nSidebars = len(sidebars) - # TODO: How should this work with .set_page_*()/.set_title()? if nNavs == 0: if nSidebars == 0: return _DEFAULT_PAGE_FUNCTION( @@ -35,25 +32,10 @@ def page_auto(*args: object) -> Tag: *new_args # pyright: ignore[reportGeneralTypeIssues] ) - # If multiple sidebars(), wrap them in layout_sidebar() - # TODO: - # 1. Maybe this logic be should handled by non-top-level ctx managers? - # That is, if we're not in a top-level ctx manager, automatically wrap - # Sidebar() into layout_sidebar()? - # 2. Provide a way to exit the layout.sidebar() context? Maybe '---'? if nSidebars > 1: - new_args: object = [] - sidebar_idx = [i for i, x in enumerate(args) if isinstance(x, ui.Sidebar)] - new_args.append(*args[0 : sidebar_idx[0]]) - for i, x in enumerate(sidebar_idx): - j = sidebar_idx[i + 1] if i < len(sidebar_idx) - 1 else len(args) - s = ui.layout_sidebar( - cast(ui.Sidebar, args[x]), - *self.args[x + 1 : j], # type: ignore - ) - new_args.append(s) - - return _DEFAULT_PAGE_FUNCTION(*new_args) + raise NotImplementedError( + "Multiple top-level sidebars not allowed. Did you meant to wrap each one in layout_sidebar()?" + ) # At least one nav else: From 8afcc28fab10c225eeeae7609f6268a55f3613b2 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Fri, 15 Dec 2023 18:54:09 -0600 Subject: [PATCH 11/26] Add set_page_*, use_page_* functions --- shiny/express/_page.py | 358 ++++++++++++++++++++++++++++++++++++----- shiny/express/_run.py | 55 +------ 2 files changed, 318 insertions(+), 95 deletions(-) diff --git a/shiny/express/_page.py b/shiny/express/_page.py index 2ce54f55d..e34a75264 100644 --- a/shiny/express/_page.py +++ b/shiny/express/_page.py @@ -1,17 +1,50 @@ from __future__ import annotations -from htmltools import Tag +from typing import Callable, Literal, Optional, cast + +from htmltools import Tag, TagAttrValue, TagChild, TagList from .. import ui +from ..types import MISSING, MISSING_TYPE from ..ui._navs import NavMenu, NavPanel +from ..ui._sidebar import Sidebar +from ..ui.css import CssUnit from ._recall_context import RecallContextManager +from ._run import get_top_level_recall_context_manager + +__all__ = ( + "page_auto", + "page_auto_cm", + "set_page_title", + "set_page_lang", + "set_page_fillable", + "set_page_wider", + "use_page_fixed", + "use_page_fluid", + "use_page_fillable", + "use_page_sidebar", + "use_page_navbar", +) def page_auto_cm() -> RecallContextManager[Tag]: - return RecallContextManager(page_auto) + return RecallContextManager( + page_auto, + kwargs={ + "_page_fn": None, + "_fillable": False, + "_wider": False, + }, + ) -def page_auto(*args: object) -> Tag: +def page_auto( + *args: object, + _page_fn: Callable[[object], Tag] | None, + _fillable: bool, + _wider: bool, + **kwargs: object, +) -> Tag: # Presence of a top-level nav items and/or sidebar determines the page function navs = [x for x in args if isinstance(x, (NavPanel, NavMenu))] sidebars = [x for x in args if isinstance(x, ui.Sidebar)] @@ -19,42 +52,283 @@ def page_auto(*args: object) -> Tag: nNavs = len(navs) nSidebars = len(sidebars) - if nNavs == 0: - if nSidebars == 0: - return _DEFAULT_PAGE_FUNCTION( - *args # pyright: ignore[reportGeneralTypeIssues] - ) - - if nSidebars == 1: - # page_sidebar() needs sidebar to be the first arg - new_args = sidebars + [x for x in args if x not in sidebars] - return ui.page_sidebar( - *new_args # pyright: ignore[reportGeneralTypeIssues] - ) - - if nSidebars > 1: - raise NotImplementedError( - "Multiple top-level sidebars not allowed. Did you meant to wrap each one in layout_sidebar()?" - ) - - # At least one nav - else: - if nSidebars == 0: - # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? - return ui.page_navbar(*args) # pyright: ignore[reportGeneralTypeIssues] - - if nSidebars == 1: - return ui.page_navbar( - *args, # pyright: ignore[reportGeneralTypeIssues] - sidebar=sidebars[0], - ) - - if nSidebars > 1: - raise NotImplementedError( - "Multiple top-level sidebars not allowed in combination with top-level navs" - ) - - return _DEFAULT_PAGE_FUNCTION(*args) # pyright: ignore[reportGeneralTypeIssues] - - -_DEFAULT_PAGE_FUNCTION = ui.page_fixed + if _page_fn is None: + if nNavs == 0: + if nSidebars == 0: + if _fillable: + _page_fn = ( + ui.page_fillable + ) # pyright: ignore[reportGeneralTypeIssues] + elif _wider: + _page_fn = ui.page_fluid # pyright: ignore[reportGeneralTypeIssues] + else: + _page_fn = ui.page_fixed # pyright: ignore[reportGeneralTypeIssues] + + elif nSidebars == 1: + # page_sidebar() needs sidebar to be the first arg + # TODO: Change page_sidebar() to remove `sidebar` and accept a sidebar as a + # *arg. + _page_fn = ui.page_sidebar # pyright: ignore[reportGeneralTypeIssues] + args = tuple(sidebars + [x for x in args if x not in sidebars]) + + else: + raise NotImplementedError( + "Multiple top-level sidebars not allowed. Did you meant to wrap each one in layout_sidebar()?" + ) + + # At least one nav + else: + if nSidebars == 0: + # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? + _page_fn = ui.page_navbar # pyright: ignore[reportGeneralTypeIssues] + + elif nSidebars == 1: + # TODO: change page_navbar() to remove `sidebar` and accept a sidebar as a + # *arg. + _page_fn = ui.page_navbar # pyright: ignore[reportGeneralTypeIssues] + kwargs["sidebar"] = sidebars[0] + + else: + raise NotImplementedError( + "Multiple top-level sidebars not allowed in combination with top-level navs." + ) + + # If we got here, _page_fn is not None, but the type checker needs a little help. + _page_fn = cast(Callable[[object], Tag], _page_fn) + return _page_fn(*args, **kwargs) + + +# ====================================================================================== +# Page attribute setters +# ====================================================================================== + + +def set_page_title(title: str) -> None: + get_top_level_recall_context_manager().kwargs["title"] = title + + +def set_page_fillable(fillable: bool) -> None: + get_top_level_recall_context_manager().kwargs["_fillable"] = fillable + + +def set_page_wider(wider: bool) -> None: + get_top_level_recall_context_manager().kwargs["_wider"] = wider + + +def set_page_lang(lang: str) -> None: + get_top_level_recall_context_manager().kwargs["lang"] = lang + + +# ====================================================================================== +# Page functions +# ====================================================================================== + + +def use_page_fixed( + *, + title: Optional[str] = None, + lang: Optional[str] = None, + **kwargs: str, +) -> None: + """ + Create a fixed page. + + This function wraps :func:`~shiny.ui.page_fixed`. + + Parameters + ---------- + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + **kwargs + Attributes on the page level container. + """ + get_top_level_recall_context_manager().kwargs.update( + dict( + _page_fn=ui.page_fixed, + title=title, + lang=lang, + **kwargs, + ) + ) + + +def use_page_fluid( + *, + title: Optional[str] = None, + lang: Optional[str] = None, + **kwargs: str, +) -> None: + """ + Create a fluid page. + + This function wraps :func:`~shiny.ui.page_fluid`. + + Parameters + ---------- + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + **kwargs + Attributes on the page level container. + """ + get_top_level_recall_context_manager().kwargs.update( + dict( + _page_fn=ui.page_fluid, + title=title, + lang=lang, + **kwargs, + ) + ) + + +def use_page_fillable( + *, + padding: Optional[CssUnit | list[CssUnit]] = None, + gap: Optional[CssUnit] = None, + fillable_mobile: bool = False, + title: Optional[str] = None, + lang: Optional[str] = None, + **kwargs: TagAttrValue, +) -> None: + """ + Use a fillable page. + + This function wraps :func:`~shiny.ui.page_fillable`. + + Parameters + ---------- + padding + Padding to use for the body. See :func:`~shiny.ui.css_unit.as_css_padding` + for more details. + fillable_mobile + Whether or not the page should fill the viewport's height on mobile devices + (i.e., narrow windows). + gap + A CSS length unit passed through :func:`~shiny.ui.css_unit.as_css_unit` + defining the `gap` (i.e., spacing) between elements provided to `*args`. + title + The browser window title (defaults to the host URL of the page). Can also be set + as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + """ + get_top_level_recall_context_manager().kwargs.update( + dict( + _page_fn=ui.page_fillable, + padding=padding, + gap=gap, + fillable_mobile=fillable_mobile, + title=title, + lang=lang, + **kwargs, + ) + ) + + +def use_page_sidebar( + *, + title: Optional[str | Tag | TagList] = None, + fillable: bool = True, + fillable_mobile: bool = False, + window_title: str | MISSING_TYPE = MISSING, + lang: Optional[str] = None, + **kwargs: TagAttrValue, +) -> None: + """ + Create a page with a sidebar and a title. + This function wraps :func:`~shiny.ui.page_sidebar`. + Parameters + ---------- + sidebar + Content to display in the sidebar. + title + A title to display at the top of the page. + fillable + Whether or not the main content area should be considered a fillable + (i.e., flexbox) container. + fillable_mobile + Whether or not ``fillable`` should apply on mobile devices. + window_title + The browser's window title (defaults to the host URL of the page). Can also be + set as a side effect via :func:`~shiny.ui.panel_title`. + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + **kwargs + Additional attributes passed to :func:`~shiny.ui.layout_sidebar`. + Returns + ------- + : + A UI element. + """ + get_top_level_recall_context_manager().kwargs.update( + dict( + _page_fn=ui.page_sidebar, + title=title, + fillable=fillable, + fillable_mobile=fillable_mobile, + window_title=window_title, + lang=lang, + **kwargs, + ) + ) + + +def use_page_navbar( + *, + title: Optional[str | Tag | TagList] = None, + id: Optional[str] = None, + selected: Optional[str] = None, + sidebar: Optional[Sidebar] = None, + # Only page_navbar gets enhanced treatement for `fillable` + # If an `*args`'s `data-value` attr string is in `fillable`, then the component is fillable + fillable: bool | list[str] = True, + fillable_mobile: bool = False, + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, + position: Literal["static-top", "fixed-top", "fixed-bottom"] = "static-top", + header: Optional[TagChild] = None, + footer: Optional[TagChild] = None, + bg: Optional[str] = None, + inverse: bool = False, + underline: bool = True, + collapsible: bool = True, + fluid: bool = True, + window_title: str | MISSING_TYPE = MISSING, + lang: Optional[str] = None, +) -> None: + get_top_level_recall_context_manager().kwargs.update( + dict( + _page_fn=ui.page_navbar, + title=title, + id=id, + selected=selected, + sidebar=sidebar, + fillable=fillable, + fillable_mobile=fillable_mobile, + gap=gap, + padding=padding, + position=position, + header=header, + footer=footer, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + fluid=fluid, + window_title=window_title, + lang=lang, + ) + ) diff --git a/shiny/express/_run.py b/shiny/express/_run.py index 5892755eb..e5c63ee48 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -10,7 +10,6 @@ from .._app import App from ..session import Inputs, Outputs, Session -from ._page import page_auto_cm from ._recall_context import RecallContextManager from .display_decorator._func_displayhook import _display_decorator_function_def from .display_decorator._node_transformers import ( @@ -131,64 +130,14 @@ def set_result(x: object): _top_level_recall_context_manager: RecallContextManager[Tag] -_top_level_recall_context_manager_has_been_replaced = False def reset_top_level_recall_context_manager(): + from ._page import page_auto_cm + global _top_level_recall_context_manager - global _top_level_recall_context_manager_has_been_replaced _top_level_recall_context_manager = page_auto_cm() - _top_level_recall_context_manager_has_been_replaced = False def get_top_level_recall_context_manager(): return _top_level_recall_context_manager - - -def replace_top_level_recall_context_manager( - cm: RecallContextManager[Tag], - force: bool = False, -) -> RecallContextManager[Tag]: - """ - Replace the current top level RecallContextManager with another one. - - This transfers the `args` and `kwargs` from the previous RecallContextManager to the - new one. Normally it will only have an effect the first time it's run; it only - replace the previous one if has not already been replaced. To override this - behavior, this use `force=True`. - - Parameters - ---------- - cm - The RecallContextManager to replace the previous one. - force - If `False` (the default) and the top level RecallContextManager has already been - replaced, return with no changes. If `True`, this will aways replace. - - Returns - ------- - : - The previous top level RecallContextManager. - """ - global _top_level_recall_context_manager - global _top_level_recall_context_manager_has_been_replaced - - old_cm = _top_level_recall_context_manager - - if force is False and _top_level_recall_context_manager_has_been_replaced: - return old_cm - - args = old_cm.args.copy() - args.extend(cm.args) - cm.args = args - - kwargs = old_cm.kwargs.copy() - kwargs.update(cm.kwargs) - cm.kwargs = kwargs - - old_cm.__exit__(BaseException, None, None) - cm.__enter__() - _top_level_recall_context_manager = cm - _top_level_recall_context_manager_has_been_replaced = True - - return old_cm From af25ce2a9b82b6b416dc49e4c85c5b9d1706f9a0 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Mon, 18 Dec 2023 11:15:04 -0600 Subject: [PATCH 12/26] Update docstrings --- shiny/express/ui/_cm_components.py | 100 ++++++++++++++++++++++------- 1 file changed, 77 insertions(+), 23 deletions(-) diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index 3f0932257..c425390e1 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -65,7 +65,7 @@ def sidebar( padding: Optional[CssUnit | list[CssUnit]] = None, ) -> RecallContextManager[ui.Sidebar]: """ - Sidebar element + Context manager for sidebar element Create a collapsing sidebar layout. This function wraps :func:`~shiny.ui.sidebar`. @@ -150,7 +150,7 @@ def layout_sidebar( **kwargs: TagAttrValue, ): """ - Sidebar layout + Context manager for sidebar layout Create a sidebar layout component which can be dropped inside any Shiny UI page method (e.g. :func:`~shiny.shiny.ui.page_fillable`) or :func:`~shiny.ui.card` @@ -224,7 +224,7 @@ def layout_column_wrap( **kwargs: TagAttrValue, ): """ - A grid-like, column-first layout + Context manager for a grid-like, column-first layout Wraps a 1d sequence of UI elements into a 2d grid. The number of columns (and rows) in the grid dependent on the column `width` as well as the size of the display. @@ -299,7 +299,8 @@ def layout_columns( **kwargs: TagAttrValue, ): """ - Create responsive, column-based grid layouts, based on a 12-column grid. + Context manager for responsive, column-based grid layouts, based on a 12-column + grid. Parameters ---------- @@ -318,19 +319,19 @@ def layout_columns( element (which wraps to the next row). Negative values are also allowed, and are treated as empty columns. For example, `col_widths=(-2, 8, -2)` would allocate 8 columns to an element (with 2 empty columns on either side). - * A dictionary of column widths at different breakpoints. The keys should be - one of `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, or `"xxl"`, and the values are + * A dictionary of column widths at different breakpoints. The keys should be one + of `"xs"`, `"sm"`, `"md"`, `"lg"`, `"xl"`, or `"xxl"`, and the values are either of the above. For example, `col_widths={"sm": (3, 3, 6), "lg": (4)}`. row_heights The heights of the rows, possibly at different breakpoints. Can be one of the following: - * A numeric vector, where each value represents the - [fractional unit](https://css-tricks.com/introduction-fr-css-unit/) - (`fr`) height of the relevant row. If there are more rows than values - provided, the pattern will be repeated. For example, `row_heights=(1, 2)` - allows even rows to take up twice as much space as odd rows. + * A numeric vector, where each value represents the [fractional + unit](https://css-tricks.com/introduction-fr-css-unit/) (`fr`) height of the + relevant row. If there are more rows than values provided, the pattern will be + repeated. For example, `row_heights=(1, 2)` allows even rows to take up twice + as much space as odd rows. * A list of numeric or CSS length units, where each value represents the height of the relevant row. If more rows are needed than values provided, the pattern will repeat. For example, `row_heights=["auto", 1]` allows the height of odd @@ -402,7 +403,7 @@ def card( **kwargs: TagAttrValue, ): """ - A Bootstrap card component + Context manager for Bootstrap card component This function wraps :func:`~shiny.ui.card`. A general purpose container for grouping related UI elements together with a border and optional padding. To learn more about @@ -457,7 +458,7 @@ def accordion( **kwargs: TagAttrValue, ): """ - Create a vertically collapsing accordion. + Context manager for a vertically collapsing accordion. This function wraps :func:`~shiny.ui.accordion`. @@ -508,7 +509,7 @@ def accordion_panel( **kwargs: TagAttrValue, ): """ - Single accordion panel. + Context manager for single accordion panel. This function wraps :func:`~shiny.ui.accordion_panel`. @@ -549,7 +550,7 @@ def navset( footer: TagChild = None, ): """ - Render a set of nav items + Context manager for a set of nav items Parameters ---------- @@ -601,7 +602,7 @@ def navset_card( footer: TagChild = None, ): """ - Render a set of nav items inside a card container. + Context manager for a set of nav items inside a card container. Parameters ---------- @@ -653,7 +654,7 @@ def nav_panel( icon: TagChild = None, ): """ - Create a nav item pointing to some internal content. + Context manager for nav item pointing to some internal content. This function wraps :func:`~shiny.ui.nav`. @@ -682,7 +683,7 @@ def nav_panel( def nav_control(): """ - Place a control in the navigation container. + Context manager for a control in the navigation container. This function wraps :func:`~shiny.ui.nav_control`. @@ -702,7 +703,7 @@ def nav_menu( align: Literal["left", "right"] = "left", ): """ - Create a menu of nav items. + Context manager for a menu of nav items. This function wraps :func:`~shiny.ui.nav_menu`. @@ -756,6 +757,59 @@ def value_box( class_: Optional[str] = None, **kwargs: TagAttrValue, ): + """ + Context manager for a value box + + This function wraps :func:`~shiny.ui.value_box`. + + An opinionated (:func:`~shiny.ui.card`-powered) box, designed for + displaying a `value` and `title`. Optionally, a `showcase` can provide for context + for what the `value` represents (for example, it could hold an icon, or even a + :func:`~shiny.ui.output_plot`). + + Parameters + ---------- + title,value + A string, number, or :class:`~htmltools.Tag` child to display as + the title or value of the value box. The `title` appears above the `value`. + showcase + A :class:`~htmltools.Tag` child to showcase (e.g., an icon, a + :func:`~shiny.ui.output_plot`, etc). + showcase_layout + One of `"left center"` (default), `"top right"` or `"bottom"`. Alternatively, + you can customize the showcase layout options with the + :func:`~shiny.ui.showcase_left_center`, :func:`~shiny.ui.showcase_top_right()`, + or :func:`~shiny.ui.showcase_bottom()` functions. Use the options functions when + you want to control the height or width of the showcase area. + theme + The name of a theme (e.g. `"primary"`, `"danger"`, `"purple"`, `"bg-green"`, + `"text-red"`) for the value box, or a theme constructed with + :func:`~shiny.ui.value_box_theme`. The theme names provide a convenient way to + use your app's Bootstrap theme colors as the foreground or background colors of + the value box. For more control, you can create your own theme with + :func:`~shiny.ui.value_box_theme` where you can pass foreground and background + colors directly. Bootstrap supported color themes: `"blue"`, `"purple"`, + `"pink"`, `"red"`, `"orange"`, `"yellow"`, `"green"`, `"teal"`, and `"cyan"`. + These colors can be used with `bg-NAME`, `text-NAME`, and + `bg-gradient-NAME1-NAME2` to change the background, foreground, or use a + background gradient respectively. If a `theme` string does not start with + `text-` or `bg-`, it will be auto prefixed with `bg-`. + full_screen + If `True`, an icon will appear when hovering over the card body. Clicking the + icon expands the card to fit viewport size. + height,max_height + Any valid CSS unit (e.g., `height="200px"`). Doesn't apply when a card is made + `full_screen`. + fill + Whether to allow the value box to grow/shrink to fit a fillable container with + an opinionated height (e.g., :func:`~shiny.ui.page_fillable`). + class_ + Utility classes for customizing the appearance of the summary card. Use `bg-*` + and `text-*` classes (e.g, `"bg-danger"` and `"text-light"`) to customize the + background/foreground colors. + **kwargs + Additional attributes to pass to :func:`~shiny.ui.card`. + """ return RecallContextManager( ui.value_box, args=(title, value), @@ -780,7 +834,7 @@ def value_box( def panel_well(**kwargs: TagAttrValue): """ - Create a well panel + Context manager for a well panel This function wraps :func:`~shiny.ui.panel_well`. @@ -801,7 +855,7 @@ def panel_conditional( **kwargs: TagAttrValue, ): """ - Create a conditional panel + Context manager for a conditional panel This function wraps :func:`~shiny.ui.panel_conditional`. @@ -854,7 +908,7 @@ def panel_fixed( **kwargs: TagAttrValue, ): """ - Create a panel of absolutely positioned content. + Context manager for a panel of absolutely positioned content. This function wraps :func:`~shiny.ui.panel_fixed`. @@ -901,7 +955,7 @@ def panel_absolute( **kwargs: TagAttrValue, ): """ - Create a panel of absolutely positioned content. + Context manager for a panel of absolutely positioned content. This function wraps :func:`~shiny.ui.panel_absolute`. From 55b60fdc03a0d5be4dd86cbd4f061c8c0f8a4802 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 16:38:05 -0600 Subject: [PATCH 13/26] Add .tagify() method to RecallContextManager --- shiny/express/_recall_context.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/shiny/express/_recall_context.py b/shiny/express/_recall_context.py index 0dc55d52a..dc636d6a0 100644 --- a/shiny/express/_recall_context.py +++ b/shiny/express/_recall_context.py @@ -5,7 +5,7 @@ from types import TracebackType from typing import Callable, Generic, Mapping, Optional, Type, TypeVar -from htmltools import Tag, wrap_displayhook_handler +from htmltools import MetadataNode, Tag, TagList, wrap_displayhook_handler from .._typing_extensions import ParamSpec @@ -54,6 +54,19 @@ def __exit__( sys.displayhook(res) return False + def tagify(self) -> Tag | TagList | MetadataNode | str: + res = self.fn(*self.args, **self.kwargs) + + if callable(getattr(res, "tagify", None)): + return res.tagify() # pyright: ignore + if callable(getattr(res, "_repr_html_", None)): + return res._repr_html_() # pyright: ignore + + raise RuntimeError( + "RecallContextManager was used without `with`. When used this way, the " + "result must have a .tagify() or ._repr_html_() method, but it does not." + ) + def wrap_recall_context_manager( fn: Callable[P, R] From 16d03f2166ebe19dfa5eeed130c40a5fa3f75ea1 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 18:40:06 -0600 Subject: [PATCH 14/26] Add shiny.express.layout compatibility shim --- shiny/express/layout.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 shiny/express/layout.py diff --git a/shiny/express/layout.py b/shiny/express/layout.py new file mode 100644 index 000000000..a6b72473e --- /dev/null +++ b/shiny/express/layout.py @@ -0,0 +1,14 @@ +import warnings + +from . import ui + +warnings.warn( + "shiny.express.layout has been deprecated and renamed to shiny.express.ui. " + "Please import shiny.express.ui instead of shiny.express.layout.", + ImportWarning, + stacklevel=2, +) + + +def __getattr__(name: str) -> object: + return getattr(ui, name) From 364cbad692eb9dd9b3062873817dad4fb9528965 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 18:58:31 -0600 Subject: [PATCH 15/26] Remove navset and navset_card --- shiny/express/ui/__init__.py | 42 ++-- shiny/express/ui/_cm_components.py | 377 ++++++++++++++++++++++++++--- 2 files changed, 365 insertions(+), 54 deletions(-) diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index 8003d97a4..d529ec2e9 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -117,11 +117,18 @@ card, accordion, accordion_panel, - navset, - navset_card, nav_panel, nav_control, nav_menu, + navset_bar, + navset_card_pill, + navset_card_tab, + navset_card_underline, + navset_hidden, + navset_pill, + navset_pill_list, + navset_tab, + navset_underline, value_box, panel_well, panel_conditional, @@ -248,11 +255,18 @@ "card", "accordion", "accordion_panel", - "navset", - "navset_card", "nav_panel", "nav_control", "nav_menu", + "navset_bar", + "navset_card_pill", + "navset_card_tab", + "navset_card_underline", + "navset_hidden", + "navset_pill", + "navset_pill_list", + "navset_tab", + "navset_underline", "value_box", "panel_well", "panel_conditional", @@ -272,17 +286,8 @@ "column", # Deprecated in favor of layout_columns "row", # Deprecated in favor of layout_columns "nav", # Deprecated in favor of nav_panel - "navset_bar", - "navset_card_pill", - "navset_card_tab", - "navset_card_underline", - "navset_hidden", - "navset_pill", - "navset_pill_card", - "navset_pill_list", - "navset_tab", - "navset_tab_card", - "navset_underline", + "navset_pill_card", # Deprecated + "navset_tab_card", # Deprecated "page_bootstrap", "page_output", "panel_main", # Deprecated @@ -295,10 +300,5 @@ "tooltip", ), # Items from shiny.express.ui that don't have a counterpart in shiny.ui - "shiny.express.ui": ( - "set_page", - # TODO: Migrate these to shiny.ui - "navset", - "navset_card", - ), + "shiny.express.ui": ("set_page",), } diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index 3e673399f..a3cba68f8 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -11,7 +11,7 @@ from ...ui._accordion import AccordionPanel from ...ui._card import CardItem from ...ui._layout_columns import BreakpointsUser -from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetCard +from ...ui._navs import NavMenu, NavPanel, NavSet, NavSetBar, NavSetCard from ...ui.css import CssUnit from .. import _run from .._recall_context import RecallContextManager @@ -556,21 +556,20 @@ def accordion_panel( # ====================================================================================== -def navset( +def navset_tab( *, - type: Literal["underline", "pill", "tab"] = "underline", id: Optional[str] = None, selected: Optional[str] = None, header: TagChild = None, footer: TagChild = None, ) -> RecallContextManager[NavSet]: """ - Context manager for a set of nav items + Context manager for a set of nav items as a tabset. + + This function wraps :func:`~shiny.ui.navset_tab`. Parameters ---------- - type - The type of navset to render. Can be one of `"underline"`, `"pill"`, or `"tab"`. id If provided, will create an input value that holds the currently selected nav item. @@ -582,21 +581,81 @@ def navset( footer UI to display below the selected content. """ - # *args - # A collection of nav items (e.g., :func:`shiny.ui.nav`). + return RecallContextManager( + ui.navset_tab, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + ), + ) + + +def navset_pill( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSet]: + """ + Context manager for a set of nav items as a pillset. + + This function wraps :func:`~shiny.ui.navset_pill`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_pill, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + ), + ) + - funcs = { - "underline": ui.navset_underline, - "pill": ui.navset_pill, - "tab": ui.navset_tab, - } +def navset_underline( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSet]: + """ + Context manager for a set of nav items whose active/focused navigation links are + styled with an underline. - func = funcs.get(type, None) - if func is None: - raise ValueError(f"Invalid navset type: {type!r}") + This function wraps :func:`~shiny.ui.navset_underline`. + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ return RecallContextManager( - func, + ui.navset_underline, kwargs=dict( id=id, selected=selected, @@ -606,9 +665,44 @@ def navset( ) -def navset_card( +def navset_hidden( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSet]: + """ + Context manager for nav contents without the nav items. + + This function wraps :func:`~shiny.ui.navset_hidden`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ + return RecallContextManager( + ui.navset_hidden, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + ), + ) + + +def navset_card_tab( *, - type: Literal["underline", "pill", "tab"] = "underline", id: Optional[str] = None, selected: Optional[str] = None, title: Optional[TagChild] = None, @@ -617,12 +711,12 @@ def navset_card( footer: TagChild = None, ) -> RecallContextManager[NavSetCard]: """ - Context manager for a set of nav items inside a card container. + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_card_tab`. Parameters ---------- - type - The type of navset to render. Can be one of `"underline"`, `"pill"`, or `"tab"`. id If provided, will create an input value that holds the currently selected nav item. @@ -636,21 +730,50 @@ def navset_card( footer UI to display below the selected content. """ - # *args - # A collection of nav items (e.g., :func:`shiny.ui.nav`). + return RecallContextManager( + ui.navset_card_tab, + kwargs=dict( + id=id, + selected=selected, + title=title, + sidebar=sidebar, + header=header, + footer=footer, + ), + ) - funcs = { - "underline": ui.navset_card_underline, - "pill": ui.navset_card_pill, - "tab": ui.navset_card_tab, - } - func = funcs.get(type, None) - if func is None: - raise ValueError(f"Invalid navset type: {type!r}") +def navset_card_pill( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + title: Optional[TagChild] = None, + sidebar: Optional[ui.Sidebar] = None, + header: TagChild = None, + footer: TagChild = None, +) -> RecallContextManager[NavSetCard]: + """ + Context manager for a set of nav items as a tabset inside a card container. + This function wraps :func:`~shiny.ui.navset_card_pill`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. + header + UI to display above the selected content. + footer + UI to display below the selected content. + """ return RecallContextManager( - func, + ui.navset_card_pill, kwargs=dict( id=id, selected=selected, @@ -662,6 +785,194 @@ def navset_card( ) +def navset_card_underline( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + title: Optional[TagChild] = None, + sidebar: Optional[ui.Sidebar] = None, + header: TagChild = None, + footer: TagChild = None, + placement: Literal["above", "below"] = "above", +) -> RecallContextManager[NavSetCard]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_card_underline`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. + header + UI to display above the selected content. + footer + UI to display below the selected content. + placement + Placement of the nav items relative to the content. + """ + return RecallContextManager( + ui.navset_card_underline, + kwargs=dict( + id=id, + selected=selected, + title=title, + sidebar=sidebar, + header=header, + footer=footer, + placement=placement, + ), + ) + + +def navset_pill_list( + *, + id: Optional[str] = None, + selected: Optional[str] = None, + header: TagChild = None, + footer: TagChild = None, + well: bool = True, + widths: tuple[int, int] = (4, 8), +) -> RecallContextManager[NavSet]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_pill_list`. + + Parameters + ---------- + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. + header + UI to display above the selected content. + footer + UI to display below the selected content. + placement + Placement of the nav items relative to the content. + """ + return RecallContextManager( + ui.navset_pill_list, + kwargs=dict( + id=id, + selected=selected, + header=header, + footer=footer, + well=well, + widths=widths, + ), + ) + + +def navset_bar( + *, + title: TagChild, + id: Optional[str] = None, + selected: Optional[str] = None, + sidebar: Optional[ui.Sidebar] = None, + fillable: bool | list[str] = True, + gap: Optional[CssUnit] = None, + padding: Optional[CssUnit | list[CssUnit]] = None, + position: Literal[ + "static-top", "fixed-top", "fixed-bottom", "sticky-top" + ] = "static-top", + header: TagChild = None, + footer: TagChild = None, + bg: Optional[str] = None, + # TODO: default to 'auto', like we have in R (parse color via webcolors?) + inverse: bool = False, + underline: bool = True, + collapsible: bool = True, + fluid: bool = True, +) -> RecallContextManager[NavSetBar]: + """ + Context manager for a set of nav items as a tabset inside a card container. + + This function wraps :func:`~shiny.ui.navset_bar`. + + Parameters + ---------- + title + Title to display in the navbar. + id + If provided, will create an input value that holds the currently selected nav + item. + selected + Choose a particular nav item to select by default value (should match it's + ``value``). + sidebar + A :class:`~shiny.ui.Sidebar` component to display on every + :func:`~shiny.ui.nav_panel` page. + fillable + Whether or not to allow fill items to grow/shrink to fit the browser window. If + `True`, all `nav()` pages are fillable. A character vector, matching the value + of `nav()`s to be filled, may also be provided. Note that, if a `sidebar` is + provided, `fillable` makes the main content portion fillable. + gap + A CSS length unit defining the gap (i.e., spacing) between elements provided to + `*args`. + padding + Padding to use for the body. This can be a numeric vector (which will be + interpreted as pixels) or a character vector with valid CSS lengths. The length + can be between one and four. If one, then that value will be used for all four + sides. If two, then the first value will be used for the top and bottom, while + the second value will be used for left and right. If three, then the first will + be used for top, the second will be left and right, and the third will be + bottom. If four, then the values will be interpreted as top, right, bottom, and + left respectively. + position + Determines whether the navbar should be displayed at the top of the page with + normal scrolling behavior ("static-top"), pinned at the top ("fixed-top"), or + pinned at the bottom ("fixed-bottom"). Note that using "fixed-top" or + "fixed-bottom" will cause the navbar to overlay your body content, unless you + add padding (e.g., ``tags.style("body {padding-top: 70px;}")``). + header + UI to display above the selected content. + footer + UI to display below the selected content. + bg + Background color of the navbar (a CSS color). + inverse + Either ``True`` for a light text color or ``False`` for a dark text color. + collapsible + ``True`` to automatically collapse the navigation elements into an expandable + menu on mobile devices or narrow window widths. + fluid + ``True`` to use fluid layout; ``False`` to use fixed layout. + """ + return RecallContextManager( + ui.navset_bar, + kwargs=dict( + title=title, + id=id, + selected=selected, + sidebar=sidebar, + fillable=fillable, + gap=gap, + padding=padding, + position=position, + header=header, + footer=footer, + bg=bg, + inverse=inverse, + underline=underline, + collapsible=collapsible, + fluid=fluid, + ), + ) + + def nav_panel( title: TagChild, *, From b3db4dbc36964fcffb820b21252161590261d015 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 19:21:47 -0600 Subject: [PATCH 16/26] Fix example app --- examples/express/nav_app.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/express/nav_app.py b/examples/express/nav_app.py index 0f968d5c6..0a72627ff 100644 --- a/examples/express/nav_app.py +++ b/examples/express/nav_app.py @@ -4,12 +4,12 @@ from shiny import render from shiny.express import input, ui -with layout.layout_column_wrap(width=1 / 2): - with layout.navset(): - with layout.nav_panel(title="One"): +with ui.layout_column_wrap(width=1 / 2): + with ui.navset_underline(): + with ui.nav_panel(title="One"): ui.input_slider("n", "N", 1, 100, 50) - with layout.nav_panel(title="Two"): + with ui.nav_panel(title="Two"): @render.plot def histogram(): @@ -17,11 +17,11 @@ def histogram(): x = 100 + 15 * np.random.randn(437) plt.hist(x, input.n(), density=True) - with layout.navset_card(): - with layout.nav_panel(title="One"): + with ui.navset_card_underline(): + with ui.nav_panel(title="One"): ui.input_slider("n2", "N", 1, 100, 50) - with layout.nav_panel(title="Two"): + with ui.nav_panel(title="Two"): @render.plot def histogram2(): From b084224b3f1579d2f952e53c71c7241b5a4c6b5a Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 19:21:58 -0600 Subject: [PATCH 17/26] Fix exports --- shiny/express/ui/_cm_components.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index a3cba68f8..962df319e 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -25,8 +25,6 @@ "card", "accordion", "accordion_panel", - "navset", - "navset_card", "nav_panel", "nav_control", "nav_menu", From 675d015a123facaf67952c675a62fb42c307f142 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 19:22:09 -0600 Subject: [PATCH 18/26] Update docstrings --- shiny/express/ui/_cm_components.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index 962df319e..f4dbd002f 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -68,7 +68,7 @@ def sidebar( """ Context manager for sidebar element - Create a collapsing sidebar layout. This function wraps :func:`~shiny.ui.sidebar`. + This function wraps :func:`~shiny.ui.sidebar`. Parameters ---------- @@ -158,6 +158,8 @@ def layout_sidebar( """ Context manager for sidebar layout + This function wraps :func:`~shiny.ui.layout_sidebar`. + Create a sidebar layout component which can be dropped inside any Shiny UI page method (e.g. :func:`~shiny.shiny.ui.page_fillable`) or :func:`~shiny.ui.card` context. @@ -232,9 +234,10 @@ def layout_column_wrap( """ Context manager for a grid-like, column-first layout + This function wraps :func:`~shiny.ui.layout_column_wrap`. + Wraps a 1d sequence of UI elements into a 2d grid. The number of columns (and rows) in the grid dependent on the column `width` as well as the size of the display. - This function wraps :func:`~shiny.ui.layout_column_wrap`. Parameters ---------- @@ -313,6 +316,8 @@ def layout_columns( Context manager for responsive, column-based grid layouts, based on a 12-column grid. + This function wraps :func:`~shiny.ui.layout_columns`. + Parameters ---------- col_widths @@ -416,9 +421,11 @@ def card( """ Context manager for Bootstrap card component - This function wraps :func:`~shiny.ui.card`. A general purpose container for grouping - related UI elements together with a border and optional padding. To learn more about - `card()`s, see [this article](https://rstudio.github.io/bslib/articles/cards.html). + This function wraps :func:`~shiny.ui.card`. + + A general purpose container for grouping related UI elements together with a border + and optional padding. To learn more about `card()`s, see [this + article](https://rstudio.github.io/bslib/articles/cards.html). Parameters ---------- From 03664602f70dc0be8cfaff4010bcc932ed02a286 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 19:22:22 -0600 Subject: [PATCH 19/26] Update test apps --- .../apps/shiny-express-accordion/app.py | 8 +-- .../apps/shiny-express-page-default/app.py | 57 ++++++++----------- .../apps/shiny-express-page-fillable/app.py | 6 +- .../apps/shiny-express-page-fluid/app.py | 6 +- .../apps/shiny-express-page-sidebar/app.py | 8 +-- .../shiny/shiny-express/accordion/app.py | 8 +-- .../shiny/shiny-express/page_default/app.py | 57 ++++++++----------- .../shiny/shiny-express/page_fillable/app.py | 6 +- .../shiny/shiny-express/page_fluid/app.py | 6 +- .../shiny/shiny-express/page_sidebar/app.py | 8 +-- tests/playwright/utils/express_utils.py | 4 +- 11 files changed, 78 insertions(+), 96 deletions(-) diff --git a/tests/playwright/deploys/apps/shiny-express-accordion/app.py b/tests/playwright/deploys/apps/shiny-express-accordion/app.py index 62f95aa23..99d15cf03 100644 --- a/tests/playwright/deploys/apps/shiny-express-accordion/app.py +++ b/tests/playwright/deploys/apps/shiny-express-accordion/app.py @@ -1,11 +1,11 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -with layout.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): - with layout.accordion_panel("Panel 1"): +with ui.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): + with ui.accordion_panel("Panel 1"): ui.input_slider("n", "N", 1, 100, 50) - with layout.accordion_panel("Panel 2"): + with ui.accordion_panel("Panel 2"): @render.text def txt(): diff --git a/tests/playwright/deploys/apps/shiny-express-page-default/app.py b/tests/playwright/deploys/apps/shiny-express-page-default/app.py index 6318139ef..969222336 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-default/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-default/app.py @@ -1,5 +1,5 @@ from shiny import ui -from shiny.express import layout +from shiny.express import ui ui.tags.style( """ @@ -7,41 +7,32 @@ background-color: #00000022} """ ) -with layout.div(id="shell"): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R1" - with layout.row(): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R2-R1C1R1" - with layout.row(): - "R1C1R2-R1C1R2" +with ui.div(id="shell"): + with ui.layout_columns(col_widths=[8, 4]): + "R1C1R1" + with ui.layout_columns(col_widths=[8, 4]): + with ui.div(): + ui.div("R1C1R2-R1C1R1") - with layout.column(width=4): - "R1C1R2-R1C2" + ui.div("R1C1R2-R1C1R2") - with layout.column(width=4): - "R1C2" + "R1C1R2-R1C2" -with layout.column(width=6): + "R1C2" + +with ui.layout_columns(col_widths=[6, 6]): # check height is below 300px - bounding box - with layout.navset_card(id="express_navset_card_tab", type="tab"): - with layout.nav_panel(title="Two"): + with ui.navset_card_tab(id="express_navset_card_tab"): + with ui.nav_panel(title="Two"): ... - -with layout.column(width=6): - with layout.row(): - with layout.navset(id="express_navset_tab", type="tab"): - for fn_txt, fn in [ - ("pre", layout.pre), - ("div", layout.div), - ("span", layout.span), - ]: - with layout.nav_panel(title=fn_txt): - for i in range(3): - with fn(): - ui.HTML(f"{fn_txt} {i}") + with ui.navset_tab(id="express_navset_tab"): + for fn_txt, fn in [ + ("pre", ui.pre), + ("div", ui.div), + ("span", ui.span), + ]: + with ui.nav_panel(title=fn_txt): + for i in range(3): + with fn(): + ui.HTML(f"{fn_txt} {i}") diff --git a/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py b/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py index ee9653a15..9e9a55222 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py @@ -1,9 +1,9 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -layout.set_page(layout.page_fillable()) +ui.set_page(ui.page_fillable()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py b/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py index 4d664501b..cff986ebf 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py @@ -1,9 +1,9 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -layout.set_page(layout.page_fluid()) +ui.set_page(ui.page_fluid()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py index a20d6dd36..23e3fe746 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py @@ -1,12 +1,12 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -layout.set_page(layout.page_sidebar(title="PageTitle")) +ui.set_page(ui.page_sidebar(title="PageTitle")) -with layout.sidebar(id="sidebar", title="SidebarTitle"): +with ui.sidebar(id="sidebar", title="SidebarTitle"): "Sidebar Content" -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/shiny/shiny-express/accordion/app.py b/tests/playwright/shiny/shiny-express/accordion/app.py index 62f95aa23..99d15cf03 100644 --- a/tests/playwright/shiny/shiny-express/accordion/app.py +++ b/tests/playwright/shiny/shiny-express/accordion/app.py @@ -1,11 +1,11 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -with layout.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): - with layout.accordion_panel("Panel 1"): +with ui.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): + with ui.accordion_panel("Panel 1"): ui.input_slider("n", "N", 1, 100, 50) - with layout.accordion_panel("Panel 2"): + with ui.accordion_panel("Panel 2"): @render.text def txt(): diff --git a/tests/playwright/shiny/shiny-express/page_default/app.py b/tests/playwright/shiny/shiny-express/page_default/app.py index 6318139ef..969222336 100644 --- a/tests/playwright/shiny/shiny-express/page_default/app.py +++ b/tests/playwright/shiny/shiny-express/page_default/app.py @@ -1,5 +1,5 @@ from shiny import ui -from shiny.express import layout +from shiny.express import ui ui.tags.style( """ @@ -7,41 +7,32 @@ background-color: #00000022} """ ) -with layout.div(id="shell"): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R1" - with layout.row(): - with layout.row(): - with layout.column(width=8): - with layout.row(): - "R1C1R2-R1C1R1" - with layout.row(): - "R1C1R2-R1C1R2" +with ui.div(id="shell"): + with ui.layout_columns(col_widths=[8, 4]): + "R1C1R1" + with ui.layout_columns(col_widths=[8, 4]): + with ui.div(): + ui.div("R1C1R2-R1C1R1") - with layout.column(width=4): - "R1C1R2-R1C2" + ui.div("R1C1R2-R1C1R2") - with layout.column(width=4): - "R1C2" + "R1C1R2-R1C2" -with layout.column(width=6): + "R1C2" + +with ui.layout_columns(col_widths=[6, 6]): # check height is below 300px - bounding box - with layout.navset_card(id="express_navset_card_tab", type="tab"): - with layout.nav_panel(title="Two"): + with ui.navset_card_tab(id="express_navset_card_tab"): + with ui.nav_panel(title="Two"): ... - -with layout.column(width=6): - with layout.row(): - with layout.navset(id="express_navset_tab", type="tab"): - for fn_txt, fn in [ - ("pre", layout.pre), - ("div", layout.div), - ("span", layout.span), - ]: - with layout.nav_panel(title=fn_txt): - for i in range(3): - with fn(): - ui.HTML(f"{fn_txt} {i}") + with ui.navset_tab(id="express_navset_tab"): + for fn_txt, fn in [ + ("pre", ui.pre), + ("div", ui.div), + ("span", ui.span), + ]: + with ui.nav_panel(title=fn_txt): + for i in range(3): + with fn(): + ui.HTML(f"{fn_txt} {i}") diff --git a/tests/playwright/shiny/shiny-express/page_fillable/app.py b/tests/playwright/shiny/shiny-express/page_fillable/app.py index ee9653a15..9e9a55222 100644 --- a/tests/playwright/shiny/shiny-express/page_fillable/app.py +++ b/tests/playwright/shiny/shiny-express/page_fillable/app.py @@ -1,9 +1,9 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -layout.set_page(layout.page_fillable()) +ui.set_page(ui.page_fillable()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/shiny/shiny-express/page_fluid/app.py b/tests/playwright/shiny/shiny-express/page_fluid/app.py index 4d664501b..cff986ebf 100644 --- a/tests/playwright/shiny/shiny-express/page_fluid/app.py +++ b/tests/playwright/shiny/shiny-express/page_fluid/app.py @@ -1,9 +1,9 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -layout.set_page(layout.page_fluid()) +ui.set_page(ui.page_fluid()) -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/shiny/shiny-express/page_sidebar/app.py b/tests/playwright/shiny/shiny-express/page_sidebar/app.py index a20d6dd36..23e3fe746 100644 --- a/tests/playwright/shiny/shiny-express/page_sidebar/app.py +++ b/tests/playwright/shiny/shiny-express/page_sidebar/app.py @@ -1,12 +1,12 @@ from shiny import render, ui -from shiny.express import input, layout +from shiny.express import input, ui -layout.set_page(layout.page_sidebar(title="PageTitle")) +ui.set_page(ui.page_sidebar(title="PageTitle")) -with layout.sidebar(id="sidebar", title="SidebarTitle"): +with ui.sidebar(id="sidebar", title="SidebarTitle"): "Sidebar Content" -with layout.card(id="card"): +with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) @render.text diff --git a/tests/playwright/utils/express_utils.py b/tests/playwright/utils/express_utils.py index 961500efa..5c039b6b5 100644 --- a/tests/playwright/utils/express_utils.py +++ b/tests/playwright/utils/express_utils.py @@ -6,7 +6,7 @@ from playwright.sync_api import Page from shiny import ui -from shiny.express import layout +from shiny.express import ui as xui def verify_express_accordion(page: Page) -> None: @@ -82,4 +82,4 @@ def verify_express_page_sidebar(page: Page) -> None: sidebar.expect_text("SidebarTitle Sidebar Content") output_txt = OutputTextVerbatim(page, "txt") output_txt.expect_value("50") - compare_annotations(ui.sidebar, layout.sidebar) + compare_annotations(ui.sidebar, xui.sidebar) From 8df8d943061a74d702e9a7196b8b45e20af26fbc Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 19:29:43 -0600 Subject: [PATCH 20/26] Import fixes in test apps --- tests/playwright/deploys/apps/shiny-express-accordion/app.py | 2 +- tests/playwright/deploys/apps/shiny-express-page-default/app.py | 1 - .../playwright/deploys/apps/shiny-express-page-fillable/app.py | 2 +- tests/playwright/deploys/apps/shiny-express-page-fluid/app.py | 2 +- tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py | 2 +- tests/playwright/shiny/shiny-express/accordion/app.py | 2 +- tests/playwright/shiny/shiny-express/page_default/app.py | 1 - tests/playwright/shiny/shiny-express/page_fillable/app.py | 2 +- tests/playwright/shiny/shiny-express/page_fluid/app.py | 2 +- tests/playwright/shiny/shiny-express/page_sidebar/app.py | 2 +- 10 files changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/playwright/deploys/apps/shiny-express-accordion/app.py b/tests/playwright/deploys/apps/shiny-express-accordion/app.py index 99d15cf03..60d79fc2c 100644 --- a/tests/playwright/deploys/apps/shiny-express-accordion/app.py +++ b/tests/playwright/deploys/apps/shiny-express-accordion/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui with ui.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): diff --git a/tests/playwright/deploys/apps/shiny-express-page-default/app.py b/tests/playwright/deploys/apps/shiny-express-page-default/app.py index 969222336..de1577c1b 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-default/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-default/app.py @@ -1,4 +1,3 @@ -from shiny import ui from shiny.express import ui ui.tags.style( diff --git a/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py b/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py index 9e9a55222..9c2013b04 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-fillable/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui ui.set_page(ui.page_fillable()) diff --git a/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py b/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py index cff986ebf..0422113b8 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-fluid/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui ui.set_page(ui.page_fluid()) diff --git a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py index 23e3fe746..fac04a612 100644 --- a/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py +++ b/tests/playwright/deploys/apps/shiny-express-page-sidebar/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui ui.set_page(ui.page_sidebar(title="PageTitle")) diff --git a/tests/playwright/shiny/shiny-express/accordion/app.py b/tests/playwright/shiny/shiny-express/accordion/app.py index 99d15cf03..60d79fc2c 100644 --- a/tests/playwright/shiny/shiny-express/accordion/app.py +++ b/tests/playwright/shiny/shiny-express/accordion/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui with ui.accordion(id="express_accordion", open=["Panel 1", "Panel 2"]): diff --git a/tests/playwright/shiny/shiny-express/page_default/app.py b/tests/playwright/shiny/shiny-express/page_default/app.py index 969222336..de1577c1b 100644 --- a/tests/playwright/shiny/shiny-express/page_default/app.py +++ b/tests/playwright/shiny/shiny-express/page_default/app.py @@ -1,4 +1,3 @@ -from shiny import ui from shiny.express import ui ui.tags.style( diff --git a/tests/playwright/shiny/shiny-express/page_fillable/app.py b/tests/playwright/shiny/shiny-express/page_fillable/app.py index 9e9a55222..9c2013b04 100644 --- a/tests/playwright/shiny/shiny-express/page_fillable/app.py +++ b/tests/playwright/shiny/shiny-express/page_fillable/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui ui.set_page(ui.page_fillable()) diff --git a/tests/playwright/shiny/shiny-express/page_fluid/app.py b/tests/playwright/shiny/shiny-express/page_fluid/app.py index cff986ebf..0422113b8 100644 --- a/tests/playwright/shiny/shiny-express/page_fluid/app.py +++ b/tests/playwright/shiny/shiny-express/page_fluid/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui ui.set_page(ui.page_fluid()) diff --git a/tests/playwright/shiny/shiny-express/page_sidebar/app.py b/tests/playwright/shiny/shiny-express/page_sidebar/app.py index 23e3fe746..fac04a612 100644 --- a/tests/playwright/shiny/shiny-express/page_sidebar/app.py +++ b/tests/playwright/shiny/shiny-express/page_sidebar/app.py @@ -1,4 +1,4 @@ -from shiny import render, ui +from shiny import render from shiny.express import input, ui ui.set_page(ui.page_sidebar(title="PageTitle")) From 2f801f7e0071f47e8be4ae308055d44036443177 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 19:48:33 -0600 Subject: [PATCH 21/26] Documentation updates --- docs/_quartodoc.yml | 57 +++++++++++++++++++----------- shiny/express/ui/_cm_components.py | 22 +++--------- shiny/ui/_navs.py | 2 +- 3 files changed, 43 insertions(+), 38 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 96cf24eea..86b0c6738 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -283,26 +283,43 @@ quartodoc: - title: Shiny Express desc: Functions for Shiny Express applications contents: - - express.layout.set_page - - express.layout.p - - express.layout.div - - express.layout.span - - express.layout.pre - - express.layout.sidebar - - express.layout.layout_columns - - express.layout.layout_column_wrap - - express.layout.column - - express.layout.row - - express.layout.card - - express.layout.accordion - - express.layout.accordion_panel - - express.layout.navset - - express.layout.navset_card - - express.layout.nav_panel - - express.layout.page_fluid - - express.layout.page_fixed - - express.layout.page_fillable - - express.layout.page_sidebar + - kind: page + path: ContextManagerComponents + summary: + name: "Context manager components" + desc: "" + flatten: true + contents: + - express.ui.set_page + - express.ui.sidebar + - express.ui.layout_sidebar + - express.ui.layout_column_wrap + - express.ui.layout_columns + - express.ui.card + - express.ui.accordion + - express.ui.accordion_panel + - express.ui.nav_panel + - express.ui.nav_control + - express.ui.nav_menu + - express.ui.navset_bar + - express.ui.navset_card_pill + - express.ui.navset_card_tab + - express.ui.navset_card_underline + - express.ui.navset_hidden + - express.ui.navset_pill + - express.ui.navset_pill_list + - express.ui.navset_tab + - express.ui.navset_underline + - express.ui.value_box + - express.ui.panel_well + - express.ui.panel_conditional + - express.ui.panel_fixed + - express.ui.panel_absolute + - express.ui.page_fluid + - express.ui.page_fixed + - express.ui.page_fillable + - express.ui.page_sidebar + - express.ui.page_navbar - title: Deprecated desc: "" contents: diff --git a/shiny/express/ui/_cm_components.py b/shiny/express/ui/_cm_components.py index f4dbd002f..269fbe6e1 100644 --- a/shiny/express/ui/_cm_components.py +++ b/shiny/express/ui/_cm_components.py @@ -856,16 +856,16 @@ def navset_pill_list( If provided, will create an input value that holds the currently selected nav item. selected - Choose a particular nav item to select by default value (should match it's + Choose a particular nav item to select by default value (should match its ``value``). - sidebar - A :class:`shiny.ui.Sidebar` component to display on every :func:`~shiny.ui.nav` page. header UI to display above the selected content. footer UI to display below the selected content. - placement - Placement of the nav items relative to the content. + well + ``True`` to place a well (gray rounded rectangle) around the navigation list. + widths + Column widths of the navigation list and tabset content areas respectively. """ return RecallContextManager( ui.navset_pill_list, @@ -1017,11 +1017,6 @@ def nav_control() -> RecallContextManager[NavPanel]: Context manager for a control in the navigation container. This function wraps :func:`~shiny.ui.nav_control`. - - Parameters - ---------- - *args - UI elements to display as the nav item. """ return RecallContextManager(ui.nav_control) @@ -1042,11 +1037,6 @@ def nav_menu( ---------- title A title to display. Can be a character string or UI elements (i.e., tags). - *args - A collection of nav items (e.g., :func:`~shiny.ui.nav`) and/or strings. - Strings will be rendered as a section header unless the string is a set - of two or more hyphens (e.g., ``---``), in which case it will be rendered - as a divider. value The value of the item. This is used to determine whether the item is active (when an ``id`` is provided to the nav container), programmatically select the @@ -1563,8 +1553,6 @@ def page_navbar( selected Choose a particular nav item to select by default value (should match it's ``value``). - sidebar - A :func:`~shiny.ui.sidebar` component to display on every page. fillable Whether or not the main content area should be considered a fillable (i.e., flexbox) container. diff --git a/shiny/ui/_navs.py b/shiny/ui/_navs.py index 8a883d164..44e686b87 100644 --- a/shiny/ui/_navs.py +++ b/shiny/ui/_navs.py @@ -923,7 +923,7 @@ def navset_pill_list( If provided, will create an input value that holds the currently selected nav item. selected - Choose a particular nav item to select by default value (should match it's + Choose a particular nav item to select by default value (should match its ``value``). header UI to display above the selected content. From a7b666419af31d0a6e6010ae4a05bfd16a16efbf Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 21:37:25 -0600 Subject: [PATCH 22/26] Update known missing items --- shiny/express/ui/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index eba8cdb77..30ecd9c3f 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -277,6 +277,11 @@ "navset_pill_card", # Deprecated "navset_tab_card", # Deprecated "page_bootstrap", + "page_fixed", + "page_sidebar", + "page_fillable", + "page_navbar", + "page_fluid", "page_output", "panel_main", # Deprecated "panel_sidebar", # Deprecated @@ -288,5 +293,5 @@ "tooltip", ), # Items from shiny.express.ui that don't have a counterpart in shiny.ui - "shiny.express.ui": ("set_page",), + "shiny.express.ui": (), } From 31c8130ce7ba5f884d743d051e1cfa65f4201ef0 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Wed, 20 Dec 2023 21:38:01 -0600 Subject: [PATCH 23/26] Update quartodoc entries --- docs/_quartodoc.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 86b0c6738..788fd0c33 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -290,7 +290,6 @@ quartodoc: desc: "" flatten: true contents: - - express.ui.set_page - express.ui.sidebar - express.ui.layout_sidebar - express.ui.layout_column_wrap @@ -315,11 +314,6 @@ quartodoc: - express.ui.panel_conditional - express.ui.panel_fixed - express.ui.panel_absolute - - express.ui.page_fluid - - express.ui.page_fixed - - express.ui.page_fillable - - express.ui.page_sidebar - - express.ui.page_navbar - title: Deprecated desc: "" contents: From 06b31e05096741aa360b447ab32e314b3f095b84 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Dec 2023 00:04:07 -0600 Subject: [PATCH 24/26] API updates --- docs/_quartodoc.yml | 11 ++ examples/express/plot_app.py | 4 +- shiny/express/_page.py | 334 -------------------------------- shiny/express/_run.py | 2 +- shiny/express/ui/__init__.py | 9 +- shiny/express/ui/_page.py | 38 ++++ shiny/ui/__init__.py | 4 +- shiny/ui/_page.py | 98 +++++++++- tests/pytest/test_express_ui.py | 3 +- 9 files changed, 161 insertions(+), 342 deletions(-) delete mode 100644 shiny/express/_page.py create mode 100644 shiny/express/ui/_page.py diff --git a/docs/_quartodoc.yml b/docs/_quartodoc.yml index 788fd0c33..7d1ec56ec 100644 --- a/docs/_quartodoc.yml +++ b/docs/_quartodoc.yml @@ -19,6 +19,8 @@ quartodoc: - ui.page_fluid - ui.page_fixed - ui.page_bootstrap + - ui.page_auto + - ui.page_output - title: UI Layouts desc: Control the layout of multiple UI components. contents: @@ -314,6 +316,15 @@ quartodoc: - express.ui.panel_conditional - express.ui.panel_fixed - express.ui.panel_absolute + contents: + - kind: page + path: PageFunctions + summary: + name: "Page functions" + desc: "" + flatten: true + contents: + - express.ui.page_opts - title: Deprecated desc: "" contents: diff --git a/examples/express/plot_app.py b/examples/express/plot_app.py index 05d8643f4..605fe6e62 100644 --- a/examples/express/plot_app.py +++ b/examples/express/plot_app.py @@ -1,8 +1,8 @@ import matplotlib.pyplot as plt import numpy as np -from shiny import render, ui -from shiny.express import input +from shiny import render +from shiny.express import input, ui ui.input_slider("n", "N", 1, 100, 50) diff --git a/shiny/express/_page.py b/shiny/express/_page.py deleted file mode 100644 index e34a75264..000000000 --- a/shiny/express/_page.py +++ /dev/null @@ -1,334 +0,0 @@ -from __future__ import annotations - -from typing import Callable, Literal, Optional, cast - -from htmltools import Tag, TagAttrValue, TagChild, TagList - -from .. import ui -from ..types import MISSING, MISSING_TYPE -from ..ui._navs import NavMenu, NavPanel -from ..ui._sidebar import Sidebar -from ..ui.css import CssUnit -from ._recall_context import RecallContextManager -from ._run import get_top_level_recall_context_manager - -__all__ = ( - "page_auto", - "page_auto_cm", - "set_page_title", - "set_page_lang", - "set_page_fillable", - "set_page_wider", - "use_page_fixed", - "use_page_fluid", - "use_page_fillable", - "use_page_sidebar", - "use_page_navbar", -) - - -def page_auto_cm() -> RecallContextManager[Tag]: - return RecallContextManager( - page_auto, - kwargs={ - "_page_fn": None, - "_fillable": False, - "_wider": False, - }, - ) - - -def page_auto( - *args: object, - _page_fn: Callable[[object], Tag] | None, - _fillable: bool, - _wider: bool, - **kwargs: object, -) -> Tag: - # Presence of a top-level nav items and/or sidebar determines the page function - navs = [x for x in args if isinstance(x, (NavPanel, NavMenu))] - sidebars = [x for x in args if isinstance(x, ui.Sidebar)] - - nNavs = len(navs) - nSidebars = len(sidebars) - - if _page_fn is None: - if nNavs == 0: - if nSidebars == 0: - if _fillable: - _page_fn = ( - ui.page_fillable - ) # pyright: ignore[reportGeneralTypeIssues] - elif _wider: - _page_fn = ui.page_fluid # pyright: ignore[reportGeneralTypeIssues] - else: - _page_fn = ui.page_fixed # pyright: ignore[reportGeneralTypeIssues] - - elif nSidebars == 1: - # page_sidebar() needs sidebar to be the first arg - # TODO: Change page_sidebar() to remove `sidebar` and accept a sidebar as a - # *arg. - _page_fn = ui.page_sidebar # pyright: ignore[reportGeneralTypeIssues] - args = tuple(sidebars + [x for x in args if x not in sidebars]) - - else: - raise NotImplementedError( - "Multiple top-level sidebars not allowed. Did you meant to wrap each one in layout_sidebar()?" - ) - - # At least one nav - else: - if nSidebars == 0: - # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? - _page_fn = ui.page_navbar # pyright: ignore[reportGeneralTypeIssues] - - elif nSidebars == 1: - # TODO: change page_navbar() to remove `sidebar` and accept a sidebar as a - # *arg. - _page_fn = ui.page_navbar # pyright: ignore[reportGeneralTypeIssues] - kwargs["sidebar"] = sidebars[0] - - else: - raise NotImplementedError( - "Multiple top-level sidebars not allowed in combination with top-level navs." - ) - - # If we got here, _page_fn is not None, but the type checker needs a little help. - _page_fn = cast(Callable[[object], Tag], _page_fn) - return _page_fn(*args, **kwargs) - - -# ====================================================================================== -# Page attribute setters -# ====================================================================================== - - -def set_page_title(title: str) -> None: - get_top_level_recall_context_manager().kwargs["title"] = title - - -def set_page_fillable(fillable: bool) -> None: - get_top_level_recall_context_manager().kwargs["_fillable"] = fillable - - -def set_page_wider(wider: bool) -> None: - get_top_level_recall_context_manager().kwargs["_wider"] = wider - - -def set_page_lang(lang: str) -> None: - get_top_level_recall_context_manager().kwargs["lang"] = lang - - -# ====================================================================================== -# Page functions -# ====================================================================================== - - -def use_page_fixed( - *, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: str, -) -> None: - """ - Create a fixed page. - - This function wraps :func:`~shiny.ui.page_fixed`. - - Parameters - ---------- - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Attributes on the page level container. - """ - get_top_level_recall_context_manager().kwargs.update( - dict( - _page_fn=ui.page_fixed, - title=title, - lang=lang, - **kwargs, - ) - ) - - -def use_page_fluid( - *, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: str, -) -> None: - """ - Create a fluid page. - - This function wraps :func:`~shiny.ui.page_fluid`. - - Parameters - ---------- - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Attributes on the page level container. - """ - get_top_level_recall_context_manager().kwargs.update( - dict( - _page_fn=ui.page_fluid, - title=title, - lang=lang, - **kwargs, - ) - ) - - -def use_page_fillable( - *, - padding: Optional[CssUnit | list[CssUnit]] = None, - gap: Optional[CssUnit] = None, - fillable_mobile: bool = False, - title: Optional[str] = None, - lang: Optional[str] = None, - **kwargs: TagAttrValue, -) -> None: - """ - Use a fillable page. - - This function wraps :func:`~shiny.ui.page_fillable`. - - Parameters - ---------- - padding - Padding to use for the body. See :func:`~shiny.ui.css_unit.as_css_padding` - for more details. - fillable_mobile - Whether or not the page should fill the viewport's height on mobile devices - (i.e., narrow windows). - gap - A CSS length unit passed through :func:`~shiny.ui.css_unit.as_css_unit` - defining the `gap` (i.e., spacing) between elements provided to `*args`. - title - The browser window title (defaults to the host URL of the page). Can also be set - as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - """ - get_top_level_recall_context_manager().kwargs.update( - dict( - _page_fn=ui.page_fillable, - padding=padding, - gap=gap, - fillable_mobile=fillable_mobile, - title=title, - lang=lang, - **kwargs, - ) - ) - - -def use_page_sidebar( - *, - title: Optional[str | Tag | TagList] = None, - fillable: bool = True, - fillable_mobile: bool = False, - window_title: str | MISSING_TYPE = MISSING, - lang: Optional[str] = None, - **kwargs: TagAttrValue, -) -> None: - """ - Create a page with a sidebar and a title. - This function wraps :func:`~shiny.ui.page_sidebar`. - Parameters - ---------- - sidebar - Content to display in the sidebar. - title - A title to display at the top of the page. - fillable - Whether or not the main content area should be considered a fillable - (i.e., flexbox) container. - fillable_mobile - Whether or not ``fillable`` should apply on mobile devices. - window_title - The browser's window title (defaults to the host URL of the page). Can also be - set as a side effect via :func:`~shiny.ui.panel_title`. - lang - ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This - will be used as the lang in the ```` tag, as in ````. The - default, `None`, results in an empty string. - **kwargs - Additional attributes passed to :func:`~shiny.ui.layout_sidebar`. - Returns - ------- - : - A UI element. - """ - get_top_level_recall_context_manager().kwargs.update( - dict( - _page_fn=ui.page_sidebar, - title=title, - fillable=fillable, - fillable_mobile=fillable_mobile, - window_title=window_title, - lang=lang, - **kwargs, - ) - ) - - -def use_page_navbar( - *, - title: Optional[str | Tag | TagList] = None, - id: Optional[str] = None, - selected: Optional[str] = None, - sidebar: Optional[Sidebar] = None, - # Only page_navbar gets enhanced treatement for `fillable` - # If an `*args`'s `data-value` attr string is in `fillable`, then the component is fillable - fillable: bool | list[str] = True, - fillable_mobile: bool = False, - gap: Optional[CssUnit] = None, - padding: Optional[CssUnit | list[CssUnit]] = None, - position: Literal["static-top", "fixed-top", "fixed-bottom"] = "static-top", - header: Optional[TagChild] = None, - footer: Optional[TagChild] = None, - bg: Optional[str] = None, - inverse: bool = False, - underline: bool = True, - collapsible: bool = True, - fluid: bool = True, - window_title: str | MISSING_TYPE = MISSING, - lang: Optional[str] = None, -) -> None: - get_top_level_recall_context_manager().kwargs.update( - dict( - _page_fn=ui.page_navbar, - title=title, - id=id, - selected=selected, - sidebar=sidebar, - fillable=fillable, - fillable_mobile=fillable_mobile, - gap=gap, - padding=padding, - position=position, - header=header, - footer=footer, - bg=bg, - inverse=inverse, - underline=underline, - collapsible=collapsible, - fluid=fluid, - window_title=window_title, - lang=lang, - ) - ) diff --git a/shiny/express/_run.py b/shiny/express/_run.py index a121a0446..34c934d6e 100644 --- a/shiny/express/_run.py +++ b/shiny/express/_run.py @@ -133,7 +133,7 @@ def set_result(x: object): def reset_top_level_recall_context_manager() -> None: - from ._page import page_auto_cm + from .ui._page import page_auto_cm global _top_level_recall_context_manager _top_level_recall_context_manager = page_auto_cm() diff --git a/shiny/express/ui/__init__.py b/shiny/express/ui/__init__.py index 30ecd9c3f..b36336fc4 100644 --- a/shiny/express/ui/__init__.py +++ b/shiny/express/ui/__init__.py @@ -135,6 +135,10 @@ panel_absolute, ) +from ._page import ( + page_opts, +) + __all__ = ( # Imports from htmltools "TagList", @@ -265,6 +269,8 @@ "panel_conditional", "panel_fixed", "panel_absolute", + # Imports from ._page + "page_opts", ) @@ -282,6 +288,7 @@ "page_fillable", "page_navbar", "page_fluid", + "page_auto", "page_output", "panel_main", # Deprecated "panel_sidebar", # Deprecated @@ -293,5 +300,5 @@ "tooltip", ), # Items from shiny.express.ui that don't have a counterpart in shiny.ui - "shiny.express.ui": (), + "shiny.express.ui": ("page_opts",), } diff --git a/shiny/express/ui/_page.py b/shiny/express/ui/_page.py new file mode 100644 index 000000000..36451e56c --- /dev/null +++ b/shiny/express/ui/_page.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +from typing import Callable + +from htmltools import Tag + +from ... import ui +from ...types import MISSING, MISSING_TYPE +from .._recall_context import RecallContextManager +from .._run import get_top_level_recall_context_manager + +__all__ = ("page_opts",) + + +def page_auto_cm() -> RecallContextManager[Tag]: + return RecallContextManager(ui.page_auto) + + +def page_opts( + *, + title: str | MISSING_TYPE = MISSING, + lang: str | MISSING_TYPE = MISSING, + page_fn: Callable[..., Tag] | None | MISSING_TYPE = MISSING, + fillable: bool | MISSING_TYPE = MISSING, + full_width: bool | MISSING_TYPE = MISSING, +) -> None: + cm = get_top_level_recall_context_manager() + + if not isinstance(title, MISSING_TYPE): + cm.kwargs["title"] = title + if not isinstance(lang, MISSING_TYPE): + cm.kwargs["lang"] = lang + if not isinstance(page_fn, MISSING_TYPE): + cm.kwargs["_page_fn"] = page_fn + if not isinstance(fillable, MISSING_TYPE): + cm.kwargs["_fillable"] = fillable + if not isinstance(full_width, MISSING_TYPE): + cm.kwargs["_full_width"] = full_width diff --git a/shiny/ui/__init__.py b/shiny/ui/__init__.py index 39a9f4832..49a725aab 100644 --- a/shiny/ui/__init__.py +++ b/shiny/ui/__init__.py @@ -121,6 +121,7 @@ page_fluid, page_fixed, page_bootstrap, + page_auto, page_output, ) from ._progress import Progress @@ -298,7 +299,6 @@ "output_text_verbatim", "output_table", "output_ui", - "page_output", # _page "page_sidebar", "page_navbar", @@ -306,6 +306,8 @@ "page_fluid", "page_fixed", "page_bootstrap", + "page_auto", + "page_output", # _popover "popover", # _valuebox diff --git a/shiny/ui/_page.py b/shiny/ui/_page.py index 207bd1ab0..3d2fe6a3b 100644 --- a/shiny/ui/_page.py +++ b/shiny/ui/_page.py @@ -7,10 +7,11 @@ "page_fluid", "page_fixed", "page_bootstrap", + "page_auto", "page_output", ) -from typing import Literal, Optional, Sequence +from typing import Callable, Literal, Optional, Sequence, cast from htmltools import ( MetadataNode, @@ -31,7 +32,7 @@ from ._html_deps_external import bootstrap_deps from ._html_deps_py_shiny import page_output_dependency from ._html_deps_shinyverse import components_dependency -from ._navs import navset_bar +from ._navs import NavMenu, NavPanel, navset_bar from ._sidebar import Sidebar, layout_sidebar from ._tag import consolidate_attrs from ._utils import get_window_title @@ -443,6 +444,99 @@ def page_bootstrap( ) +def page_auto( + *args: TagChild | TagAttrs, + _page_fn: Callable[..., Tag] | None = None, + _fillable: bool = False, + _full_width: bool = False, + **kwargs: object, +) -> Tag: + """ + A page container which automatically decides which page function to use. + + If there is a top-level nav, this will use :func:`~shiny.ui.page_navbar`. If not, + and there is a top-level sidebar, this will use :func:`~shiny.ui.page_sidebar`. + + If there are neither top-level navs nor sidebars, this will use the ``_fillable`` + and ``_full_width`` arguments to determine which page function to use. + + Parameters + ---------- + *args + UI elements. These are used to determine which page function to use, and they + are also passed along to that page function. + _page_fn + The page function to use. If ``None`` (the default), will automatically choose + one based on the arguments provided. + _fillable + This has an effect only if there are no sidebars or top-level navs. If ``True``, + use :func:`~shiny.ui.page_fillable`, where the content fills the window. If + ``False`` (the default), the value of ``_full_width`` will determine which page + function is used. + _full_width + This has an effect only if there are no sidebars or top-level navs, and + ``_fillable`` is ``False``. If this is ``False`` (the default), use use + :func:`~shiny.ui.page_fixed`; if ``True``, use :func:`~shiny.ui.page_fillable`. + **kwargs + Additional arguments, which are passed to the page function. + + Returns + ------- + : + A UI element. + """ + + # Presence of a top-level nav items and/or sidebar determines the page function + navs = [x for x in args if isinstance(x, (NavPanel, NavMenu))] + sidebars = [x for x in args if isinstance(x, Sidebar)] + + nNavs = len(navs) + nSidebars = len(sidebars) + + if _page_fn is None: + if nNavs == 0: + if nSidebars == 0: + if _fillable: + _page_fn = page_fillable # pyright: ignore[reportGeneralTypeIssues] + elif _full_width: + _page_fn = page_fluid # pyright: ignore[reportGeneralTypeIssues] + else: + _page_fn = page_fixed # pyright: ignore[reportGeneralTypeIssues] + + elif nSidebars == 1: + # page_sidebar() needs sidebar to be the first arg + # TODO: Change page_sidebar() to remove `sidebar` and accept a sidebar as a + # *arg. + _page_fn = page_sidebar # pyright: ignore[reportGeneralTypeIssues] + args = tuple(sidebars + [x for x in args if x not in sidebars]) + + else: + raise NotImplementedError( + "Multiple top-level sidebars not allowed. Did you meant to wrap each one in layout_sidebar()?" + ) + + # At least one nav + else: + if nSidebars == 0: + # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? + _page_fn = page_navbar # pyright: ignore[reportGeneralTypeIssues] + + elif nSidebars == 1: + # TODO: change page_navbar() to remove `sidebar` and accept a sidebar as a + # *arg. + _page_fn = page_navbar # pyright: ignore[reportGeneralTypeIssues] + kwargs["sidebar"] = sidebars[0] + + else: + raise NotImplementedError( + "Multiple top-level sidebars not allowed in combination with top-level navs." + ) + + # If we got here, _page_fn is not None, but the type checker needs a little help. + _page_fn = cast(Callable[..., Tag], _page_fn) + return _page_fn(*args, **kwargs) + + def page_output(id: str) -> Tag: """ Create a page container where the entire body is a UI output. diff --git a/tests/pytest/test_express_ui.py b/tests/pytest/test_express_ui.py index ee99effdc..eee54b246 100644 --- a/tests/pytest/test_express_ui.py +++ b/tests/pytest/test_express_ui.py @@ -10,7 +10,8 @@ def test_express_ui_is_complete(): """ Make sure shiny.express.ui covers everything that shiny.ui does, or explicitly lists - the item in _known_missing. + the item in `_known_missing`. + These entries are in `_known_missing` in shiny/express/ui/__init__.py """ from shiny import ui From c03f684d752c83f37b3dd4ec1becc35c5f277408 Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Dec 2023 17:44:53 -0600 Subject: [PATCH 25/26] Update page_auto --- shiny/express/ui/_page.py | 30 ++++++++++++++-- shiny/ui/_page.py | 76 +++++++++++++++++++++++++-------------- 2 files changed, 76 insertions(+), 30 deletions(-) diff --git a/shiny/express/ui/_page.py b/shiny/express/ui/_page.py index 36451e56c..f06d549f4 100644 --- a/shiny/express/ui/_page.py +++ b/shiny/express/ui/_page.py @@ -24,6 +24,30 @@ def page_opts( fillable: bool | MISSING_TYPE = MISSING, full_width: bool | MISSING_TYPE = MISSING, ) -> None: + """ + Set page-level options for the current app. + + title + The browser window title (defaults to the host URL of the page). + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + fillable + If there is a top-level sidebar or nav, then the value is passed through to the + :func:`~shiny.ui.page_sidebar` or :func:`~shiny.ui.page_navbar` function. + Otherwise, if ``True``, use :func:`~shiny.ui.page_fillable`, where the content + fills the window; if ``False`` (the default), the value of ``full_width`` will + determine which page function is used. + full_width + This has an effect only if there are no sidebars or top-level navs, and + ``fillable`` is ``False``. If this is ``False`` (the default), use use + :func:`~shiny.ui.page_fixed`; if ``True``, use :func:`~shiny.ui.page_fillable`. + page_fn + The page function to use. If ``None`` (the default), will automatically choose + one based on the arguments provided. If not ``None``, this will override all + heuristics for choosing page functions. + """ cm = get_top_level_recall_context_manager() if not isinstance(title, MISSING_TYPE): @@ -31,8 +55,8 @@ def page_opts( if not isinstance(lang, MISSING_TYPE): cm.kwargs["lang"] = lang if not isinstance(page_fn, MISSING_TYPE): - cm.kwargs["_page_fn"] = page_fn + cm.kwargs["page_fn"] = page_fn if not isinstance(fillable, MISSING_TYPE): - cm.kwargs["_fillable"] = fillable + cm.kwargs["fillable"] = fillable if not isinstance(full_width, MISSING_TYPE): - cm.kwargs["_full_width"] = full_width + cm.kwargs["full_width"] = full_width diff --git a/shiny/ui/_page.py b/shiny/ui/_page.py index 3d2fe6a3b..c7cd08980 100644 --- a/shiny/ui/_page.py +++ b/shiny/ui/_page.py @@ -446,9 +446,11 @@ def page_bootstrap( def page_auto( *args: TagChild | TagAttrs, - _page_fn: Callable[..., Tag] | None = None, - _fillable: bool = False, - _full_width: bool = False, + title: str | MISSING_TYPE = MISSING, + lang: str | MISSING_TYPE = MISSING, + fillable: bool | MISSING_TYPE = MISSING, + full_width: bool = False, + page_fn: Callable[..., Tag] | None = None, **kwargs: object, ) -> Tag: """ @@ -457,26 +459,34 @@ def page_auto( If there is a top-level nav, this will use :func:`~shiny.ui.page_navbar`. If not, and there is a top-level sidebar, this will use :func:`~shiny.ui.page_sidebar`. - If there are neither top-level navs nor sidebars, this will use the ``_fillable`` - and ``_full_width`` arguments to determine which page function to use. + If there are neither top-level navs nor sidebars, this will use the ``fillable`` and + ``full_width`` arguments to determine which page function to use. Parameters ---------- *args UI elements. These are used to determine which page function to use, and they are also passed along to that page function. - _page_fn - The page function to use. If ``None`` (the default), will automatically choose - one based on the arguments provided. - _fillable - This has an effect only if there are no sidebars or top-level navs. If ``True``, - use :func:`~shiny.ui.page_fillable`, where the content fills the window. If - ``False`` (the default), the value of ``_full_width`` will determine which page - function is used. - _full_width + title + The browser window title (defaults to the host URL of the page). + lang + ISO 639-1 language code for the HTML page, such as ``"en"`` or ``"ko"``. This + will be used as the lang in the ```` tag, as in ````. The + default, `None`, results in an empty string. + fillable + If there is a top-level sidebar or nav, then the value is passed through to the + :func:`~shiny.ui.page_sidebar` or :func:`~shiny.ui.page_navbar` function. + Otherwise, if ``True``, use :func:`~shiny.ui.page_fillable`, where the content + fills the window; if ``False`` (the default), the value of ``full_width`` will + determine which page function is used. + full_width This has an effect only if there are no sidebars or top-level navs, and - ``_fillable`` is ``False``. If this is ``False`` (the default), use use + ``fillable`` is ``False``. If this is ``False`` (the default), use use :func:`~shiny.ui.page_fixed`; if ``True``, use :func:`~shiny.ui.page_fillable`. + page_fn + The page function to use. If ``None`` (the default), will automatically choose + one based on the arguments provided. If not ``None``, this will override all + heuristics for choosing page functions. **kwargs Additional arguments, which are passed to the page function. @@ -485,6 +495,10 @@ def page_auto( : A UI element. """ + if not isinstance(title, MISSING_TYPE): + kwargs["title"] = title + if not isinstance(lang, MISSING_TYPE): + kwargs["lang"] = lang # Presence of a top-level nav items and/or sidebar determines the page function navs = [x for x in args if isinstance(x, (NavPanel, NavMenu))] @@ -492,22 +506,27 @@ def page_auto( nNavs = len(navs) nSidebars = len(sidebars) - - if _page_fn is None: + if page_fn is None: if nNavs == 0: if nSidebars == 0: - if _fillable: - _page_fn = page_fillable # pyright: ignore[reportGeneralTypeIssues] - elif _full_width: - _page_fn = page_fluid # pyright: ignore[reportGeneralTypeIssues] + if isinstance(fillable, MISSING_TYPE): + fillable = False + + if fillable: + page_fn = page_fillable # pyright: ignore[reportGeneralTypeIssues] + elif full_width: + page_fn = page_fluid # pyright: ignore[reportGeneralTypeIssues] else: - _page_fn = page_fixed # pyright: ignore[reportGeneralTypeIssues] + page_fn = page_fixed # pyright: ignore[reportGeneralTypeIssues] elif nSidebars == 1: + if not isinstance(fillable, MISSING_TYPE): + kwargs["fillable"] = fillable + # page_sidebar() needs sidebar to be the first arg # TODO: Change page_sidebar() to remove `sidebar` and accept a sidebar as a # *arg. - _page_fn = page_sidebar # pyright: ignore[reportGeneralTypeIssues] + page_fn = page_sidebar # pyright: ignore[reportGeneralTypeIssues] args = tuple(sidebars + [x for x in args if x not in sidebars]) else: @@ -517,14 +536,17 @@ def page_auto( # At least one nav else: + if not isinstance(fillable, MISSING_TYPE): + kwargs["fillable"] = fillable + if nSidebars == 0: # TODO: what do we do when nArgs != nNavs? Just let page_navbar handle it (i.e. error)? - _page_fn = page_navbar # pyright: ignore[reportGeneralTypeIssues] + page_fn = page_navbar # pyright: ignore[reportGeneralTypeIssues] elif nSidebars == 1: # TODO: change page_navbar() to remove `sidebar` and accept a sidebar as a # *arg. - _page_fn = page_navbar # pyright: ignore[reportGeneralTypeIssues] + page_fn = page_navbar # pyright: ignore[reportGeneralTypeIssues] kwargs["sidebar"] = sidebars[0] else: @@ -533,8 +555,8 @@ def page_auto( ) # If we got here, _page_fn is not None, but the type checker needs a little help. - _page_fn = cast(Callable[..., Tag], _page_fn) - return _page_fn(*args, **kwargs) + page_fn = cast(Callable[..., Tag], page_fn) + return page_fn(*args, **kwargs) def page_output(id: str) -> Tag: From cdf8a3edb5608fe64ec0b8de7feb2683cc3159ed Mon Sep 17 00:00:00 2001 From: Winston Chang Date: Thu, 21 Dec 2023 17:49:52 -0600 Subject: [PATCH 26/26] Replace set_page() with page_opts() --- tests/playwright/shiny/shiny-express/page_fillable/app.py | 2 +- tests/playwright/shiny/shiny-express/page_fluid/app.py | 2 +- tests/playwright/shiny/shiny-express/page_sidebar/__init__.py | 0 tests/playwright/shiny/shiny-express/page_sidebar/app.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) delete mode 100644 tests/playwright/shiny/shiny-express/page_sidebar/__init__.py diff --git a/tests/playwright/shiny/shiny-express/page_fillable/app.py b/tests/playwright/shiny/shiny-express/page_fillable/app.py index 9c2013b04..94e8b2a99 100644 --- a/tests/playwright/shiny/shiny-express/page_fillable/app.py +++ b/tests/playwright/shiny/shiny-express/page_fillable/app.py @@ -1,7 +1,7 @@ from shiny import render from shiny.express import input, ui -ui.set_page(ui.page_fillable()) +ui.page_opts(fillable=True) with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) diff --git a/tests/playwright/shiny/shiny-express/page_fluid/app.py b/tests/playwright/shiny/shiny-express/page_fluid/app.py index 0422113b8..54fd50c64 100644 --- a/tests/playwright/shiny/shiny-express/page_fluid/app.py +++ b/tests/playwright/shiny/shiny-express/page_fluid/app.py @@ -1,7 +1,7 @@ from shiny import render from shiny.express import input, ui -ui.set_page(ui.page_fluid()) +ui.page_opts(full_width=True) with ui.card(id="card"): ui.input_slider("a", "A", 1, 100, 50) diff --git a/tests/playwright/shiny/shiny-express/page_sidebar/__init__.py b/tests/playwright/shiny/shiny-express/page_sidebar/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/playwright/shiny/shiny-express/page_sidebar/app.py b/tests/playwright/shiny/shiny-express/page_sidebar/app.py index fac04a612..8168abf73 100644 --- a/tests/playwright/shiny/shiny-express/page_sidebar/app.py +++ b/tests/playwright/shiny/shiny-express/page_sidebar/app.py @@ -1,7 +1,7 @@ from shiny import render from shiny.express import input, ui -ui.set_page(ui.page_sidebar(title="PageTitle")) +ui.page_opts(title="PageTitle") with ui.sidebar(id="sidebar", title="SidebarTitle"): "Sidebar Content"