--- title: "Views and grids" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Views and grids} %\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`'s arrangement is two independent things, and it takes two constructor arguments to match: * **`views`** --- the *structure*: which panels belong to each view (the global tabs across the top of the app) and the view's display name. One `dock_view` per view. * **`grids`** --- the *geometry*: how each view's panels are split, nested, tabbed and sized. One `dock_grid` per view. This vignette covers the grid syntax you use to describe geometry, how the two slots fit together at construction, and the patterns you'll reach for most. # The big picture A board stores structure and geometry as two aligned slots, keyed by view id --- read them with `board_views()` and `board_grids()`. They are kept apart on purpose: membership is owned by the update lifecycle (add / remove a block), geometry by the client (you drag a sash, the board records it). A panel is *placed* in a view when it is in both slots; the two are reconciled only where the placement is read. There is a third representation you should know the name of but will rarely touch: **`dock_layout`**. It is *dockView's own* form --- the `{grid, panels, activeGroup}` payload the front end renders and echoes back. It is the wire type at the client boundary, not something you author with. Our model is `dock_view` + `dock_grid`; `dock_layout` is what crosses to dockView (`?dock_layout`). The constructor is forgiving about which slot you fill: | You pass | Result | | --- | --- | | `grids` only | one view per grid (id = the list name), members taken from the grid's panels | | `views` only | structure with no explicit geometry; each view renders a flat default over its members | | both | aligned by id: `views` sets membership + names, `grids` sets geometry | | neither | a single default view (sidebar + main when there are extensions) | So for a layout with real geometry you usually just pass `grids`; the views come along for free. Reach for `views` when you want a display name that differs from the id, or a view without an explicit arrangement. # Grid syntax You describe geometry with `dock_grid()` (and its helpers `panels()` and `group()`). Two rules cover everything: 1. **List nesting alternates orientation.** `dock_grid()`'s top level lays its children out horizontally; one level of `list()` nesting flips to vertical; another flips back, and so on. 2. **Character vectors create tabs.** A vector of IDs shares one DockView panel as tabs; a `list()` of IDs is split into separate panels. ## One panel A single ID fills the whole view: ```{r} new_dock_board( blocks = c(a = new_dataset_block()), grids = list(Page = dock_grid("a")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ │ │ a │ │ │ └────────────────────────────┘ ``` The view is named after the grid's list name (`Page`), and its members are that grid's panels (`a`). ## Two panels side by side Two top-level children → two columns split horizontally: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), grids = list(Page = dock_grid("a", "b")) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────────┬──────────────┤ │ │ │ │ a │ b │ │ │ │ └─────────────┴──────────────┘ ``` ## Two panels stacked vertically Wrap the children 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()), grids = list(Page = dock_grid(list("a", "b"))) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ a │ ├────────────────────────────┤ │ b │ └────────────────────────────┘ ``` The outer level still describes a horizontal split, but with a single child that "split" is one full-width column. The inner `list("a", "b")` is at depth 1, so it splits **vertically**: `a` stacks on top of `b`. ## Tabs (multiple panels in one) Use a **character vector** (not a list) to put several panels in one DockView panel as tabs: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), grids = list(Page = dock_grid(c("a", "b"))) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ ┌──────┐┌──────┐ │ │ │ a ││ b │ │ │ └──────┘└──────┘ │ │ │ │ (a is shown, b is a tab) │ │ │ └────────────────────────────┘ ``` `dock_grid(list("a", "b"))` (panels split) and `dock_grid(c("a", "b"))` (panels tabbed) read almost the same 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 arrangement. ## Two columns, the right one stacked ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), grids = list(Page = dock_grid("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() ), grids = list(Page = dock_grid(list("a", "b"), list("c", "d"))) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────────┬──────────────┤ │ a │ c │ ├─────────────┼──────────────┤ │ b │ d │ └─────────────┴──────────────┘ ``` ## Three rows in one column A third level of nesting flips back to horizontal inside the vertical stack --- a row holding two panels side by side: ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), grids = list(Page = dock_grid(list("a", list("b", "c")))) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├────────────────────────────┤ │ a │ ├─────────────┬──────────────┤ │ b │ c │ └─────────────┴──────────────┘ ``` ## Sidebar + tabs A common dashboard shape: a narrow column on the left holding an extension, 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(), grids = list( Page = dock_grid("edit_board", c("a", "b")) ) ) ``` ``` ┌────────────────────────────┐ │ Page [+] │ ├─────────┬──────────────────┤ │ │ ┌────┐┌────┐ │ │ edit │ │ a ││ b │ │ │ │ └────┘└────┘ │ │ │ │ │ │ (tabs) │ └─────────┴──────────────────┘ ``` This is also the shape `new_dock_board()` gives you by default when both blocks and extensions are present, and you pass neither slot: extensions on the left, blocks on the right. # Multiple views (pages) Give `grids` more than one entry; each list name becomes a separate page in the view-nav dropdown, and each becomes its own view: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), extensions = new_edit_board_extension(), grids = list( Analysis = dock_grid("a", "b"), Editor = dock_grid("edit_board") ) ) ``` `Analysis` view (active by default --- the first view wins): ``` ┌────────────────────────────┐ │ Analysis [+] ← view nav │ ├────────────────────────────┤ │ a │ b │ └────────────────────────────┘ ``` Switching to `Editor` via the dropdown: ``` ┌────────────────────────────┐ │ Editor [+] │ ├────────────────────────────┤ │ │ │ edit │ │ │ └────────────────────────────┘ ``` Each grid follows exactly the same syntax as the single-view form: character vectors for tabs, nested lists for splits. ## Display names distinct from the id A grid's list name is the view *id*, and it doubles as the display label. When you want a label that differs from the id --- or a view with no geometry at all --- fill the `views` slot too. A `dock_view()` carries the membership and the display name; align it to the grid by id: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), views = list( main = dock_view(c("a", "b"), name = "Analysis") ), grids = list( main = dock_grid("a", "b") ) ) ``` The nav shows **Analysis**; the stable id `main` is what `active`, renames and updates address. ## Choosing the initially active view The first view is active by default. To start elsewhere, name it with `new_dock_board(active = )`, using its view id: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), extensions = new_edit_board_extension(), grids = list( Analysis = dock_grid("a", "b"), Editor = dock_grid("edit_board") ), active = "Editor" ) ``` ``` ┌────────────────────────────┐ │ Editor [+] ← starts here│ ├────────────────────────────┤ │ edit │ └────────────────────────────┘ ``` Which view is active is a single field on the views collection, keyed by id --- it belongs to the collection, not to any one view, so it is always exactly one. Change it at runtime with `active_view(board) <- id`, and read it back with `active_view(board)`; index `board_grids(board)[[id]]` if you then need that view's geometry. ## Views without geometry Pass `views` alone (bare member vectors coerce to `dock_view`s) for structure with no explicit arrangement. Each such view renders a flat default over its members until the client arranges it (which the board then records): ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), views = list( Analysis = c("a", "b"), Empty = character() ) ) ``` An empty view shows the same watermark prompt as an empty board, scoped to that tab. # Putting it all together A pot-pourri: multiple views, nested grids, tabbed panels, an extension 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") ), grids = list( Data = dock_grid( "edit_board", panels("raw", "cleaned", active = "cleaned"), sizes = c(0.25, 0.75) ), Analysis = dock_grid( group("summary", "plot1", sizes = c(0.4, 0.6)), "plot2", sizes = c(0.55, 0.45) ), Charts = dock_grid(panels("plot1", "plot2")) ), active = "Charts" ) ``` Three views; `Charts` is active, so the user lands there first. **Charts** (active on load): one tabbed panel, `plot1` shown, `plot2` a tab. ``` ┌────────────────────────────┐ │ Charts [+] │ ├────────────────────────────┤ │ ┌───────┐┌───────┐ │ │ │ plot1 ││ plot2 │ │ │ └───────┘└───────┘ │ │ (plot1 shown, plot2 tab) │ └────────────────────────────┘ ``` **Data**: a slim extension sidebar on the left, a wide right column with `raw` / `cleaned` tabbed. `active = "cleaned"` opens the cleaned tab; `sizes = c(0.25, 0.75)` carves out the narrow sidebar. ``` ┌────────────────────────────┐ │ Data [+] │ ├──────┬─────────────────────┤ │ │┌─────┐┌─────────┐ │ │ edit ││ raw ││ cleaned │ │ │ │└─────┘└─────────┘ │ │ │ (cleaned shown) │ └──────┴─────────────────────┘ 25% 75% ``` **Analysis**: two columns at 55/45. The left is a nested `group()` with a 40/60 vertical split (summary over plot1); the right is `plot2`. ``` ┌────────────────────────────┐ │ Analysis [+] │ ├─────────────┬──────────────┤ │ summary │ │ 40% / 55% ├─────────────┤ plot2 │ │ plot1 │ │ 60% / 45% └─────────────┴──────────────┘ 55% 45% ``` # Custom split ratios and active tabs The bare list-of-IDs form splits space evenly and opens the first tab. For non-default ratios or a non-default open tab, use the typed helpers: * `dock_grid(..., sizes = c(...))` --- sizes parallel to the root children. They are relative; they need not sum to `1` (they are normalised for you). * `group(..., sizes = c(...))` --- the same, for a nested branch below the root. * `panels(..., active = "...")` --- a tabbed leaf with an explicit open tab. ## A 30/70 split `sizes` runs parallel to the children in `...`. Two children split horizontally; two sizes make the split uneven: ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), grids = list( Main = dock_grid("a", "b", sizes = c(0.3, 0.7)) ) ) ``` ``` ┌────────────────────────────┐ │ Main [+] │ ├──────────┬─────────────────┤ │ │ │ │ a │ b │ │ │ │ └──────────┴─────────────────┘ 30% 70% ``` ## Stacking with sizes Same idea, vertical: `orientation = "vertical"` makes the root split run top-to-bottom. ```{r} new_dock_board( blocks = c(a = new_dataset_block(), b = new_head_block()), grids = list( Main = dock_grid("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 --- same as a bare character vector. Pass `active` to open a different tab: ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), grids = list( Main = dock_grid(panels("a", "b", "c", active = "b")) ) ) ``` ``` ┌────────────────────────────┐ │ Main [+] │ ├────────────────────────────┤ │ ┌────┐┌──────┐┌────┐ │ │ │ a ││ b ││ c │ │ │ └────┘└──────┘└────┘ │ │ ↑ │ │ b is open by default │ └────────────────────────────┘ ``` ## Sizes inside a nested branch `group()` is an inner `list(...)` with its own `sizes` --- use it for ratios on any non-root branch: ```{r} new_dock_board( blocks = c( a = new_dataset_block(), b = new_head_block(), c = new_head_block() ), grids = list( Main = dock_grid( "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; inner `group(..., sizes = c(0.6, 0.4))` controls the right column's stack. # Cheat-sheet Geometry (`dock_grid(...)`): | Goal | Syntax | | --- | --- | | One panel | `dock_grid("a")` | | Two side-by-side | `dock_grid("a", "b")` | | Two stacked | `dock_grid(list("a", "b"))` | | Tabbed panel | `dock_grid(c("a", "b"))` | | Sidebar + main | `dock_grid("ext", "main")` | | Two columns, both stacked | `dock_grid(list("a", "b"), list("c", "d"))` | | Custom split ratio | `dock_grid("a", "b", sizes = c(0.3, 0.7))` | | Custom open tab | `dock_grid(panels("a", "b", active = "b"))` | | Vertical top-level split | `dock_grid("a", "b", orientation = "vertical")` | Board (`new_dock_board(...)`): | Goal | Syntax | | --- | --- | | One view with geometry | `grids = list(Page = dock_grid(...))` | | Several views | `grids = list(A = dock_grid(...), B = dock_grid(...))` | | A display name != id | `views = list(id = dock_view(members, name = "..."))` | | Structure, no geometry | `views = list(Page = c("a", "b"))` | | Start on view `B` | `new_dock_board(grids = list(A = ..., B = ...), active = "B")` | | Empty starter view | `views = list(Page = character())` | # Where to go from here - `?dock_grid` for the geometry syntax (with `panels()` and `group()`). - `?dock_view` for the structure type, and `?active_view` for switching the active view at runtime. - `?dock_layout` for the dockView boundary form, and `as_dock_grid()` / `as_dock_layout()` for the casts across it.