16  Theming and Wrappers

When CSS alone (Chapter 15) is not enough — you want a different page shell, a theme system like bslib, or a layout that does not map onto ptr_app()’s defaults — write a thin wrapper around the public ggpaintr primitives. A single-formula wrapper composes the bare L3 pieces (Chapter 12) plus the scaffolding they need:

16.1 A worked example: a bslib wrapper

The entire wrapper is short enough to read in one screen — everything ggpaintr-specific is public API; the only non-ggpaintr pieces are bslib::page_sidebar() and the theme = passthrough, which belong to bslib:

my_app_bslib <- function(formula,
                         envir = parent.frame(),
                         ui_text = NULL,
                         expr_check = TRUE,
                         safe_to_remove = character(),
                         theme = NULL) {
  if (!requireNamespace("bslib", quietly = TRUE)) {
    rlang::abort("Package 'bslib' is required for my_app_bslib().")
  }
  if (is.null(theme)) {
    theme <- bslib::bs_theme(version = 5, bootswatch = "flatly")
  }
  id <- "ptr"
  title <- ptr_resolve_ui_text("title", ui_text = ui_text)$label %||% "ggpaintr"

  ui <- bslib::page_sidebar(
    title = title,
    theme = theme,
    sidebar = bslib::sidebar(
      title = "Controls",
      # The `.ptr-app` div restores the themed scope the bslib page chrome
      # does not provide, and ptr_ui_assets() ships the bundle (deduped
      # page-wide by htmlDependency).
      shiny::tags$div(
        class = "ptr-app",
        ptr_ui_assets(),
        ptr_ui_controls(
          id = id, formula = formula,
          ui_text = ui_text,
          expr_check = expr_check,
          shared = NULL
        )
      )
    ),
    bslib::card(
      shiny::tags$div(
        class = "ptr-app",
        ptr_ui_assets(),
        ptr_ui_toggle_code(
          ptr_ui_inline_error(ptr_ui_plot(id), ptr_ui_error(id)),
          ptr_ui_code(id)
        )
      )
    )
  )

  server <- function(input, output, session) {
    ptr_server(
      formula = formula,
      id = id,
      envir = envir,
      ui_text = ui_text,
      expr_check = expr_check,
      safe_to_remove = safe_to_remove
    )
  }

  shiny::shinyApp(ui = ui, server = server)
}

my_app_bslib("ggplot(iris, aes(ppVar('Sepal.Length'), ppVar('Sepal.Width'), color = ppVar('Species'))) + geom_point()")

The same ggpaintr machinery under a bslib page_sidebar shell with the flatly theme.

Note the bslib root is exactly the case ptr_ui_page() does not cover (the bundled CSS is Bootstrap-3-scoped, and a BS5 page is not a BS3 page builder) — which is why the wrapper reproduces the shell’s two obligations by hand: ptr_ui_assets() once per ggpaintr region, and the .ptr-app theme scope around the pieces. That recipe is the same one the navbarPage decomposition uses (Chapter 12).

16.2 Wrappers compose

A “dark mode” variant is one line:

my_app_dark <- function(formula, ...) {
  my_app_bslib(
    formula,
    theme = bslib::bs_theme(version = 5, bootswatch = "darkly"),
    ...
  )
}

The same template extends to other shells — bslib::page_navbar(), a custom fluidPage() with bespoke chrome, a flexdashboard-style page: swap the UI half, leave the ptr_server() half alone. A wrapper is free to expose any downstream-library arguments (here theme =) alongside the ggpaintr ones it forwards (ui_text =, expr_check =, safe_to_remove =, envir =).

16.3 Public-API limits

The bare pieces are independently placeable, so a layout that puts the plot and the generated code in separate containers — two side-by-side bslib::card()s, say — is just a matter of dropping each piece where you want it instead of nesting them through the combinators; skip ptr_ui_toggle_code() / ptr_ui_inline_error() and the panes render plain and standalone. Single-formula ppVar(shared = "...") coordination also works on the bare path — ptr_server() self-binds every declared key under its own namespace, exactly like ptr_app(). The coordinator trio is only for two or more ggpaintr blocks (Chapter 11).

One thing a wrapper cannot do: hand-place individual placeholder widgets. ptr_ui_controls() is the supported rendering of the generated widgets, and there is no public accessor for the parsed node tree (Chapter 12).