--- title: "Defining dock layouts" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Defining dock layouts} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include=FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ``` ```{r setup} library(blockr.core) library(blockr.dock) ``` A `dock_board` exposes a single argument that controls panel arrangement: the `layout`. This vignette walks through every shape `layout` accepts, shows what UI each shape produces, and ends with patterns for the layouts you'll build most often. # The big picture Internally, every board carries a `dock_layouts()` object. This is a list of one or more **views** (the global tabs you see at the top of the app). A view's content is a `dock_layout`: the per-view DockView grid that arranges block and extension **panels**. Single-page boards are the degenerate case: a `dock_layouts` with one auto-named view called `"Page"`. The view-nav dropdown is always present; it just has one entry until you add more. When you pass a `layout =` argument to `new_dock_board()`, the constructor normalises it to a `dock_layouts`: | You pass | Treated as | Final shape | | --- | --- | --- | | `dock_layouts(...)` | multi-view, as-is | itself | | A `dock_layout` (resolved via `create_dock_layout()`) | single page | `dock_layouts(Page = ...)` | | A raw list (grid spec) | single-page raw grid | `dock_layouts(Page = ...)` | ```{r coercion-flow, eval=TRUE, echo=FALSE} blockr.core::include_mermaid("coercion-flow") ``` The catch-all bottom branch covers anything else: a raw nested list, a top-level character vector, even a named list of lists. **Names at the top level are dropped** there; to get multiple views you must use `dock_layouts(...)` explicitly. So you only ever need to think about two things: the **grid syntax** for arranging panels inside a view, and the **`dock_layouts()` syntax** for having more than one view. # Starting from scratch The simplest possible board: no blocks, no extensions, no `layout`. Since, `new_dock_layout()` defaults to `dock_layouts(Page = default_view_grid(blocks, extensions))`, and `default_view_grid()` returns an empty list when there are no blocks or extensions, the resulting layout is `dock_layouts(Page = list())`: ```{r} new_dock_board() ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ │ │ ⊕ │ │ │ │ Start by adding a panel │ │ │ │ [ Add panel ] │ │ │ └────────────────────────────┘ ``` You get a single auto-named `Page` view holding the watermark prompt. Clicking **Add panel** would normally open the panel picker, but here the board has no blocks or extensions to choose from, so the picker says so and points you to add a block first. From this empty starting point you can grow the board interactively. When you do supply blocks or extensions but still no `layout`, the default `Page` is auto-populated by `default_grid()`: blocks alone become a single row of panels; blocks together with extensions become the familiar sidebar-and-main shape (extensions on the left, blocks on the right). Pass `layout = ` only to override that. # Single-page raw grid The simplest `layout`: a list (or character vector) of block/extension IDs. The shape of the list determines the grid. The two rules to internalise: 1. **List nesting alternates orientation.** The top level lays its children out horizontally; one level of nesting flips to vertical; another level flips back to horizontal, and so on. 2. **Character vectors create tabs.** A vector of IDs lives inside one DockView panel; a list of IDs gets split into multiple panels. ## One panel A single ID gives one panel filling the whole view: ```{r} new_dock_board( blocks = c(a = new_dataset_block()), layout = list("a") ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ │ │ a │ │ │ └────────────────────────────┘ ``` ## Two panels side by side Two top-level entries → two columns split horizontally: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), layout = list("a", "b") ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────────┬──────────────┤ │ │ │ │ a │ b │ │ │ │ └─────────────┴──────────────┘ ``` ## Two panels stacked vertically Wrap the entries in **one extra layer** of `list()` to introduce a vertical split: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), layout = list(list("a", "b")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ a │ ├────────────────────────────┤ │ b │ └────────────────────────────┘ ``` The outer list still describes a horizontal split, but with only one child that "split" is a single full-width column. The inner `list("a", "b")` is at depth 1, so it splits **vertically**: `a` stacks on top of `b`. ## Tabs (multiple views in one panel) Use a **character vector** (not a list) to put multiple panels in the same DockView panel as tabs: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), layout = list(c("a", "b")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ ┌──────┐┌──────┐ │ │ │ a ││ b │ │ │ └──────┘└──────┘ │ │ │ │ (a is shown, b is a tab) │ │ │ └────────────────────────────┘ ``` `list("a", "b")` (panels split) and `list(c("a", "b"))` (panels tabbed) look almost identical in source but produce very different UIs. The list/vector distinction flips between *split a panel* and *tabify a panel*. # Nested grids Combine the two rules to build any layout. ## Two columns, the right one stacked ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), layout = list("a", list("b", "c")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────────┬──────────────┤ │ │ b │ │ a ├──────────────┤ │ │ c │ └─────────────┴──────────────┘ ``` ## Two columns, both stacked ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block(), d = new_head_block() ), layout = list(list("a", "b"), list("c", "d")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────────┬──────────────┤ │ a │ c │ ├─────────────┼──────────────┤ │ b │ d │ └─────────────┴──────────────┘ ``` ## Three rows in one column Add a third level of nesting to flip back to horizontal inside the vertical stack. Useful when you want a row that holds two panels side-by-side: ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), layout = list(list("a", list("b", "c"))) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ a │ ├─────────────┬──────────────┤ │ b │ c │ └─────────────┴──────────────┘ ``` ## Sidebar + tabs A common dashboard shape: a narrow column on the left holding an extension, and a tabbed panel on the right with several blocks: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), extensions = new_edit_board_extension(), layout = list("edit_board_extension", c("a", "b")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────┬──────────────────┤ │ │ ┌────┐┌────┐ │ │ edit │ │ a ││ b │ │ │ │ └────┘└────┘ │ │ │ │ │ │ (tabs) │ └─────────┴──────────────────┘ ``` This is also what `default_grid()` produces when both blocks and extensions are present: extensions on the left, blocks on the right. # Multiple views (pages) Wrap the per-view grids in `dock_layouts()`. Each named entry becomes a separate page in the view-nav dropdown: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), extensions = new_edit_board_extension(), layout = dock_layouts( Analysis = list("a", "b"), Editor = list("edit_board_extension") ) ) ``` `Analysis` view (active by default, since the first view wins): ``` ┌────────────────────────────┐ │ Analysis [+] ← view nav │ ├────────────────────────────┤ │ a │ b │ └────────────────────────────┘ ``` Switching to `Editor` via the dropdown: ``` ┌────────────────────────────┐ │ Editor [+] │ ├────────────────────────────┤ │ │ │ edit │ │ │ └────────────────────────────┘ ``` Each value inside `dock_layouts()` follows exactly the same grid syntax as the single-page form: character vectors for tabs, nested lists for splits. ## Choosing the initially active view By default, the first view is active. To start on a different one, mark its spec with the `active` attribute. The convenient way is `dock_view()`: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), extensions = new_edit_board_extension(), layout = dock_layouts( Analysis = list("a", "b"), Editor = dock_view("edit_board_extension", active = TRUE) ) ) ``` ``` ┌────────────────────────────┐ │ Editor [+] ← starts here│ ├────────────────────────────┤ │ edit │ └────────────────────────────┘ ``` `dock_view(..., active = TRUE)` is a thin wrapper that calls `structure(list(...), active = TRUE)` under the hood. For one-off cases you can also write the attribute directly: ```{r} overview <- list("a") attr(overview, "active") <- TRUE dock_layouts( Analysis = list("a", "b"), Overview = overview ) ``` The validator rejects more than one view marked `active`. If none is marked, the first view wins. ## Empty views A view with no panels is fine. Switching to it shows the same watermark prompt as the empty default board (see `Starting from scratch`), scoped to that tab. ```{r} dock_layouts( Analysis = list("a", "b"), Empty = list() ) ``` # Putting it all together A pot-pourri exercising every feature: multiple views, nested grids, tabbed panels, an extension as a sidebar, and an explicit active view. ```{r} new_dock_board( blocks = c( raw = new_dataset_block(), cleaned = new_head_block(), summary = new_head_block(), plot1 = new_scatter_block(), plot2 = new_scatter_block() ), extensions = new_edit_board_extension(), links = list( new_link("raw", "cleaned", "data"), new_link("cleaned", "summary", "data"), new_link("cleaned", "plot1", "data"), new_link("cleaned", "plot2", "data") ), layout = dock_layouts( Data = list("edit_board_extension", c("raw", "cleaned")), Analysis = list(list("summary", "plot1"), "plot2"), Charts = dock_view(c("plot1", "plot2"), active = TRUE) ) ) ``` The board has three views. `Charts` is marked active, so the user lands there first. **Charts** (active on load): one tabbed panel with `plot1` shown and `plot2` selectable as a tab. ``` ┌────────────────────────────┐ │ Charts [+] │ ├────────────────────────────┤ │ ┌───────┐┌───────┐ │ │ │ plot1 ││ plot2 │ │ │ └───────┘└───────┘ │ │ │ │ (plot1 shown, plot2 tab) │ │ │ └────────────────────────────┘ ``` **Data**: extension on the left, two dataset blocks tabbed on the right. Same pattern as the "Sidebar + tabs" example earlier, just inside a named view. ``` ┌────────────────────────────┐ │ Data [+] │ ├─────────┬──────────────────┤ │ │ ┌─────┐┌────────┐│ │ edit │ │ raw ││cleaned ││ │ │ └─────┘└────────┘│ │ │ │ │ │ (data tabs) │ └─────────┴──────────────────┘ ``` **Analysis**: two top-level columns. The left column is a vertical stack (`list("summary", "plot1")`) and the right column is a single panel (`"plot2"`). ``` ┌────────────────────────────┐ │ Analysis [+] │ ├─────────────┬──────────────┤ │ summary │ │ ├─────────────┤ plot2 │ │ plot1 │ │ └─────────────┴──────────────┘ ``` # Cheat-sheet | Goal | Syntax | | --- | --- | | One panel | `list("a")` | | Two side-by-side panels | `list("a", "b")` | | Two stacked panels | `list(list("a", "b"))` | | Tabbed single panel | `list(c("a", "b"))` | | Sidebar + main | `list("ext", "main")` | | Two columns, both stacked | `list(list("a", "b"), list("c", "d"))` | | Multiple views | `dock_layouts(A = ..., B = ...)` | | Start on view `B` | `dock_layouts(A = ..., B = dock_view(..., active = TRUE))` | | Empty starter view | `dock_layouts(Page = list())` (or just `dock_layouts()`) | # Where to go from here - See `?dock_layouts` and `?dock_view` for the reference docs of the multi-view API. - See `?create_dock_layout` for fine-grained control over how a single view's grid is built.