8.18.1.19. 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: modules/dasImgui/examples/tutorial/tab_bar.das.

8.18.1.19.1. Walkthrough

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

8.18.1.19.1.1. 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.

8.18.1.19.1.2. 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.

8.18.1.19.1.3. 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.

8.18.1.19.1.4. 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.

8.18.1.19.1.5. 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.

8.18.1.19.1.6. 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.

8.18.1.19.1.7. Standalone vs live

Same convention as the other tutorials.

See also

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

Features-side demos: modules/dasImgui/examples/features/containers_layout.das — tab_bar + tree_node + collapsing_header composition; modules/dasImgui/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.