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.
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 |
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.
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:
┌────────────────────────────┐
│ 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.
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:
A single ID gives one panel filling the whole view:
┌────────────────────────────┐
│ Page [+] │
├────────────────────────────┤
│ │
│ a │
│ │
└────────────────────────────┘
Two top-level entries → two columns split horizontally:
new_dock_board(
blocks = c(a = new_dataset_block(), b = new_head_block()),
layouts = list("a", "b")
)┌────────────────────────────┐
│ Page [+] │
├─────────────┬──────────────┤
│ │ │
│ a │ b │
│ │ │
└─────────────┴──────────────┘
Wrap the entries in one extra layer of
list() to introduce a vertical split:
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.
Use a character vector (not a list) to put multiple panels in the same DockView panel as tabs:
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.
Combine the two rules to build any layout.
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 │
└─────────────┴──────────────┘
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 │
└─────────────┴──────────────┘
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:
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 │
└─────────────┴──────────────┘
Pass a named list. Each named entry becomes a separate page in the view-nav dropdown:
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.
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):
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.
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.
A pot-pourri exercising every feature: multiple views, nested grids, tabbed panels, an extension as a sidebar, and an explicit active view.
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%
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.dock_layout(..., sizes =) runs parallel to the children
passed in .... With two children the layout splits
horizontally; with two sizes it does so unevenly:
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%
Same idea, vertical layout: pass
orientation = "vertical" so the root split runs
top-to-bottom instead of left-to-right.
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%
│ │
└────────────────────────────┘
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:
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 │
│ │
└────────────────────────────┘
group() is the equivalent of an inner
list(...) but with its own sizes. Use it
whenever you need ratios on a non-root branch:
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.
| 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()) |
?dock_layout for the per-view layout type
(including panels() and group()) and
?active_view for switching the active view at runtime.