Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
1716b1d
Add TopLevelRecallContextManager which inspects args to infer page de…
cpsievert Dec 11, 2023
5fa9a52
WIP convert layout to ui
wch Dec 12, 2023
19b8f44
Add remaining components to shiny.express.ui
wch Dec 12, 2023
fac3ca2
Install htmltools from github
wch Dec 13, 2023
d9361d2
Update examples
wch Dec 13, 2023
f03bb44
Better test error messages
wch Dec 13, 2023
74412af
Update express UI components
wch Dec 13, 2023
63e7307
Merge remote-tracking branch 'origin/main' into express-layout-to-ui
wch Dec 14, 2023
9d75b26
Remove row and column from express.ui
wch Dec 14, 2023
d3e5f32
Merge branch 'main' into express-inspect-args
wch Dec 15, 2023
b552067
Move TopLevelRecallContextManager code to page_auto
wch Dec 15, 2023
1a8935d
For multiple sidebars, require use of layout_sidebar()
wch Dec 15, 2023
8afcc28
Add set_page_*, use_page_* functions
wch Dec 16, 2023
af25ce2
Update docstrings
wch Dec 18, 2023
38472f0
Merge branch 'main' into express-layout-to-ui
wch Dec 20, 2023
55b60fd
Add .tagify() method to RecallContextManager
wch Dec 20, 2023
16d03f2
Add shiny.express.layout compatibility shim
wch Dec 21, 2023
364cbad
Remove navset and navset_card
wch Dec 21, 2023
b3db4db
Fix example app
wch Dec 21, 2023
b084224
Fix exports
wch Dec 21, 2023
675d015
Update docstrings
wch Dec 21, 2023
0366460
Update test apps
wch Dec 21, 2023
8df8d94
Import fixes in test apps
wch Dec 21, 2023
2f801f7
Documentation updates
wch Dec 21, 2023
1a7cdd8
Merge branch 'express-layout-to-ui' into express-inspect-args-2
wch Dec 21, 2023
a7b6664
Update known missing items
wch Dec 21, 2023
31c8130
Update quartodoc entries
wch Dec 21, 2023
06b31e0
API updates
wch Dec 21, 2023
c03f684
Update page_auto
wch Dec 21, 2023
cdf8a3e
Replace set_page() with page_opts()
wch Dec 21, 2023
cb7886e
Merge remote-tracking branch 'origin/main' into express-inspect-args
wch Dec 22, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
334 changes: 334 additions & 0 deletions shiny/express/_page.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
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]
Copy link
Collaborator Author

@cpsievert cpsievert Dec 18, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not that we absolutely need this now, but I think we could support wider=F when fillable=T by including a {"class": "container"} (probably on the <body>)?

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 ``<html>`` tag, as in ``<html lang="en">``. 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 ``<html>`` tag, as in ``<html lang="en">``. 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 ``<html>`` tag, as in ``<html lang="en">``. 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 ``<html>`` tag, as in ``<html lang="en">``. 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,
)
)
9 changes: 1 addition & 8 deletions shiny/express/_recall_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 wrap_displayhook_handler

from .._typing_extensions import ParamSpec

Expand All @@ -19,12 +19,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
if args is None:
args = tuple()
if kwargs is None:
Expand All @@ -33,11 +31,6 @@ def __init__(
self.kwargs: dict[str, object] = dict(kwargs)

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 = wrap_displayhook_handler(self.args.append)
Expand Down
Loading