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 regionsleft, 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 — body is the frame the dock relocates as one piece.

The gestures

Every panel has a title bar. Drag it and drop-zone guides appear:

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