LayoutEngine
The framework-agnostic engine for one layout - measures, positions panels, owns moveable elements, dispatches actions.
"The framework-agnostic layout engine for one ModelLayout. It implements the model's
layout-controller contract, measures the elements an adapter registers and writes their rects
into the model, positions tab panels over their tabset's content area (structural style only),
owns the moveable elements that host tab content, dispatches actions through onAction, and
tells adapters when to re-render through a revision counter."
In React you never create one: Dockable.Root does, and useDockable() gives it to you. Most
apps use three methods: doAction, sync and (rarely) isSupportsPopout.
const { engine } = useDockable();
engine.doAction(Actions.selectTab("t1"));For apps
| method | description |
|---|---|
of(model) (static: LayoutEngine.of(model)) | the engine driving model's main layout, once a view created it (a mounted Dockable.Root), else undefined. UI outside the layout uses it to dispatch through onAction |
doAction(action) | dispatches an action through onAction, which may replace or veto it. Returns the added node for addTab |
interceptAction(action) | runs onAction on an action without applying it: returns the action to apply (the same one, or the handler's replacement), or undefined when vetoed. doAction is interceptAction followed by model.doAction |
sync() | runs the measure-and-position cycle: measures every registered element into the model, positions the tab panels and discovers the splitter size. The primitives call it after every commit; call it yourself when something moved the tabsets without resizing any element (for example flipping dir at runtime, gap 4) |
getModel() | the model |
getLayoutId(), getLayout(), isMainLayout() | the layout this engine drives |
getMainEngine() | the main layout's engine (for a popout's engine) |
isSupportsPopout() | whether window layouts open as native popouts |
isInWindow(node) | whether a tab or tabset lives in a popout window's layout |
canPopout(node) | whether a tab (or a whole tabset) can be popped out now: popouts supported, not in a window already, and enablePopout on it (every tab of it) |
popout(node) | pops a tab or a whole tabset out into a window, through onAction |
dockBack(node) | moves a tab (or every tab of a tabset) from a window into the main layout's active tabset (else its first); emptying a window closes it |
isRealtimeResize() | the realtimeResize option |
isSplitterDragging() | true while any splitter of the model is being dragged |
getTabDragSpeed() | seconds a view may take to animate the drop outline |
registerTabList(container, element, vertical?) | registers (or, with null, unregisters) a tabset's or border's tab list for tab overflow: the tabs that do not fit are hidden, keeping the selected one (Dockable.TabList does) |
registerOverflowTrigger(container, element) | registers the overflow trigger beside a tab list: the space it takes is reserved in the strip (Dockable.TabOverflowTrigger does) |
getHiddenTabs(containerId) | the ids of a container's tabs hidden by tab overflow, in model order (the same array until it changes) |
subscribeOverflow(listener) | calls listener when a container's hidden tabs change; returns the unsubscribe function |
handleOverlayPointerDown(event) | call on every pointerdown of the document (capture phase): a press in the main layout's area outside an open overlay border's panel closes it (Dockable.Root does). Returns true when a panel closed |
handleOverlayKeyDown(event, key) | call on every keydown with the close key (keyMap.closeOverlayBorder): with focus in an open overlay panel or on its tab button, closes it and focuses the tab button (Dockable.Root does) |
closeOverlayBorder(border) | closes a border's panel (deselects its tab through onAction); focus in the panel goes back to the tab button |
focusAdjacentTabset(delta) | moves focus to the selected tab button of the next (1) or previous (-1) tabset in this layout (wrapping); the target also becomes the active tabset. Returns true when focus moved |
getPopoutManager() | the popout windows of the model (owned by the main engine) |
getDragDropManager() | the drag-and-drop state machine of this layout (addTabWithDragAndDrop(event, json, onDrop?, dragImage?) starts a new-tab drag from any element) |
getOnExternalDrag() | the onExternalDrag handler (set on the main engine) |
getDragGroup() | the drag group this layout exchanges tabs in (the main engine's), if any |
registerDropZone(element, options) | makes element (anywhere in the document) a drop zone for this model's drags: { accepts?, onDrop, onOverChange? }. While a drag the zone accepts is over it, the layouts hide their outline; a drop calls onDrop with the dragged node instead of moving it. Returns the unregister function |
getCurrentDocument(), getCurrentWindow(), getWindowId() | the document and window the layout renders in |
getLayoutRef() | the root element, once attached |
For adapters
The adapter's side of the cycle, as the engine documents it:
- call
prepare()before rendering a layout (computes paths and min/max sizes); - render the structure, registering elements with
registerMeasurable,registerTabPanelandregisterSplitter; - call
sync()after every commit (a layout effect in React); - re-render when the revision from
subscribe/getSnapshotchanges.
"The engine never renders geometry through the adapter: panel positions are written directly, so a resize or splitter drag does not require a re-render."
| method | description |
|---|---|
createLayoutEngine(options) | creates a LayoutEngine for a layout of model (the main layout by default) |
setOptions(options) | updates the callbacks and options an adapter passes on every render (onAction, onModelChange, realtimeResize, tabDragSpeed, popout) |
subscribe(listener) / getSnapshot() | the render revision: a number that changes whenever adapters should re-render (bound, for useSyncExternalStore) |
getContentRevision() | a revision that only changes when tab content should re-render too |
prepare(path?) | computes the data-layout-path of every node and the min/max sizes; call before rendering |
attachRoot(element) / detachRoot() | attaches the layout's root element (the containing block the panels are positioned in) and starts observing it and its window |
registerMeasurable(node, kind, element) | registers (or, with null, unregisters) an element whose geometry feeds the model; kind is "row", "tabset", "tabstrip", "tabsetcontent", "tabbutton", … |
registerTabPanel(node, element) | registers the element a tab's content panel is positioned with |
registerSplitter(element, isHorizontal, register?) | registers a splitter element; its thickness becomes the model's splitter size |
getMoveableElement(tab) | the element hosting the tab's content, created on first use |
attachMoveable(tab, panel) | moves the tab's moveable element into panel; the same element is re-parented, never cloned |
releaseMoveable(tab, panel?) | parks the moveable element in the hidden moveables home when its panel goes away |
createMoveableElement() | creates the element that hosts a tab's content: <div data-dockable-moveable> in the main layout's document, with structural sizing only. Called by the model, lazily |
getMoveablesHome() | the hidden element moveables are parked in (main layout only) |
syncLayoutMetrics() | batch-measures all registered elements and writes rects into the model; returns true on change (part of sync()) |
positionTabPanels() | positions the tab panels over their parent's content area and sets their visibility (part of sync()) |
updateRect() | re-measures the layout root; a changed size relayouts |
setSplitterDragging(dragging) | marks a splitter drag in progress (used by the splitter controller) |
getBoundingClientRect(element) | an element's rect relative to the layout root |
getDomRect() / getFreshDomRect() | the layout root's rect in viewport coordinates (cached per pass / measured now) |
getScreenRect(rect) | a layout-relative rect in screen coordinates (for opening popout windows) |
getRelativeRect(rect) | a sub-layout-relative rect relative to the main layout |
redrawLayout() / redrawLayoutAndTabContent() | asks adapters to re-render the structure (and the content) |
dispose() | detaches and forgets everything |
Options
ILayoutEngineOptions:
| option | description |
|---|---|
model | the model |
layoutId | the layout this engine drives; defaults to the main layout |
onAction | OnAction: return the action (or a replacement) to apply it, or undefined to veto it |
onModelChange | OnModelChange: called after the model applied an action |
mainEngine | the main layout's engine, for engines of popout sub-layouts |
measure | injectable element measurement (tests pass fixed rects); defaults to getBoundingClientRect |
realtimeResize | true (default) to resize live while a splitter is dragged; false to preview and commit on release |
tabDragSpeed | seconds a view may take to animate the drop outline (default 0.3) |
onExternalDrag | OnExternalDrag: accepts foreign drags (files, links, other libraries) as new tabs (main engine only; popout layouts use the main engine's) |
onAllowDrop | OnAllowDrop: whether a drag may drop at a target, like model.setOnAllowDrop, which it sets while given (main engine only); removing it restores the model's previous rule |
dragGroup | DragGroup: layouts of other models this layout exchanges tabs with by drag and drop (main engine only). Without one, drags never cross models |
popout | popout windows (main engine only): popoutURL, supportsPopout, closePolicy, title, onPopoutOpen, onPopoutClose, mirrorRoot (true or a list of <html>/<body> attribute names to mirror into each popout) |
Helpers exported with it
| export | description |
|---|---|
isTabPanelVisible(tab) | whether a tab's panel is shown: selected, and not hidden by a maximized tabset or a hidden border |
MOVEABLE_ATTRIBUTE | "data-dockable-moveable", the attribute on the element hosting a tab's content |
POPOUT_ATTRIBUTE | "data-dockable-popout", the attribute on a popout's content root |
The core never touches the global document or window: all DOM access goes through the root
element's ownerDocument and defaultView, which is why the same engine runs inside a popout
window.