diff --git a/solara/hooks/misc.py b/solara/hooks/misc.py index f4eff52df..1b64f06dd 100644 --- a/solara/hooks/misc.py +++ b/solara/hooks/misc.py @@ -27,6 +27,7 @@ "use_unique_key", "use_state_or_update", "use_previous", + "use_trait_observe", ] T = TypeVar("T") U = TypeVar("U") @@ -232,3 +233,31 @@ def assign(): solara.use_effect(assign, [value]) return ref.current + + +def use_trait_observe(has_trait_object, name): + """Observe a trait on an object, and return its value. + + This is useful when you want your component to be in sync with a trait + of a widget or [HasTraits object](https://traitlets.readthedocs.io/en/stable/). + + When the trait changes, your component will be re-rendered. + + See [use_dark_effective](/api/use_dark_effective) for an example. + """ + counter = solara.use_reactive(0) + counter.get() # make the main component depend on this counter + + def connect(): + def update(change): + counter.value += 1 + + has_trait_object.observe(update, name) + + def cleanup(): + has_trait_object.unobserve(update, name) + + return cleanup + + solara.use_effect(connect, [has_trait_object, name]) + return getattr(has_trait_object, name) diff --git a/solara/lab/components/__init__.py b/solara/lab/components/__init__.py index f00c9df75..385689cca 100644 --- a/solara/lab/components/__init__.py +++ b/solara/lab/components/__init__.py @@ -3,4 +3,4 @@ from .input_date import InputDate, InputDateRange # noqa: F401 from .menu import ClickMenu, ContextMenu, Menu # noqa: F401 F403 from .tabs import Tab, Tabs # noqa: F401 -from .theming import ThemeToggle, theme # noqa: F401 +from .theming import ThemeToggle, theme, use_dark_effective # noqa: F401 diff --git a/solara/lab/components/theming.py b/solara/lab/components/theming.py index 390810854..ed2beb3ca 100644 --- a/solara/lab/components/theming.py +++ b/solara/lab/components/theming.py @@ -11,6 +11,20 @@ ipyvuetify.Themes.theme = cast(ipyvuetify.Themes.Theme, theme) +def use_dark_effective(): + """Return True if the frontend is using a dark theme. + + Equivalent of + + ```python + solara.use_trait_observe(solara.lab.theme, "dark_effective") + ``` + + See [use_trait_observe](/api/use_trait_observe). + """ + return solara.use_trait_observe(theme, "dark_effective") + + def _set_theme(themes: Union[Dict[str, Dict[str, str]], None]): if themes is None: return diff --git a/solara/website/components/header.py b/solara/website/components/header.py index 3af55c979..20e910081 100644 --- a/solara/website/components/header.py +++ b/solara/website/components/header.py @@ -5,9 +5,6 @@ from solara.alias import rv from solara.server import settings -# TODO: remove import once function is included in solara -from solara.website.pages.apps.scatter import use_dark_effective - @solara._component_vue("algolia.vue") def Algolia(app_id: str, index_name: str, api_key: str, debug=False): @@ -22,6 +19,7 @@ def Header( # use routes of parent (assuming we are a child of a layout) route_current, all_routes = solara.use_route(level=-1) router = solara.use_router() + dark_effective = solara.lab.use_dark_effective() # set states for menu with solara.Column(gap="0px"): @@ -36,7 +34,7 @@ def Header( with solara.Button(icon=True, class_="hidden-md-and-up", on_click=lambda: on_toggle_left_menu and on_toggle_left_menu()): rv.Icon(children=["mdi-menu"]) with solara.Link(path_or_route="/"): - solara.Image(router.root_path + f"/static/assets/images/logo{'_white' if use_dark_effective() else ''}.svg") + solara.Image(router.root_path + f"/static/assets/images/logo{'_white' if dark_effective else ''}.svg") rv.Spacer() if settings.search.enabled: diff --git a/solara/website/pages/api/__init__.py b/solara/website/pages/api/__init__.py index e6f8a7e5f..ee9f94eec 100644 --- a/solara/website/pages/api/__init__.py +++ b/solara/website/pages/api/__init__.py @@ -80,6 +80,7 @@ "use_reactive", "use_state", "use_state_or_update", + "use_trait_observe", ], }, { @@ -136,6 +137,7 @@ "task", "theming", "use_task", + "use_dark_effective", ], }, ] diff --git a/solara/website/pages/api/use_dark_effective.py b/solara/website/pages/api/use_dark_effective.py new file mode 100644 index 000000000..b9216cd67 --- /dev/null +++ b/solara/website/pages/api/use_dark_effective.py @@ -0,0 +1,13 @@ +"""# use_dark_effective + +""" +import solara +import solara.autorouting +import solara.lab +from solara.website.utils import apidoc + +from . import NoPage + +title = "use_dark_effective" +Page = NoPage +__doc__ += apidoc(solara.lab.use_dark_effective) # type: ignore diff --git a/solara/website/pages/api/use_trait_observe.py b/solara/website/pages/api/use_trait_observe.py new file mode 100644 index 000000000..a03ce89a7 --- /dev/null +++ b/solara/website/pages/api/use_trait_observe.py @@ -0,0 +1,13 @@ +"""# use_trait_observe + +""" +import solara +import solara.autorouting +import solara.lab +from solara.website.utils import apidoc + +from . import NoPage + +title = "use_trait_observe" +Page = NoPage +__doc__ += apidoc(solara.use_trait_observe) # type: ignore diff --git a/solara/website/pages/apps/scatter.py b/solara/website/pages/apps/scatter.py index c1a733b5b..f5e202660 100644 --- a/solara/website/pages/apps/scatter.py +++ b/solara/website/pages/apps/scatter.py @@ -49,34 +49,10 @@ def reset(): State.df.value = None -def use_trait_observe(has_trait_object, name): - # TODO: this hook should go into solara - counter = solara.use_reactive(0) - counter.get() # make the main component depend on this counter - - def connect(): - def update(change): - counter.value += 1 - - has_trait_object.observe(update, name) - - def cleanup(): - has_trait_object.unobserve(update, name) - - return cleanup - - solara.use_effect(connect, []) - return getattr(has_trait_object, name) - - -def use_dark_effective(): - return use_trait_observe(solara.lab.theme, "dark_effective") - - @solara.component def Page(): df = State.df.value - dark_effective = use_dark_effective() + dark_effective = solara.lab.use_dark_effective() # the .scatter will set this cross filter filter, _set_filter = solara.use_cross_filter(id(df)) @@ -142,5 +118,5 @@ def get_data(): @solara.component def Layout(children): route, routes = solara.use_route() - dark_effective = use_dark_effective() + dark_effective = solara.lab.use_dark_effective() return solara.AppLayout(children=children, toolbar_dark=dark_effective, color=None) # if dark_effective else "primary")