--- 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 `layouts`. This vignette walks through every shape `layouts` 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` collection. This is a named 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 panel arrangement, stored as a tree of block / extension IDs. Panel content is derived from the board's blocks and extensions on demand — only the arrangement is stored. 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 `layouts =` argument to `new_dock_board()`, the constructor normalises it to a `dock_layouts`: | You pass | Treated as | Final shape | | --- | --- | --- | | A named `list(A = ..., B = ...)` | multi-view (each name is a view) | `dock_layouts` collection | | A `dock_layout` | single page | wrapped as `Page` slot | | A raw list (grid spec) | single-page raw grid, resolved on the way in | wrapped as `Page` slot | ```{r coercion-flow, eval=TRUE, echo=FALSE} blockr.core::include_mermaid("coercion-flow") ``` The dispatch is name-driven: a list with top-level names becomes multi-view (one slot per name); an unnamed list (or a `dock_layout`) is treated as a single-page grid spec. To get multiple views, name the top-level entries. So you only ever need to think about two things: the **grid syntax** for arranging panels inside a view, and **named-list syntax** for having more than one view. # Starting from scratch The simplest possible board: no blocks, no extensions, no `layouts`. Since `new_dock_board()` defaults to `layouts = default_layout(blocks, extensions)` — which returns an empty `dock_layout` here — and the empty layout gets wrapped as a single `Page` view, the resulting collection is a `dock_layouts` with one empty `Page` slot: ```{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 `layouts`, the default `Page` is auto-populated by `default_layout()`: 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 `layouts = ` 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()), layouts = 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()), layouts = 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()), layouts = 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()), layouts = 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() ), layouts = 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() ), layouts = 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() ), layouts = 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(), layouts = list("edit_board_extension", c("a", "b")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────┬──────────────────┤ │ │ ┌────┐┌────┐ │ │ edit │ │ a ││ b │ │ │ │ └────┘└────┘ │ │ │ │ │ │ (tabs) │ └─────────┴──────────────────┘ ``` This is also what `default_layout()` produces when both blocks and extensions are present: extensions on the left, blocks on the right. # Multiple views (pages) Pass a named list. 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(), layouts = list( 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 slot value 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 layout is active. To start on a different one, name it with `new_dock_board(active = )`, using its view id (the `layouts` key): ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), extensions = new_edit_board_extension(), layouts = list( Analysis = list("a", "b"), Editor = dock_layout("edit_board_extension") ), active = "Editor" ) ``` ``` ┌────────────────────────────┐ │ Editor [+] ← starts here│ ├────────────────────────────┤ │ edit │ └────────────────────────────┘ ``` Which view is active is tracked as a single field on the `dock_layouts` collection, keyed by view id — like the id itself, it belongs to the collection, not to any one layout. So it is always exactly one: the id named in `active` (defaulting to the first view). Change it at runtime with `active_view(board) <- id`. ## 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} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), layouts = list( 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") ), layouts = list( Data = dock_layout( "edit_board_extension", panels("raw", "cleaned", active = "cleaned"), sizes = c(0.25, 0.75) ), Analysis = dock_layout( group("summary", "plot1", sizes = c(0.4, 0.6)), "plot2", sizes = c(0.55, 0.45) ), Charts = dock_layout(panels("plot1", "plot2")) ), active = "Charts" ) ``` 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**: a slim extension sidebar on the left holding the editor, and a wide right column with `raw` / `cleaned` tabbed. `active = "cleaned"` opens the cleaned tab by default; `sizes = c(0.25, 0.75)` carves out the narrow sidebar. ``` ┌────────────────────────────┐ │ Data [+] │ ├──────┬─────────────────────┤ │ │┌─────┐┌─────────┐ │ │ edit ││ raw ││ cleaned │ │ │ │└─────┘└─────────┘ │ │ │ (cleaned shown) │ └──────┴─────────────────────┘ 25% 75% ``` **Analysis**: two top-level columns at 55/45. The left column is a nested `group()` with a 40/60 vertical split (summary on top, plot1 below); the right column is a single panel (`plot2`). ``` ┌────────────────────────────┐ │ Analysis [+] │ ├─────────────┬──────────────┤ │ summary │ │ 40% / 55% ├─────────────┤ plot2 │ │ │ │ │ plot1 │ │ 60% / 45% │ │ │ └─────────────┴──────────────┘ 55% 45% ``` # Custom split ratios and active tabs The list-of-IDs sugar splits space evenly and opens the first panel in a tabbed group. For non-default ratios or a non-default open tab, escalate to the typed constructors: * `dock_layout(..., sizes = c(...))` — sizes parallel to the root children. The numbers are relative; they don't have to sum to `1`. * `group(..., sizes = c(...))` — same, for a nested branch (any level below the root). * `panels(..., active = "...")` — tabbed leaf with an explicit open tab. Equivalent to a character-vector child, except you also get to pick the active tab. ## A 30/70 split `dock_layout(..., sizes =)` runs parallel to the children passed in `...`. With two children the layout splits horizontally; with two sizes it does so unevenly: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), layouts = list( Main = dock_layout("a", "b", sizes = c(0.3, 0.7)) ) ) ``` ``` ┌────────────────────────────┐ │ Main [+] │ ├──────────┬─────────────────┤ │ │ │ │ a │ b │ │ │ │ └──────────┴─────────────────┘ 30% 70% ``` ## Stacking with sizes Same idea, vertical layout: pass `orientation = "vertical"` so the root split runs top-to-bottom instead of left-to-right. ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), layouts = list( Main = dock_layout("a", "b", orientation = "vertical", sizes = c(0.25, 0.75)) ) ) ``` ``` ┌────────────────────────────┐ │ Main [+] │ ├────────────────────────────┤ │ a │ 25% ├────────────────────────────┤ │ │ │ b │ 75% │ │ └────────────────────────────┘ ``` ## Choosing the open tab `panels()` builds a tabbed leaf. Without `active`, the first ID opens by default — same behaviour as a bare character vector. Pass `active` to open a different tab on load: ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), layouts = list( Main = dock_layout(panels("a", "b", "c", active = "b")) ) ) ``` ``` ┌────────────────────────────┐ │ Main [+] │ ├────────────────────────────┤ │ ┌────┐┌──────┐┌────┐ │ │ │ a ││ b ││ c │ │ │ └────┘└──────┘└────┘ │ │ ↑ │ │ b is open by default │ │ │ └────────────────────────────┘ ``` ## Sizes inside a nested branch `group()` is the equivalent of an inner `list(...)` but with its own `sizes`. Use it whenever you need ratios on a non-root branch: ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), layouts = list( Main = dock_layout( "a", group("b", "c", sizes = c(0.6, 0.4)), sizes = c(0.3, 0.7) ) ) ) ``` ``` ┌────────────────────────────┐ │ Main [+] │ ├────────┬───────────────────┤ │ │ b │ inner: 60% top │ ├───────────────────┤ │ a │ c │ inner: 40% bottom │ │ │ │ │ │ └────────┴───────────────────┘ 30% 70% ``` Outer `sizes = c(0.3, 0.7)` controls the root split (left / right); inner `group(..., sizes = c(0.6, 0.4))` controls the right column's internal stack. # 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"))` | | Custom split ratio | `dock_layout("a", "b", sizes = c(0.3, 0.7))` | | Custom open tab | `dock_layout(panels("a", "b", active = "b"))` | | Vertical top-level split | `dock_layout("a", "b", orientation = "vertical")` | | Multiple views | `list(A = ..., B = ...)` | | Start on view `B` | `new_dock_board(layouts = list(A = ..., B = ...), active = "B")` | | Empty starter view | `list(Page = list())` | # Where to go from here - See `?dock_layout` for the per-view layout type (including `panels()` and `group()`) and `?active_view` for switching the active view at runtime.