posit-dev / py-shinyswatch

Bootswatch themes for py-shiny
https://posit-dev.github.io/py-shinyswatch/
MIT License
28 stars 3 forks source link

Theme picker? #11

Closed vnijs closed 1 year ago

vnijs commented 1 year ago

py-shinyswatch looks outstanding. Thanks for creating this!

I'm almost surprised no one has asked this question yet since theme pickers seem to be very popular but ... is something like the below possible for shiny-for-python and py-shinyswatch?

library(shiny)
library(bslib)

# Define UI
ui <- fluidPage(
    theme = bs_theme(),

    # Add a select input for theme selection
    selectInput("theme", "Choose a theme:",
        choices = c(
            "default", "cerulean", "cosmo", "flatly",
            "journal", "litera", "lumen", "lux", "materia",
            "minty", "pulse", "sandstone", "simplex",
            "sketchy", "slate", "solar", "spacelab",
            "superhero", "united", "yeti"
        )
    ),

    # Add some content
    h1("Hello, Shiny!"),
    p("This is a sample Shiny app with theme selection.")
)

# Define server logic
server <- function(input, output, session) {
    # Update the theme when the input changes
    observe({
        session$setCurrentTheme(bs_theme(bootswatch = input$theme))
    })
}

# Run the application
shinyApp(ui = ui, server = server)
schloerke commented 1 year ago

Yes, but it'll be tricky as the current shinyswatch themes are only html dependencies.

It will at least need a page refresh. I don't know off the top of my head if the ui can be a function in py-shiny. If so, the we can make it work. Not pretty, but it'd work.

Currently py-shiny does not have theme support built in like R shiny

vnijs commented 1 year ago

Thanks for the quick comment @schloerke. Page refresh isn't a big deal. Would that involve something like onclick = "window.location.reload();"?

Not sure what you mean by "if the ui can be a function in py-shiny". In the below app_ui is a function. I started on the below before realizing I didn't know a way to push the theme into the header. I also use functions a lot to reuse pieces of ui across apps.

from shiny import App, Inputs, Outputs, Session, ui, reactive

import shinyswatch

def app_ui():
    return ui.page_fluid(
        # Theme code - start
        shinyswatch.theme.sketchy(),
        # Theme code - end
        ui.input_select(
            id="select_theme",
            label="Select a theme:",
            selected="superhero",
            choices={
                "superhero": "Super Hero",
                "darkly": "Darkly",
                "sketchy": "Sketchy",
            },
        ),
    )

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    def set_theme():
        return shinyswatch.get_theme(f"{input.select_theme()}")

app = App(app_ui(), server)
schloerke commented 1 year ago

Oh! Great!

Cliff notes before a proper approach on Monday...

vnijs commented 1 year ago

Gave it a try but it keeps resetting to the original value.

from shiny import App, Inputs, Outputs, Session, ui, reactive

import shinyswatch

if "theme" not in globals():
    theme = {"theme": "superhero"}

def app_ui():
    return ui.page_fluid(
        shinyswatch.get_theme(theme.get("theme", "superhero")),
        ui.tags.script(
            """
            Shiny.addCustomMessageHandler('refresh', function(message) {
                window.location.reload();
            });
            """
        ),
        ui.input_select(
            id="select_theme",
            label="Select a theme:",
            selected=theme.get("theme", "superhero"),
            choices=["superhero", "darkly", "sketchy"],
        ),
    )

def server(input: Inputs, output: Outputs, session: Session):
    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    async def set_theme():
        if input.select_theme() != theme.get("theme", "superhero"):
            theme["theme"] = input.select_theme()
            await session.send_custom_message("refresh", "")

app = App(app_ui(), server)
vnijs commented 1 year ago

app_ui isn't called on browser restart.

from shiny import App, Inputs, Outputs, Session, ui, reactive, render
import shinyswatch

if "theme" not in globals():
    print("Define theme dictionary. Was not in globals()")
    theme = {"theme": "superhero"}

def app_ui():
    print("ui function was called")
    return ui.page_fluid(
        shinyswatch.get_theme(theme.get("theme", "superhero")),
        ui.tags.script(
            """
            Shiny.addCustomMessageHandler('refresh', function(message) {
                window.location.reload();
            });
            """
        ),
        # ui.input_select(
        #     id="select_theme",
        #     label="Select a theme:",
        #     selected=theme.get("theme", "superhero"),
        #     choices=["superhero", "darkly", "sketchy"],
        # ),
        ui.output_ui("ui_select_theme"),
    )

def server(input: Inputs, output: Outputs, session: Session):
    print("server function was called")
    print(theme.get("theme", "superhero"))

    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    async def set_theme():
        if input.select_theme() != theme.get("theme", "superhero"):
            theme["theme"] = input.select_theme()
            await session.send_custom_message("refresh", "")
        # ui.update_select("select_theme", selected=theme.get("theme", "superhero"))

    @output(id="ui_select_theme")
    @render.ui
    def ui_select_theme():
        return (
            ui.input_select(
                id="select_theme",
                label="Select a theme:",
                selected=theme.get("theme", "superhero"),
                choices=["superhero", "darkly", "sketchy"],
            ),
        )

app = App(app_ui(), server, debug=False)
schloerke commented 1 year ago

app_ui isn't called on browser restart

That's not good. Not much point of having a function, then. 😝 This behavior is definitely a bug in py-shiny.

I thought your solution would have worked (but only glancing through it). But if the ui isn't being recalculated of refresh, then we're already off to a rough start.

vnijs commented 1 year ago

Thanks @schloerke. Report as an issue to py-shiny?

schloerke commented 1 year ago

If you're up for it! Please reference this one. Thank you!

schloerke commented 1 year ago

I missed that app_ui was being called as it was being sent into App. Instead, app_ui should be a function that takes a starlette.requests.Request object for context about the request.

Updated app below:

import shinyswatch
from htmltools import Tag
from starlette.requests import Request as StarletteRequest

from shiny import App, Inputs, Outputs, Session, reactive, ui

if "theme_obj" not in globals():
    print("Define theme dictionary. Was not in globals()")
    theme_obj = {"theme": "superhero"}

def app_ui(request: StarletteRequest) -> Tag:
    print("ui function was called")
    return ui.page_fluid(
        shinyswatch.get_theme(theme_obj.get("theme")),
        ui.tags.script(
            """
            Shiny.addCustomMessageHandler('refresh', function(message) {
                window.location.reload();
            });
            """
        ),
        ui.input_select(
            id="select_theme",
            label="Select a theme:",
            selected=theme_obj.get("theme"),
            choices=["superhero", "darkly", "sketchy"],
        ),
    )

def server(input: Inputs, output: Outputs, session: Session):
    print("server function was called")
    print(theme_obj.get("theme"))

    @reactive.Effect
    @reactive.event(input.select_theme, ignore_none=True)
    async def set_theme():
        if input.select_theme() != theme_obj.get("theme"):
            print("setting theme: ", input.select_theme())
            theme_obj["theme"] = input.select_theme()
            await session.send_custom_message("refresh", "")

app = App(app_ui, server, debug=False)
schloerke commented 1 year ago

** working on making shinyswatch.theme_picker(app: App)

vnijs commented 1 year ago

Nice! Thanks @schloerke. Should I remove the related issue for py-shiny?

EDIT: Just noticed you already closed that issue.

schloerke commented 1 year ago

@vnijs Let me know how #12 works for you!

Remove the theme from your app's UI and wrap your app in shinyswatch.theme_picker(app)

Ex:

# ... at bottom of file: app.py
app = shinyswatch.theme_picker(App(app_ui, server))
vnijs commented 1 year ago

Very nice @schloerke! Question: Could the theme dropdown be included in the navbar? Not without adding it back to the UI and forcing a refresh correct?

schloerke commented 1 year ago

Not without adding it back to the UI and forcing a refresh correct?

Correct. It would require a "module" type approach that would need both a ui and server function.


I'm ok using the module approach if you find it more familiar. Altering the app object is a new approach compared to R.

Thoughts?

vnijs commented 1 year ago

To be honest, I never really used modules in R. Just used functions instead. Would love to see the example. Thanks @schloerke

schloerke commented 1 year ago

Yes, it'd basically be shinyswatch.theme_picker_ui(*, default_theme: str, id: str) and shinyswatch.theme_picker_server(*, id: str). Each function would live within the UI / server respectively.