8.19.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.19.19.1. Walkthrough

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

8.19.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.19.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, (text = "Audio", closable = false,
                         flags = ImGuiTabItemFlags.None)) {
        slider_int(A_VOLUME, (text = "Volume"))
    }
    tab_item(INFO_TAB, (text = "Info", closable = false,
                        flags = ImGuiTabItemFlags.None)) {
        text("Tab metadata - read-only.")
    }
}

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.19.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.19.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.19.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 (require daslib/safe_addr for the safe_addr form):

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), (id = "DRAFT_TAB_EXT",
                                           text = "Draft",
                                           flags = 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.19.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.

  • DrawSelectedOverline — draw an overline on the selected tab.

  • FittingPolicyShrink / FittingPolicyScroll — what happens when the strip doesn’t fit horizontally (FittingPolicyShrink is the default; FittingPolicyMixed combines the two).

Per-tab flags via ImGuiTabItemFlags: UnsavedDocument (marks the header with a dot), SetSelected (a one-shot select-this-tab nudge), Leading/Trailing (pin to bar edges), NoReorder, NoPushId, NoTooltip, NoCloseWithMiddleMouseButton, NoAssumedClosure. There is no public “closable but no X” flag — ImGui’s NoCloseButton is internal bookkeeping and is not bound; drop closable instead.

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