Docking — a dockable-panel workbench
The DockManager gives your app free docking: an IDE-style
workbench of panels the user can drag apart, tab together, float into their own OS
windows, and redock — at any nesting depth, with every panel's content preserved across
the move. It's the kind of layout you know from Visual Studio, VS Code and Lazarus, here
in plain JavaScript on the native Player.
A first workbench
const dock = app.DockManager();
dock.pack.configure({ fill: 'both', expand: true });
const files = dock.panel({ title: 'Files', region: 'left' });
const editor = dock.panel({ title: 'Editor', region: 'center' });
const props = dock.panel({ title: 'Properties', region: 'right' });
const output = dock.panel({ title: 'Output', region: 'bottom' });
// Each panel exposes a `body` frame — fill it with your widgets:
editor.body.Notepad().pack.configure({ fill: 'both', expand: true });
files.body.Treeview({ columns: ['name'] }).pack.configure({ fill: 'both', expand: true });
The five regions — left, right, top, bottom, center — arrange into the
familiar cross: edge panels frame a center fill. Regions are a convenience; once panels
exist the user rearranges them freely, and you can drive the same operations from code.
Put your content in
panel.body, never in the panel itself —bodyis the frame the dock relocates as one piece.
The gestures
Every panel has a title bar. Drag it and drop-zone guides appear:
- Onto another panel's edge → split: the target cell divides in two, side by side (left/right) or stacked (top/bottom). Split again inside either half for arbitrary nesting.
- Onto another panel's center → tab: the two become a tab group (a notebook). Drop onto an existing notebook's edges to place the new tab before / after the current one, or first / last.
- To a window-edge rail → full-span dock: a full-height column (left/right) or full-width band (top/bottom) spanning the whole window — this also promotes a panel back out of a nest.
- Off the window → float: the panel becomes its own OS window. Drag its title bar back to any rail to redock.
Double-click a title bar to maximize the panel (and again to restore). Right-click it for a context menu — Float / Dock / Close / Maximize / Move, gated by the panel's options.
Through all of this the panel's live state — text selection, scroll position, a half-typed entry, focus — is untouched: the dock shows each panel wherever the layout puts it without ever rebuilding it.
Driving it from code
The same operations are public methods, so you can build toolbar buttons, restore a saved workspace, or script a default layout:
props.float('320x400+900+120'); // pop Properties out to its own window
props.dock('right'); // …and send it back
editor.tabWith(output); // group them as tabs
files.close(); // hide (still re-openable)
dock.splitPanel(editor, dock.panel({ title: 'Diff' }), 'right'); // split a new panel in
The tree model
Under the sugar, a DockManager is a recursive tree of three node kinds — a
split (paned window), a tabs group (notebook), or a panel leaf. That's
exactly what saveTree() emits:
dock.saveTree();
// { type: 'split', orient: 'horizontal', sizes: [220, 900], children: [
// { type: 'panel', key: 'Files' },
// { type: 'split', orient: 'vertical', children: [
// { type: 'panel', key: 'Editor' },
// { type: 'tabs', active: 'Output', keys: ['Output', 'Diff'] } ] } ] }
For custom docking UIs you can operate on the tree directly with glue (dock a full-span
band on the whole tree's edge), treeSplit, treeTab and treeRemove — see the
API reference. Edge precedence is glue-order:
the most recent edge band is outermost.
Sizing
dock.regionSize('left', 240); // pixels
dock.regionSize('left', { chars: 32 }); // 32 characters wide — scales with the font (DPI-safe)
dock.panel({ title: 'Nav', region: 'left', size: 200, minSize: 160 });
A minSize (explicit, or a content-derived floor) is enforced: the user can't sash a
panel to nothing, and a persisted sub-min size never reloads a panel invisible.
Saving & restoring a layout
saveLayout() captures the whole arrangement — structure, sash sizes, floating windows,
and closed panels — as a plain JSON value. loadLayout() rebuilds it, matching panels by
their key. Because the app owns each panel's content, live state is inherently
preserved across a save/load.
dock.onLayoutChange = () => sw.fs.writeFile('layout.json', JSON.stringify(dock.saveLayout()));
// on startup, after creating the panels:
try {
dock.loadLayout(JSON.parse(sw.fs.readFile('layout.json')));
} catch (e) {
/* first run — keep the default layout */
}
onLayoutChange coalesces a single user action into one notification and is suppressed
while loadLayout restores, so a save-on-change handler won't loop.
See also
DockManagerAPI reference- The
apps/docking/main.jsshowcase — a full interactive workbench - Drag & drop · Layout