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()")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:
ptr_ui_controls(formula, id = NULL, ...)— the layer picker, per-layer panels, and the Update plot button. It is bare, so the wrapper supplies the themed scope — adiv(class = "ptr-app")— and the bundled stylesheet viaptr_ui_assets()(self-deduping page-wide).- The output stack —
ptr_ui_toggle_code(ptr_ui_inline_error(ptr_ui_plot(id), ptr_ui_error(id)), ptr_ui_code(id)): plot card, inline error, and slide-out code window, byte-for-byte whatptr_app()renders internally. ptr_server(formula, id = NULL, ...)— the server side, unchanged. Forwardsui_text/expr_check/safe_to_remove/envirand wires all the reactivity, including single-formula shared keys.
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:

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).