Tab bar

tab_bar hosts one or more tab_item blocks under a strip of clickable headers. ImGui owns active-tab selection internally — the boost wrapper just observes — and routes click events to the right body. Three things make tabs subtle:

  • Only the active tab’s body runs each frame. An inactive tab’s leaves aren’t submitted to ImGui, so a snapshot shows them rendered:false and their container-path key (e.g. AUDIO_TAB/A_VOLUME) only resolves once that tab has been active — switch the tab before reading or writing leaves under it. The container’s state (TabBarState / TabItemState) lives outside the body and persists.

  • There is no ``pending_select`` channel. ImGui owns selection; imgui_click against a tab_item path is how external drivers switch tabs.

  • Closable tabs add an X-button. The wrapper feeds &state.open to BeginTabItem’s p_open parameter; ImGui flips it false on X-click and the wrapper skips the tab next frame.

Source: examples/tutorial/tab_bar.das.

Walkthrough

  1options gen2
  2
  3require imgui
  4require imgui_app
  5require opengl/opengl_boost
  6require live/glfw_live
  7require live/live_api
  8require live/live_commands
  9require live/live_vars
 10require live_host
 11require imgui/imgui_live
 12require imgui/imgui_boost_runtime
 13require imgui/imgui_boost_v2
 14require imgui/imgui_widgets_builtin
 15require imgui/imgui_containers_builtin
 16require imgui/imgui_visual_aids
 17
 18// =============================================================================
 19// TUTORIAL: tab_bar — BeginTabBar / EndTabBar with nested tab_item blocks.
 20//
 21//   tab_bar(IDENT, (text = "...", flags = ImGuiTabBarFlags....)) {
 22//       tab_item(TAB_A, (text = "A", closable = false, flags = ...)) { ... }
 23//       tab_item(TAB_B, (text = "B", closable = true,  flags = ...)) { ... }
 24//   }
 25//
 26// Key behaviors:
 27//
 28//   1. Only the ACTIVE tab's body block runs each frame — widgets inside
 29//      inactive tabs are NOT in the registry that frame. A driver doing
 30//      imgui_force_set against an inactive tab's child path gets a 404.
 31//
 32//   2. Closable tabs (closable=true) render an X-button. ImGui flips
 33//      state.open=false; the wrapper then skips BeginTabItem entirely so
 34//      the tab disappears from the strip until pending_open is set true
 35//      again.
 36//
 37//   3. ImGui owns active-tab selection — there is NO state.pending_select.
 38//      Drive it from outside by imgui_click against the tab_item path
 39//      (the click hits the tab strip header).
 40//
 41// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/tab_bar.das
 42// LIVE:       daslang-live modules/dasImgui/examples/tutorial/tab_bar.das
 43//
 44// DRIVE (when running live):
 45//   curl -X POST -d '{"name":"imgui_click","args":{"target":"TB_WIN/MAIN_TABS/AUDIO_TAB"}}' \
 46//        localhost:9090/command
 47//   curl -X POST -d '{"name":"imgui_close","args":{"target":"TB_WIN/MAIN_TABS/DRAFT_TAB"}}' \
 48//        localhost:9090/command
 49// =============================================================================
 50
 51[export]
 52def init() {
 53    live_create_window("dasImgui tab_bar tutorial", 760, 540)
 54    live_imgui_init(live_window)
 55    let io & = unsafe(GetIO())
 56    GetStyle().FontScaleMain = 1.4
 57}
 58
 59[export]
 60def update() {
 61    if (!live_begin_frame()) return
 62    begin_frame()
 63
 64    ImGui_ImplGlfw_NewFrame()
 65    apply_synth_io_override()
 66    NewFrame()
 67
 68    SetNextWindowPos(ImVec2(20.0f, 20.0f), ImGuiCond.Always)
 69    SetNextWindowSize(ImVec2(720.0f, 500.0f), ImGuiCond.Always)
 70    window(TB_WIN, (text = "tab_bar tutorial", closable = false,
 71                    flags = ImGuiWindowFlags.None)) {
 72
 73        text("Three tabs plus one closable Draft tab. Click a header to switch.")
 74        separator()
 75
 76        // Reorderable + TabListPopupButton (the ▼ menu on the right).
 77        tab_bar(MAIN_TABS, (text = "MainTabs",
 78                            flags = ImGuiTabBarFlags.Reorderable |
 79                                    ImGuiTabBarFlags.TabListPopupButton)) {
 80
 81            // ---- General tab (non-closable, default selection) ----
 82            tab_item(GENERAL_TAB, (text = "General", closable = false,
 83                                   flags = ImGuiTabItemFlags.None)) {
 84                text("Only the active tab's body block runs each frame.")
 85                text("Switch tabs - checkbox state below survives because the")
 86                text("[container] state lives outside the body.")
 87                checkbox(G_WIRE, (text = "Wireframe"))
 88                checkbox(G_LIVE, (text = "Live preview"))
 89            }
 90
 91            // ---- Audio tab ----
 92            tab_item(AUDIO_TAB, (text = "Audio", closable = false,
 93                                 flags = ImGuiTabItemFlags.None)) {
 94                slider_int(A_VOLUME, (text = "Volume"))
 95                checkbox(A_MUTE, (text = "Mute"))
 96                text("VOLUME = {A_VOLUME.value}  MUTE = {A_MUTE.value}")
 97            }
 98
 99            // ---- Info tab ----
100            tab_item(INFO_TAB, (text = "Info", closable = false,
101                                flags = ImGuiTabItemFlags.None)) {
102                text(INFO_LEAD, (text = "Tab metadata - read-only."))
103                text("MAIN_TABS.flags = Reorderable | TabListPopupButton")
104                text("Inactive tab bodies do NOT register child widgets.")
105            }
106
107            // ---- Draft tab (closable, has X-button) ----
108            tab_item(DRAFT_TAB, (text = "Draft", closable = true,
109                                 flags = ImGuiTabItemFlags.None)) {
110                text("Draft. Click the x on the tab header to close it.")
111                text("DRAFT_TAB.open = {DRAFT_TAB.open}")
112                slider_int(D_LINE_LEN, (text = "Line length"))
113            }
114        }
115
116        separator()
117        text("Outside the tab_bar - runs every frame regardless of active tab.")
118        text("DRAFT_TAB.open is observable from out here: {DRAFT_TAB.open}")
119    }
120
121    end_of_frame()
122    Render()
123    var w, h : int
124    live_get_framebuffer_size(w, h)
125    glViewport(0, 0, w, h)
126    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
127    glClear(GL_COLOR_BUFFER_BIT)
128    live_imgui_render()
129
130    live_end_frame()
131}
132
133[export]
134def shutdown() {
135    live_imgui_shutdown()
136    live_destroy_window()
137}
138
139[export]
140def main() {
141    init()
142    while (!exit_requested()) {
143        update()
144    }
145    shutdown()
146}

Requires

Already in the baseline boost layer:

  • imgui/imgui_containers_builtintab_bar, tab_item, edit_tab_item.

  • imgui/imgui_widgets_builtin — leaves used inside each tab body.

Active tab + body gating

ImGui drives active-tab selection internally. Each tab_item’s body only runs while that tab is active:

tab_bar(MAIN_TABS, (text = "MainTabs",
                    flags = ImGuiTabBarFlags.Reorderable)) {
    tab_item(GENERAL_TAB, (text = "General", closable = false,
                           flags = ImGuiTabItemFlags.None)) {
        checkbox(G_WIRE, (text = "Wireframe"))   // only registered while active
    }
    tab_item(AUDIO_TAB, ...) { ... }
    tab_item(INFO_TAB,  ...) { ... }
}

A snapshot taken while AUDIO_TAB is active shows G_WIRE under GENERAL_TAB as rendered:false — the leaf wasn’t submitted to ImGui that frame. Drivers must switch the active tab first before reading or writing leaves underneath.

Switching tabs from outside

imgui_click against a tab_item path hits the tab strip header (the boost wrapper captures the header bbox separately so click resolves to the strip, not the body):

curl -X POST -d '{"name":"imgui_click","args":{"target":"TB_WIN/MAIN_TABS/AUDIO_TAB"}}' \
     localhost:9090/command

This is the only programmatic way to switch tabs — there’s no imgui_set_active because ImGui’s internal selection isn’t exposed as a struct field. imgui_click is enough: it produces a real ImGui mouse event against the header, which is exactly what manual user interaction does.

Closable tabs

closable=true makes the X-button appear:

tab_item(DRAFT_TAB, (text = "Draft", closable = true,
                     flags = ImGuiTabItemFlags.None)) {
    text("DRAFT_TAB.open = {DRAFT_TAB.open}")
}

The wrapper threads &state.open into BeginTabItem’s p_open. When the user clicks X, ImGui flips state.open=false. Next frame the wrapper sees !state.open and skips BeginTabItem entirely — the tab disappears from the strip. Bring it back with DRAFT_TAB.pending_open = true from app code or imgui_open from a driver.

state.open defaults to true so closable tabs appear from frame 1. @live open : bool = true in TabItemState carries the value across live reload — the user’s “I closed this tab” state survives a recompile.

Sharing open state across two surfaces

For the canonical “the same flag lives in a checkbox and a tab’s X” case, use edit_tab_item against a caller-owned bool pointer:

var private DRAFT_TAB_OPEN : bool = true

// Checkbox row mirrors the X-button flag.
edit_checkbox(safe_addr(DRAFT_TAB_OPEN), (id = "TAB_VISIBLE",
                                           text = "Show Draft tab"))
// ...
edit_tab_item(safe_addr(DRAFT_TAB_OPEN), "Draft",
              ImGuiTabItemFlags.None) {
    text("Draft body — same flag the checkbox flips")
}

The two rails share one bool — flip via either control surface and the other auto-mirrors. Internal-only tab_item.open doesn’t expose this; edit_tab_item is the bidir-bind variant.

ImGuiTabBarFlags

Bar-level chrome:

  • Reorderable — drag tab headers to reorder them.

  • AutoSelectNewTabs — newly-submitted tabs become the active selection.

  • TabListPopupButton — adds a ▼ menu on the right with all tabs as a list (useful when there are many tabs).

  • NoCloseWithMiddleMouseButton — disable the middle-mouse shortcut for closable tabs.

  • NoTooltip — suppress hover tooltips on tab headers.

  • FittingPolicyResizeDown / FittingPolicyScroll — what happens when the strip doesn’t fit horizontally.

Per-tab flags via ImGuiTabItemFlags: UnsavedDocument (marks the header with a dot), SetSelected (a one-shot select-this-tab nudge), NoCloseButton (closable=true but no X), Leading/Trailing (pin to bar edges), NoReorder.

Standalone vs live

Same convention as the other tutorials.

See also

Full source: examples/tutorial/tab_bar.das

Features-side demos: examples/features/containers_layout.das — tab_bar + tree_node + collapsing_header composition; examples/features/tab_item_button.das — the tab_item_button widget for an action-button styled as a tab.

Sibling: Containers — umbrella container tour.

Boost macros — the macro layer.