8.19.9. Docking

ImGui’s native docking lets the user grab a window’s tab and drop it into a side pane, the bottom pane, or even pop it out to a free-floating window — all preserved across runs through imgui.ini. The dasImgui boost layer wraps the C++ surface in three macros: dockspace (the full-viewport dockable region), the sibling dockspace_in_window (an explicit DockSpace nested inside a host window(HOST, …)), and dock_window (each dockable panel inside either dockspace). A small DockBuilder helper seeds an initial layout so first-run users see something meaningful before they start dragging tabs around.

Source: modules/dasImgui/examples/tutorial/docking.das.

8.19.9.1. Walkthrough

The recording is voiced and self-verifying, and it docks the way a user does — no programmatic shortcuts. It drives two REAL synthetic mouse drags: it grabs the node splitter between Explorer and Source and drags it to widen the left pane, then grabs Output’s tab and drags it onto Source to stack the two as tabs. The synthetic mouse drives ImGui’s SplitterBehavior and its window-move + dock-preview + drop path exactly like a hand on the mouse would. Each drag asserts the layout actually moved (the resized pane’s size changed; the re-docked panel’s dock_id changed); a no-op drag aborts the recording at teardown rather than shipping a clip where nothing happened.

  1options gen2
  2options _comment_hygiene = true
  3options gc
  4
  5require imgui
  6require imgui_app
  7require opengl/opengl_boost
  8require live/glfw_live
  9require live/live_api
 10require live/live_commands
 11require live/live_vars
 12require live_host
 13require imgui/imgui_live
 14require imgui/imgui_boost_runtime
 15require imgui/imgui_boost_v2
 16require imgui/imgui_widgets_builtin
 17require imgui/imgui_docking_builtin
 18require imgui/imgui_visual_aids
 19
 20// =============================================================================
 21// TUTORIAL: docking — full-viewport dockspace + draggable Begin/End windows.
 22//
 23// Three things compose to make dockable UI:
 24//   1. `io.ConfigFlags |= ImGuiConfigFlags.DockingEnable` in `init()` — turns
 25//      on ImGui's docking machinery for the whole session.
 26//   2. `dockspace(DOCK_ROOT, (flags=...))` in the frame loop — wraps
 27//      DockSpaceOverViewport so the whole window becomes a dock target, and
 28//      captures the returned `state.dock_id` for the layout helper to use.
 29//   3. `dock_window(NAME, (text=..., closable=..., flags=...))` per panel —
 30//      Begin/End-wrapped windows that ImGui dynamically docks/undocks based
 31//      on the user dragging their tab.
 32//
 33// The DockBuilder helper (`setup_default_layout`) seeds a 3-pane layout on
 34// the first frame so the user sees an arranged UI without having to drag
 35// tabs manually. After the first run, dock state persists via ImGui's
 36// `imgui.ini` — drag a tab anywhere and the layout sticks.
 37//
 38// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/docking.das
 39// LIVE:       daslang-live modules/dasImgui/examples/tutorial/docking.das
 40//
 41// DRIVE (when running live):
 42//   curl -X POST -d '{"name":"imgui_undock","args":{"target":"DOCK_ROOT/OUTPUT"}}'    localhost:9090/command
 43//   curl -X POST -d '{"name":"imgui_set_window_pos","args":{"target":"DOCK_ROOT/OUTPUT","value":{"x":580,"y":220,"w":360,"h":220}}}' localhost:9090/command
 44//   curl -X POST -d '{"name":"imgui_dock_reset","args":{"target":"DOCK_ROOT"}}'        localhost:9090/command
 45//   curl -X POST -d '{"name":"imgui_close","args":{"target":"DOCK_ROOT/OUTPUT"}}'      localhost:9090/command
 46//
 47// Note: dockspace is a [container] that pushes its identifier onto the registry
 48// path, so dock_window targets are path-qualified (``DOCK_ROOT/<name>``).
 49// =============================================================================
 50
 51def setup_default_layout(dock_id : uint) {
 52    //! Seed a 3-pane layout on the first frame: Explorer (left, full height) |
 53    //! Source over Output (right column, split). Each pane's window-title string
 54    //! MUST match its dock_window's `text=` — that's how ImGui ties windows to nodes.
 55    DockBuilderRemoveNode(dock_id)
 56    DockBuilderAddDockSpaceNode(dock_id, ImGuiDockNodeFlags.None)
 57    let vp = GetMainViewport()
 58    DockBuilderSetNodeSize(dock_id, ImVec2(vp.Size.x, vp.Size.y))
 59    var left_id : uint = 0u
 60    var right_id : uint = 0u
 61    DockBuilderSplitNode(dock_id, ImGuiDir.Left, 0.25f, left_id, right_id)
 62    var top_id : uint = 0u
 63    var bottom_id : uint = 0u
 64    DockBuilderSplitNode(right_id, ImGuiDir.Up, 0.6f, top_id, bottom_id)
 65    DockBuilderDockWindow("Explorer", left_id)
 66    DockBuilderDockWindow("Source",   top_id)
 67    DockBuilderDockWindow("Output",   bottom_id)
 68    DockBuilderFinish(dock_id)
 69}
 70
 71[export]
 72def init() {
 73    live_create_window("dasImgui docking tutorial", 1024, 720)
 74    live_imgui_init(live_window)
 75    // Docking layout persists to imgui.ini; disable that so the recording always
 76    // starts from setup_default_layout's seeded 3-pane arrangement (a leftover
 77    // ini from a prior run would otherwise override the seed non-deterministically).
 78    DisableIniPersistence()
 79    var io & = unsafe(GetIO())
 80    GetStyle().FontScaleMain = 1.5
 81    // The single flag that turns docking on for the whole session. Without
 82    // this, DockSpace() / DockSpaceOverViewport() render nothing.
 83    io.ConfigFlags |= ImGuiConfigFlags.DockingEnable
 84}
 85
 86[export]
 87def update() {
 88    if (!live_begin_frame()) return
 89    begin_frame()
 90
 91    ImGui_ImplGlfw_NewFrame()
 92    apply_synth_io_override()
 93    NewFrame()
 94
 95    // Full-viewport dockspace. PassthruCentralNode lets the OS-window
 96    // background show through the unsplit center (irrelevant here — we
 97    // split everything — but it's the conventional default).
 98    dockspace(DOCK_ROOT, (flags = ImGuiDockNodeFlags.PassthruCentralNode)) {
 99        // Seed the layout once per session. has_initial_layout flips false on
100        // imgui_dock_reset; the renderer then re-runs setup. dock_id is
101        // captured by the dockspace wrapper before this block runs.
102        if (!DOCK_ROOT.has_initial_layout && DOCK_ROOT.dock_id != 0u) {
103            setup_default_layout(DOCK_ROOT.dock_id)
104            DOCK_ROOT.has_initial_layout = true
105        }
106        dock_window(EXPLORER, (text = "Explorer", closable = false,
107                               flags = ImGuiWindowFlags.None)) {
108            text("Files")
109            button(REFRESH_BTN, (text = "Refresh"))
110        }
111        dock_window(SOURCE, (text = "Source", closable = false,
112                             flags = ImGuiWindowFlags.None)) {
113            text("// drag tabs to rearrange")
114            text("// - layout sticks via imgui.ini")
115            button(SAVE_BTN, (text = "Save"))
116        }
117        dock_window(OUTPUT, (text = "Output", closable = true,
118                             flags = ImGuiWindowFlags.None)) {
119            text("> ready.")
120            button(CLEAR_BTN, (text = "Clear"))
121        }
122    }
123
124    end_of_frame()
125    Render()
126    var w, h : int
127    live_get_framebuffer_size(w, h)
128    glViewport(0, 0, w, h)
129    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
130    glClear(GL_COLOR_BUFFER_BIT)
131    live_imgui_render()
132
133    live_end_frame()
134}
135
136[export]
137def shutdown() {
138    live_imgui_shutdown()
139    live_destroy_window()
140}
141
142[export]
143def main() {
144    init()
145    while (!exit_requested()) {
146        update()
147        maybe_collect_gc()
148    }
149    shutdown()
150}

8.19.9.1.1. Requires

Same backend + boost layer as Layout, but layout helpers are replaced by the docking module:

  • imgui/imgui_docking_builtin — the dockspace, dockspace_in_window, and dock_window macros, plus the DockBuilder* bindings cherry-picked from ImGui’s internal API.

8.19.9.1.2. The flag that lights it up

ImGui docking is gated by a single io flag. Without it, DockSpace calls render nothing and dock_window panels behave like ordinary windows:

var io & = unsafe(GetIO())
io.ConfigFlags |= ImGuiConfigFlags.DockingEnable

This goes in init() once per session.

8.19.9.1.3. Seeding the layout

ImGui will happily start with every dockable window stacked in a single tab-bar — the user is expected to drag tabs into place. For a tutorial we ship a default arrangement via DockBuilder:

def setup_default_layout(dock_id : uint) {
    DockBuilderRemoveNode(dock_id)                      // clear any prior state
    DockBuilderAddDockSpaceNode(dock_id, ImGuiDockNodeFlags.None)
    let vp = GetMainViewport()
    DockBuilderSetNodeSize(dock_id, ImVec2(vp.Size.x, vp.Size.y))
    var left_id, right_id : uint
    DockBuilderSplitNode(dock_id, ImGuiDir.Left, 0.25f, left_id, right_id)
    var top_id, bottom_id : uint
    DockBuilderSplitNode(right_id, ImGuiDir.Up, 0.6f, top_id, bottom_id)
    DockBuilderDockWindow("Explorer", left_id)
    DockBuilderDockWindow("Source",   top_id)
    DockBuilderDockWindow("Output",   bottom_id)
    DockBuilderFinish(dock_id)
}

Call it from inside the dockspace block with state.dock_id — the wrapper captures that id from DockSpaceOverViewport before the block runs.

DockBuilderDockWindow matches by window title string — the same string you pass to dock_window(NAME, (text = "Explorer")). The boost macro doesn’t auto-derive the title from the identifier so the binding is explicit.

The setup is gated on state.has_initial_layout so it runs once per session — or after imgui_dock_reset flips the flag back to false.

8.19.9.1.4. Variant: dockspace inside a host window

dockspace wraps DockSpaceOverViewport — the dock region claims the entire OS window. The sibling dockspace_in_window wraps the explicit DockSpace(id, size, flags, null) call so the dock node lives INSIDE an enclosing window(HOST, ...) rather than over the viewport. Use this when the host window needs its own menu bar, decorations, or a floating / moveable frame around the dockable area.

SetNextWindowSize(ImVec2(680.0f, 440.0f), ImGuiCond.FirstUseEver)
window(HOST, (text = "Editor",
              closable = false,
              flags = ImGuiWindowFlags.MenuBar |
                      ImGuiWindowFlags.NoDocking)) {
    menu_bar(HOST_MENU) {
        menu(FILE_MENU, (text = "File", enabled = true)) {
            menu_item(SAVE_ITEM, (text = "Save"))
        }
    }
    dockspace_in_window(DS, (size = float2(0.0f, 0.0f),
                             flags = ImGuiDockNodeFlags.None)) {
        dock_window(FILES, (text = "Files", closable = false,
                            flags = ImGuiWindowFlags.None)) {
            text("project tree")
        }
        dock_window(OUTPUT, (text = "Output", closable = false,
                             flags = ImGuiWindowFlags.None)) {
            text("> ready.")
        }
    }
}

The host’s ImGuiWindowFlags.MenuBar / NoDocking live on the window call, not on dockspace_in_window — the dockspace container only manages the dock node. size = (0,0) (typical) fills the host’s available content region after the menu bar.

DS.dock_id is captured for DockBuilder* layout calls the same way as dockspace — the choice between the two is purely about whether you want the host frame around the dock area.

See modules/dasImgui/examples/features/dockspace_in_window.das for the full scene.

8.19.9.1.5. The frame loop

The dockspace(DOCK_ROOT, ...) macro wraps DockSpaceOverViewport — the dock region is the entire OS window. Inside its block, each dock_window(NAME, ...) is a Begin/End-wrapped panel that participates in the docking system. Path-prefixing works the same as containers (window / child / tab_bar): dock_window(EXPLORER) { button(REFRESH_BTN, ...) } registers the button under DOCK_ROOT/EXPLORER/REFRESH_BTN.

The closable = true option (on OUTPUT here) wires the X-button in the tab to state.open — closing the panel without rebuilding the layout.

8.19.9.1.6. Standalone vs live

Same as previous tutorials — main() runs the loop standalone; daslang-live invokes init / update / shutdown directly. ImGui’s docking state is preserved across reloads because the ImGui context survives reload (imgui_live serializes the context pointer through the reload, and the dock state lives inside that context).

8.19.9.1.7. Driving from outside

Several live commands cover the docking surface. Targets are path-qualified — the dockspace pushes its name onto the path, so panel targets are DOCK_ROOT/<name>:

# Pop Output out into a floating window
curl -X POST -d '{"name":"imgui_undock","args":{"target":"DOCK_ROOT/OUTPUT"}}' \
     localhost:9090/command

# Reposition the floating window (also works on docked windows — ImGui ignores
# the SetNextWindowPos while a window is docked, so this is most useful after
# imgui_undock). w/h are optional.
curl -X POST -d '{"name":"imgui_set_window_pos","args":{"target":"DOCK_ROOT/OUTPUT","value":{"x":580,"y":220,"w":360,"h":220}}}' \
     localhost:9090/command

# Reset the dockspace back to the default layout
curl -X POST -d '{"name":"imgui_dock_reset","args":{"target":"DOCK_ROOT"}}' \
     localhost:9090/command

# Close the OUTPUT panel (X-button equivalent — closable=true required)
curl -X POST -d '{"name":"imgui_close","args":{"target":"DOCK_ROOT/OUTPUT"}}' \
     localhost:9090/command

# Raise a panel to the front of its dock node — selects its tab when stacked
curl -X POST -d '{"name":"imgui_raise","args":{"target":"DOCK_ROOT/OUTPUT"}}' \
     localhost:9090/command

imgui_dock is the inverse of imgui_undock — it takes a value of type uint (a dock-node id from a prior DockBuilder* call) and re-docks the panel into that node. imgui_set_window_pos is the companion you’ll usually pair with imgui_undock, since a freshly undocked window picks its position from imgui.ini (or (0,0) if the window has never floated). imgui_raise brings a panel to the front of its dock node — when several panels share a node (so they render as a tab strip) it selects that panel’s tab; it’s the automation counterpart of clicking a tab, which a real click can’t reach (the dock node’s tab bar is ImGui-internal, not a registered widget).

8.19.9.1.8. Next steps

Style scopes are next — with_style for pushing colors and metrics across a sub-tree of widgets, balanced pop, and how nesting stacks.

See also

Full source: modules/dasImgui/examples/tutorial/docking.das

Richer reference: modules/dasImgui/examples/features/dock_basic.das — same boost surface with a 4-panel initial layout and a wider widget set.

Integration test: modules/dasImgui/tests/test_docking_basic.das — registration, initial-layout geometry, and live-command round-trips.

Previous tutorial: Layout

Boost macros — the macro layer.