---
title: "Tutorial"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Tutorial}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
  collapse = TRUE,
  comment  = "#>",
  warning  = FALSE,
  message  = FALSE,
  eval     = interactive()
)
```

```{r libs}
library(ggpaintr)
library(ggplot2)
library(rlang)   # for `%||%`, sym(), call2(), abort() used in the examples
```

This tutorial walks through ggpaintr from the outside in. Section 1 gets you a running app from a one-line formula and introduces the built-in placeholders. Section 2 shows how to define your own placeholders, taking each argument of the three `ptr_define_placeholder_*()` constructors in turn. Section 3 leaves the turn-key entry point behind: you write your own Shiny app, embed several plots, and wire one control to all of them.

Every app chunk is marked `eval = interactive()` — paste it at the R prompt to launch the app. Pure-ggplot chunks that produce a static plot run inline.

# 1. Basics

A ggpaintr **formula** is an ordinary `ggplot()` call written out as code, with **placeholder keywords** dropped in wherever a user should get to choose a value. `ptr_app()` reads the formula, turns each placeholder into a Shiny widget, and re-runs the `ggplot()` call with the user's inputs spliced back in.

The smallest useful app is one line:

```{r app-basic}
ptr_app(
  ggplot(mtcars, aes(x = ppVar, y = ppVar)) + geom_point()
)
```

This launches a Shiny app with two column dropdowns (one per `ppVar`), a layer panel, and an **Update plot** button. There is no `data =` argument: ggpaintr resolves the bare symbol `mtcars` in the calling environment (`envir`, default `parent.frame()`). The only required argument to `ptr_app()` is the formula itself; `envir`, `ui_text`, `css`, `expr_check`, and `spec` are all optional.

## The five built-in placeholders

Each keyword maps to a fixed widget and a fixed way of folding the input back into the formula:

| Keyword    | Widget                            | Role          | Folds back as                          |
|------------|-----------------------------------|---------------|----------------------------------------|
| `ppVar`    | column picker (data-aware)        | consumer      | a column symbol, e.g. `mpg`            |
| `ppText`   | text input                        | value         | a string                               |
| `ppNum`    | numeric input                     | value         | a number                               |
| `ppExpr`   | code box (validated)              | value         | live code, parsed to an expression     |
| `ppUpload` | file picker (+ dataset-name box)  | source        | a data frame                           |

The three **roles** in the right column matter once you start defining your own placeholders (Section 2):

- A **value** placeholder produces a self-contained value — it needs nothing from the rest of the formula.
- A **consumer** placeholder needs the upstream data's column names (a `ppVar` dropdown can only list columns once it knows what data flows into it).
- A **source** placeholder *produces* the data frame the rest of the formula reads.

A formula can mix all of them, and placeholders work anywhere in a pipeline, not just inside `ggplot()`:

```{r formula-tour}
ptr_app(
  mtcars |>
    dplyr::filter(ppExpr(mpg > 15)) |>
    ggplot(aes(x = ppVar, y = ppVar, color = ppVar)) +
    geom_point(size = ppNum) +
    labs(title = ppText)
)
```

## Seeding the widgets

Give a placeholder a single **positional argument** and it becomes the widget's starting value:

```{r app-basic-seeded}
ptr_app(
  ggplot(mtcars, aes(x = ppVar("wt"), y = ppVar("mpg"))) +
    geom_point(size = ppNum(3), alpha = ppNum(0.6)) +
    labs(title = ppText("Weight vs. mileage"))
)
```

The app now boots with `wt`/`mpg` already picked, size 3, and the title pre-filled. The default is read *literally* from the formula text — it is never evaluated as user code. (`ppNum` does accept simple arithmetic like `ppNum(2 * pi)`, folded at build time against a small allowlist.)

## Nothing renders until you click Update

ggpaintr re-draws only when the **Update plot** button is clicked. Changing a widget stages a new value but does not redraw on its own. This keeps a half-typed expression from strobing the plot — and it is the one thing to remember when scripting the app in tests: set the inputs, *then* click the button.

# 2. Defining your own placeholders

The five built-ins are themselves registered through the same public API you are about to use. You define a placeholder by calling one of three constructors, keyed by the role from Section 1:

- `ptr_define_placeholder_value()`
- `ptr_define_placeholder_consumer()`
- `ptr_define_placeholder_source()`

All three are thin wrappers over a shared core, so they take an overlapping set of arguments. We cover the **value** constructor in full first, then describe only what is *different* for consumer and source.

A few facts hold for all three:

- The registry is **process-global**. Calling a constructor *registers* the keyword as a side effect (and returns a plain-R function, see `embellish_eval` below); there is no `placeholders =` argument to thread anywhere. Register once near the top of your script, then use the keyword in any formula.
- The keyword you register is the keyword you write in the formula. By convention built-ins use the `pp` prefix, but yours need not.

## 2.1 A value placeholder — every argument

Here is a custom `ppPercent`: a 0–100 slider whose value is divided by 100 before it reaches the plot. It exercises **every** argument of `ptr_define_placeholder_value()`.

```{r define-value}
ptr_define_placeholder_value(
  keyword = "ppPercent",

  build_ui = function(node, label = NULL, selected = NULL,
                      named_args = list(), ...) {
    step <- named_args$step %||% 1
    shiny::sliderInput(
      node$id, label = label %||% "Percent",
      min = 0, max = 100,
      value = selected %||% node$default %||% 50,
      step = step
    )
  },

  resolve_expr = function(value, node, ...) {
    if (is.null(value)) return(NULL)
    as.numeric(value) / 100
  },

  validate_session_input = function(value, ctx) {
    v <- suppressWarnings(as.numeric(value))
    if (length(v) != 1L || is.na(v) || v < 0 || v > 100) {
      rlang::abort("Percent must be a single number between 0 and 100.")
    }
    value
  },

  parse_positional_arg = ptr_arg_numeric(),
  parse_named_args     = list(step = ptr_arg_numeric()),
  embellish_eval       = function(x, ...) as.numeric(x) / 100,
  ui_text_defaults = list(label = "Percent for {param}")
)
```

Used in a formula — the positional `40` seeds the slider, the named `step = 5` is forwarded to `build_ui`:

```{r define-value-app}
ptr_app(
  ggplot(mtcars, aes(x = ppVar("wt"), y = ppVar("mpg"))) +
    geom_point(alpha = ppPercent(40, step = 5))
)
```

### `keyword`

The name to register and to write in formulas. Required.

### `build_ui` — and why its signature looks the way it does

`build_ui` is a function that returns the Shiny control. Its **first argument is `node`** (required) — an object carrying, among other things, `node$id` (the input id you must give your widget) and `node$default` (the literal positional default from the formula, or `NULL`).

The framework does not call `build_ui(node)` bare. It calls it through an internal injector that *also* passes:

- `label` — the resolved label text (from `ui_text_defaults`, possibly overridden by the app's `ui_text`);
- `selected` — `node$default`, injected **only when present and only if your function can receive it**;
- `named_args` — the validated named arguments from the formula call.

This is why the signature is `function(node, label = NULL, selected = NULL, named_args = list(), ...)`:

- You **opt in** to an injected argument by naming it (`label`, `selected`, `named_args`) — or by having `...`. The injector checks your formals: if you neither name `selected` nor accept `...`, the default is never injected. Naming the ones you use and keeping `...` to swallow the rest is the safe, forward-compatible shape.
- Seed your widget's starting value from `selected %||% node$default %||% <fallback>`. `selected` carries the persisted input across re-renders; `node$default` is the boot seed; the fallback keeps the widget usable when the formula gave no default.
- Use `node$id` — and *only* `node$id` — as the widget id. Do not namespace it yourself; the framework already did.

### `resolve_expr`

`function(value, node, ...)` → the value (or expression) spliced back into the formula. `value` is the current input. Return `NULL` to contribute nothing (e.g. an empty field), which drops the argument cleanly. Here we divide by 100 so the plot sees a 0–1 alpha.

### `validate_session_input`

Optional. `function(value, ctx)`, run before `resolve_expr`. Return the value to accept it, or `rlang::abort()` to surface an inline error. For mid-typing artifacts that should not flash an error, signal `ptr_signal_partial()` instead of aborting — it is caught on the live keystroke path but not on the draw path. (`ctx` carries context; for value placeholders it is mostly empty — it earns its keep for consumers, Section 2.2.)

### `parse_positional_arg`

Declares whether the placeholder accepts a single positional default in the formula, and validates it. Pass one of the argument validators — `ptr_arg_string()`, `ptr_arg_numeric()`, `ptr_arg_symbol()`, `ptr_arg_symbol_or_string()`, `ptr_arg_expression()`. Each is a factory returning a checker that inspects the default **as unevaluated code** (no `eval()`), so `ppPercent(40)` is validated to be a numeric literal at translate time. The element factories also take `vector = TRUE` to accept a `c(...)` of elements instead of a scalar — `ptr_arg_numeric(vector = TRUE, length = 2)`, `ptr_arg_symbol(vector = TRUE)` (a multi-column default like `c(mpg, hp)`) — so a multi-column consumer can carry a positional default. Leaving `parse_positional_arg = NULL` (the default) **rejects** any positional argument — `ppPercent(40)` would error.

### `parse_named_args`

A fully-named list mapping extra named-argument names to validators, in the same family as `parse_positional_arg`. It lets the formula write `ppPercent(40, step = 5)`. The validated values arrive in `build_ui` as the `named_args` list. The name `shared` is reserved (it is ggpaintr's cross-widget binding key — Section 3) and may not appear here.

### `embellish_eval`

The plain-R meaning of the keyword *outside* `ptr_app()`. A placeholder-embellished formula must stay valid plain R that still renders the original plot with no app running; `embellish_eval` is the callable that supplies that meaning. Each constructor returns this function, so you can bind it under the keyword name —

```{r embellish-bind, eval = FALSE}
ppPercent <- ptr_define_placeholder_value("ppPercent", ...)
ppPercent(40)   # => 0.4, as ordinary R
```

— which makes a formula that uses the keyword still evaluate as ordinary ggplot code. The meaning is **author-controlled, never derived** — only you know what the keyword should mean as live R.

If you omit `embellish_eval`, value and consumer keywords default to `embellish_identity()` (the identity `function(x, ...) x`), so the placeholder call is a transparent no-op wrapper. Two built-in helpers cover the common cases:

- `embellish_identity()` — the default; returns its argument unchanged.
- `embellish_symbol_to_string()` — captures its argument *unevaluated* and returns the referenced column names as a character vector. This is the pattern a column-selecting consumer needs to run as plain R: a tidyselect verb evaluates an unknown wrapper call in non-masked scope, where bare column symbols throw `object 'mpg' not found`; returning the names as strings lets the naked formula still select by name.

### `ui_text_defaults`

A named list of copy defaults over `label`, `help`, `placeholder`, and `empty_text`, with `{param}` interpolated to the argument name. These are the *defaults*; an app can override them per-keyword or per-parameter through `ptr_app(ui_text = ...)`.

## 2.2 A consumer placeholder — the delta

A consumer is a value placeholder that additionally needs the **upstream column names**. Everything in 2.1 applies — `resolve_expr`, `validate_session_input`, `parse_positional_arg`, `parse_named_args`, `embellish_eval`, `ui_text_defaults` all mean the same thing. Only two things change.

**`build_ui` gains two required arguments: `cols` and `data`.** The injector fills `cols` with the column names of the data flowing into this point of the pipeline, and `data` with that data frame, re-running `build_ui` whenever the upstream changes. Your picker's `choices` come from `cols`:

```{r define-consumer}
ptr_define_placeholder_consumer(
  keyword = "colvars",

  build_ui = function(node, cols = character(), data = NULL,
                      label = NULL, selected = character(0), ...) {
    shiny::selectInput(
      node$id, label = label %||% "Columns",
      choices  = cols,
      selected = intersect(selected, cols),  # keep only still-valid picks
      multiple = TRUE
    )
  },

  resolve_expr = function(value, node, ...) {
    if (length(value) == 0L) return(NULL)
    rlang::call2("c", !!!as.list(value))   # c(col1, col2, ...)
  },

  parse_positional_arg = ptr_arg_symbol_or_string(),
  ui_text_defaults = list(label = "Columns for {param}")
  # validate_session_input / parse_named_args / embellish_eval: same shape as 2.1, omitted here.
)
```

```{r define-consumer-app}
ptr_app(
  mtcars |>
    dplyr::select(colvars) |>
    ggplot(aes(x = ppVar, y = ppVar)) + geom_point()
)
```

**`validate_session_input`'s `ctx` is now useful.** For a consumer, `ctx$data` holds the upstream data frame, so a validator can do data-aware checks — reject a non-numeric column, range-check the chosen values, and so on. (Same `function(value, ctx)` signature as 2.1; the difference is that `ctx$data` is populated.)

## 2.3 A source placeholder — the delta

A source *produces* the data the rest of the formula reads, so it sits at the head of a pipeline. The shared arguments (`parse_positional_arg`, `parse_named_args`, `embellish_eval`, `ui_text_defaults`) work exactly as in 2.1. Three things differ.

**`resolve_data` replaces `resolve_expr` as the required producer.** `function(value, node, ...)` must return a data frame. `resolve_expr` becomes *optional* and defaults to `function(value, node, ...) rlang::sym(value)` (the symbol that names the produced frame in generated code) — override it only if you need different generated code.

**`embellish_eval` defaults to an abort guard**, not identity — a source has no sensible plain-R meaning until you give it one. Override `embellish_eval` if you want the formula to be runnable as ordinary R.

**`shortcut = TRUE` adds a framework-owned companion text box.** When set, ggpaintr renders a sibling `textInput` (at `node$shortcut_id`) into which the user can type the *name* of an object to load from the app environment — the same "or type a dataset name" box you saw on `ppUpload`. Because the framework owns that box, **your `build_ui` must render only `node$id`** (or, for an env-name-only source, nothing at all) — rendering the shortcut yourself would bind the id twice.

This `ppDataset` lets the user type the name of any data frame in scope; the slider/selector style of widget is unnecessary because the framework text box is the entry point. Note that `resolve_data` runs later, inside the framework — so we capture the registration environment now (`.env`) and load from it, rather than reaching for `parent.frame()` at resolve time:

```{r define-source}
.env <- environment()   # the scope whose data frames should be loadable

ptr_define_placeholder_source(
  keyword  = "ppDataset",
  shortcut = TRUE,

  build_ui = function(node, label = NULL, ...) {
    # env-name-only source: the framework's shortcut text box is the sole
    # entry point, so build_ui contributes no widget of its own.
    NULL
  },

  resolve_data = function(value, node, ...) {
    nm <- if (is.character(value) && length(value) == 1L && nzchar(value)) value else NULL
    if (is.null(nm)) return(NULL)
    tryCatch(get(nm, envir = .env, inherits = TRUE),
             error = function(e) NULL)
  },

  resolve_expr     = function(value, node, ...) rlang::sym(value),
  ui_text_defaults = list(label = "Dataset for {param}")
)
```

```{r define-source-app}
ptr_app(
  ppDataset() |> ggplot(aes(x = ppVar("mpg"))) + geom_histogram()
)
```

A source that owns a real widget (e.g. a `selectInput` of dataset names) is the same shape with `shortcut = FALSE` and a `build_ui` that renders the picker at `node$id`.

# 3. Multiple plots, and writing your own Shiny app

`ptr_app()` is the turn-key entry point: it builds the whole Shiny app for you. When you want to place a ggpaintr plot inside your *own* app — alongside other UI, several plots at once, controls of your own — you drop down one level to `ptr_ui()` / `ptr_server()` and own the `shinyApp()` shell yourself.

## One plot inside your own app

You write the `fluidPage` and the server function. Put `ptr_ui(formula, id)` where the plot's controls and output should go, and `ptr_server(formula, id)` in the server with a **matching `id`**. `ptr_server()` namespaces itself — call it **bare**, never wrapped in your own `moduleServer()`:

`ptr_ui()` and `ptr_server()` take the formula the same way `ptr_app()` does: pass the `ggplot()` call directly, or — to write it once and hand the *same* formula to both — store it with `rlang::expr()` and splice it in with `!!`. (The string form still works as a fallback.)

```{r module-app}
f <- rlang::expr(ggplot(mtcars, aes(x = ppVar("wt"), y = ppVar("mpg"))) + geom_point())

ui <- shiny::fluidPage(
  shiny::h3("My dashboard"),
  ptr_ui(!!f, "plot1")
)
server <- function(input, output, session) {
  ptr_server(!!f, "plot1")
}
shiny::shinyApp(ui, server)
```

The `id` (`"plot1"`) is the namespace shared by the UI and the server; they must agree on it. Omitting it (`id = NULL`) gives bare, un-namespaced ids — fine for a single plot.

## Sharing one control across several plots

Often several plots should be driven by the *same* control — one x-axis picker, one size slider — rather than a copy per plot. You declare this with the reserved `shared = "<key>"` argument on any placeholder. Placeholders that carry the same key are backed by a single widget.

How that single widget is *placed* depends on how many formulas reference the key — this is the **partition**:

- A key used by **two or more formulas** is owned by a standalone **shared panel** that sits above the plots and drives all of them.
- A key used by **exactly one formula** renders **inline** in that one plot's own controls.

For several plots you build a small coordinator object with `ptr_shared()` and hand it to three pieces: `ptr_shared_panel()` (the standalone panel UI), each `ptr_ui(..., shared = obj)`, and `ptr_shared_server()` (whose result you thread into each `ptr_server(..., shared_state = )`).

The example below has two scatter plots over `iris`. The **size** slider is shared by both formulas, so it lands in the standalone panel and moves both plots at once. Each plot's **x-axis** picker is shared within its own formula only, so it renders inline under that plot:

```{r l2-shared-partition}
# A custom value placeholder for the shared size control: a 1-6 slider.
ptr_define_placeholder_value(
  keyword        = "ppSize",
  parse_positional_arg = ptr_arg_numeric(),
  build_ui = function(node, label = NULL, selected = NULL, ...) {
    val <- suppressWarnings(as.numeric(selected %||% node$default %||% 3))
    if (length(val) != 1L || is.na(val)) val <- 3
    shiny::sliderInput(node$id, label %||% "Size", min = 1, max = 6, value = val)
  },
  resolve_expr = function(value, node, ...) {
    out <- suppressWarnings(as.numeric(value))
    if (length(out) != 1L || is.na(out)) NULL else out
  }
)

plots <- list(
  rlang::expr(ggplot(iris, aes(x = ppVar(shared = "ax1"), y = Sepal.Width,
                               color = Species)) + geom_point(size = ppSize(shared = "sz"))),
  rlang::expr(ggplot(iris, aes(x = ppVar(shared = "ax2"), y = Petal.Width,
                               color = Species)) + geom_point(size = ppSize(shared = "sz")))
)

obj <- ptr_shared(formulas = plots)        # a list of formulas, passed as-is
obj$panel_keys           # "sz"  -- used by both formulas, so panel-owned

ui <- shiny::fluidPage(
  ptr_shared_panel(obj),                 # holds the shared size slider
  shiny::fluidRow(
    shiny::column(6, ptr_ui(!!plots[[1]], "plot_1", shared = obj)),  # ax1 inline
    shiny::column(6, ptr_ui(!!plots[[2]], "plot_2", shared = obj))   # ax2 inline
  )
)
server <- function(input, output, session) {
  sh <- ptr_shared_server(obj)
  ptr_server(!!plots[[1]], "plot_1", shared_state = sh)
  ptr_server(!!plots[[2]], "plot_2", shared_state = sh)
}
shiny::shinyApp(ui, server)
```

`obj$panel_keys` reports which keys ended up panel-owned — here, `"sz"`. Move the size slider in the panel and click the panel's draw button: both plots re-render in lockstep. Each x-axis picker, being formula-local, changes only its own plot.

The same partition rule means that with a **single** ggpaintr instance, reusing one `shared` key several times in one formula needs no coordinator at all — ggpaintr renders one inline widget and wires every occurrence to it automatically. `ptr_shared()` and the panel are only for the multi-formula case.

This is where the tutorial stops. For tailoring labels and copy, theming, and the trust model behind `ppExpr` and `ppUpload`, see the companion vignettes.
