8.19.32. data_table

ImGui’s tables API — BeginTable / EndTable with body-internal TableSetupColumn / TableHeadersRow / TableNextRow / TableSetColumnIndex / TableNextColumn cursor primitives — lives behind one boost container plus a family of snake_case pass-throughs in imgui/imgui_table_builtin. The container is named data_table (not table) because table is a daslang reserved keyword for the table<K;V> type constructor.

The container takes the ImGui id, the column count, the flags, an outer size and an inner width — same five arguments the cpp BeginTable takes — and brackets the matching EndTable. Inside the body, the row/column cursor calls are plain pass-throughs that resolve in the imgui_table_builtin module namespace (so the project-wide lint, which forbids raw imgui::* calls in user code, stays satisfied without an allow-list extension).

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

8.19.32.1. Walkthrough

The recording drives the sortable shape by clicking the real column headers: clicking Name flips the active sort ascending to descending; clicking Type replaces the key and re-groups the rows by type; and Shift+clicking Value adds it as a secondary key (the 2 badge), so within each type the rows break ties by value. Each click is verified against the re-rendered cell text, so a header click that failed to re-sort would abort the recording.

  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_table_builtin
 19require imgui/imgui_visual_aids
 20
 21// =============================================================================
 22// TUTORIAL: data_table — boost container for ImGui's tables API.
 23//
 24// `BeginTable` / `EndTable` and the body-internal cursor calls
 25// (TableSetupColumn, TableHeadersRow, TableNextRow, TableSetColumnIndex,
 26// TableNextColumn) live behind one container + six snake_case pass-throughs
 27// in `imgui/imgui_table_builtin`. The container is named `data_table`
 28// (not `table`, which is a daslang reserved keyword for `table<K;V>`).
 29//
 30//   data_table(MY_TABLE, (text = "##split", columns = 3, flags = ...,
 31//                          outer_size = float2(0,0), inner_width = 0.0f)) {
 32//       table_setup_column("Name")
 33//       table_setup_column("Type")
 34//       table_setup_column("Value")
 35//       table_headers_row()
 36//       for (row in rows) {
 37//           table_next_row()
 38//           table_set_column_index(0); text(NAME[row], (text = ...))
 39//           table_set_column_index(1); text(TYPE[row], (text = ...))
 40//           table_set_column_index(2); text(VAL[row], (text = ...))
 41//       }
 42//   }
 43//
 44// `TableState` echoes per-call config (columns / flags / outer_size /
 45// inner_width). `sort_specs()` (block helper) yields the per-frame sort
 46// spec list — see the Sortable tables section below. Multi-select
 47// hand-off and custom row-bg callbacks would extend the state
 48// additively if added later.
 49//
 50// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/data_table.das
 51// LIVE:       daslang-live modules/dasImgui/examples/tutorial/data_table.das
 52// =============================================================================
 53
 54struct private Row {
 55    name : string
 56    kind : string
 57    value : string
 58}
 59
 60var private ROWS <- [
 61    Row(name = "alpha", kind = "int", value = "42"),
 62    Row(name = "beta", kind = "float", value = "3.14"),
 63    Row(name = "gamma", kind = "string", value = "hello"),
 64    Row(name = "delta", kind = "bool", value = "true"),
 65    Row(name = "epsilon", kind = "int", value = "-7"),
 66    Row(name = "zeta", kind = "float", value = "0.001")
 67]
 68
 69// Stable column identifiers passed to table_setup_column(.., user_id=...).
 70// The sort comparator dispatches on user_id (not column_index) so the
 71// sort stays correct when the user reorders columns via drag.
 72let private COL_NAME = 0x0001u
 73let private COL_KIND = 0x0002u
 74let private COL_VAL  = 0x0003u
 75
 76def private compare_rows(spec : TableSortSpec; a : Row; b : Row) : int {
 77    // Returns -1 / 0 / 1 for a-vs-b along the column the spec names.
 78    var ord = 0
 79    if (spec.column_user_id == COL_NAME) {
 80        if (a.name < b.name) {
 81            ord = -1
 82        } elif (a.name > b.name) {
 83            ord = 1
 84        }
 85    } elif (spec.column_user_id == COL_KIND) {
 86        if (a.kind < b.kind) {
 87            ord = -1
 88        } elif (a.kind > b.kind) {
 89            ord = 1
 90        }
 91    } elif (spec.column_user_id == COL_VAL) {
 92        if (a.value < b.value) {
 93            ord = -1
 94        } elif (a.value > b.value) {
 95            ord = 1
 96        }
 97    }
 98    if (spec.sort_direction == ImGuiSortDirection.Descending) {
 99        ord = -ord
100    }
101    return ord
102}
103
104def private sort_rows(specs : array<TableSortSpec>) {
105    // Multi-key comparator: first spec that disambiguates a vs b wins; name is the total-order tiebreak.
106    ROWS |> sort() $(a, b) {
107        for (s in specs) {
108            let ord = compare_rows(s, a, b)
109            if (ord != 0) return ord < 0
110        }
111        return a.name < b.name
112    }
113}
114
115var DT_NAME : table<int; NarrativeState>
116var DT_KIND : table<int; NarrativeState>
117var DT_VAL : table<int; NarrativeState>
118
119[export]
120def init() {
121    live_create_window("dasImgui data_table tutorial", 860, 540)
122    live_imgui_init(live_window)
123    // Deterministic FirstUseEver layout for the recording: disable imgui.ini so a
124    // prior session's window pos/size can't drift the framing. Tutorial-scoped on
125    // purpose - not a blanket change to live_imgui_init.
126    DisableIniPersistence()
127    let io & = unsafe(GetIO())
128    GetStyle().FontScaleMain = 1.4
129}
130
131[export]
132def update() {
133    if (!live_begin_frame()) return
134    begin_frame()
135
136    ImGui_ImplGlfw_NewFrame()
137    apply_synth_io_override()
138    NewFrame()
139
140    SetNextWindowPos(ImVec2(30.0f, 30.0f), ImGuiCond.FirstUseEver)
141    SetNextWindowSize(ImVec2(640.0f, 380.0f), ImGuiCond.FirstUseEver)
142    window(DT_WIN, (text = "data_table", closable = false,
143                    flags = ImGuiWindowFlags.None)) {
144        text(DT_HEADER, (text = "Three columns, six rows, frozen header. Click headers to sort; Shift+click for multi-column."))
145
146        let tflags = (ImGuiTableFlags.BordersOuter |
147                      ImGuiTableFlags.RowBg |
148                      ImGuiTableFlags.Resizable |
149                      ImGuiTableFlags.Reorderable |
150                      ImGuiTableFlags.Hideable |
151                      ImGuiTableFlags.Sortable |
152                      ImGuiTableFlags.SortMulti |
153                      ImGuiTableFlags.ScrollY)
154        data_table(DT_TABLE, (text = "##rows", columns = 3,
155                              flags = tflags,
156                              outer_size = float2(0.0f, 0.0f),
157                              inner_width = 0.0f)) {
158            table_setup_scroll_freeze(0, 1)
159            table_setup_column("Name",  ImGuiTableColumnFlags.DefaultSort, 0.0f, COL_NAME)
160            table_setup_column("Type",  ImGuiTableColumnFlags.None,        0.0f, COL_KIND)
161            table_setup_column("Value", ImGuiTableColumnFlags.None,        0.0f, COL_VAL)
162            table_headers_row()
163
164            sort_specs() $(specs) {
165                sort_rows(specs)
166            }
167
168            for (i in range(length(ROWS))) {
169                table_next_row()
170                table_set_column_index(0)
171                text(DT_NAME[i], (text = ROWS[i].name))
172                table_set_column_index(1)
173                text(DT_KIND[i], (text = ROWS[i].kind))
174                table_set_column_index(2)
175                text(DT_VAL[i], (text = ROWS[i].value))
176            }
177        }
178    }
179
180    end_of_frame()
181    Render()
182    var w, h : int
183    live_get_framebuffer_size(w, h)
184    glViewport(0, 0, w, h)
185    glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
186    glClear(GL_COLOR_BUFFER_BIT)
187    live_imgui_render()
188
189    live_end_frame()
190}
191
192[export]
193def shutdown() {
194    live_imgui_shutdown()
195    live_destroy_window()
196}
197
198[export]
199def main() {
200    init()
201    while (!exit_requested()) {
202        update()
203        maybe_collect_gc()
204    }
205    shutdown()
206}

8.19.32.1.1. Requires

One extra module on top of the baseline boost layer:

  • imgui/imgui_table_builtin — the data_table container plus the table_* snake_case primitives the body calls into, the TableSortSpec struct and the sort_specs helper.

8.19.32.1.2. Container shape

data_table follows the same named-tuple convention as the other containers (window / child / tab_bar):

data_table(MY_TABLE, (text = "##rows", columns = 3,
                       flags = ImGuiTableFlags.Borders,
                       outer_size = float2(0.0f, 0.0f),
                       inner_width = 0.0f)) {
    // body
}

text is the ImGui id (use the ##suffix convention to keep it out of the visible label). columns is the column count; outer_size = float2(0,0) lets ImGui auto-size; inner_width = 0.0f means “no explicit inner width” (use the outer width).

8.19.32.1.3. Body primitives

The body cursor calls are plain def public wrappers — same arguments as the underlying ImGui calls, snake_case names:

  • table_setup_column(text, flags?, init_width_or_weight?, user_id?) — declare a column before the header row.

  • table_setup_scroll_freeze(cols, rows) — pin the first N columns / M rows during scrolling.

  • table_headers_row() — submit the header row using the table_setup_column labels.

  • table_header(text) / table_angled_headers_row() — a single custom header cell, and the rotated-label header row.

  • table_next_row(flags?, min_row_height?) — start the next row.

  • table_set_column_index(col) -> bool — jump to a specific column; returns true when the column is visible.

  • table_next_column() -> bool — advance one column (or wrap to next row); also returns the visibility bool.

  • table_set_bg_color(target, color, column_n?) — paint a row or cell background.

  • table_get_column_count() / table_get_column_index() / table_get_row_index() / table_get_column_name(col?) / table_get_column_flags(col?) — cursor and layout queries.

TableState (the container’s state struct) echoes per-call config — columns, flags, outer_size, inner_width — so snapshot consumers can read the table’s shape without parsing the daslang call site. Multi-select hand-off remains deferred — pinned ImGui 1.92.6 exposes BeginMultiSelect / ImGuiMultiSelectIO, but the boost-wrapper hand-off has not been designed yet. A custom row-bg callback API would extend the state additively if added later.

8.19.32.1.4. Sortable tables

The tutorial table uses the full sortable shape — Sortable | SortMulti | Reorderable | Hideable flags on the table, a stable user_id on each table_setup_column, and a sort_specs() block-arg helper inside the body that ImGui fires when the sort state goes dirty.

  • ImGuiTableFlags.Sortable enables single-column sort (click any header). Adding ImGuiTableFlags.SortMulti enables multi-column sort (Shift+click a second header to append a secondary sort key).

  • table_setup_column("Name", flags, init_width_or_weight, user_id = COL_NAME) tags the column with a stable identifier (a uint). The sort comparator dispatches on column_user_id rather than column_index, so the sort stays correct after the user reorders columns via drag.

  • sort_specs() $(specs) { ... } is the wrapper that captures the ImGui TableGetSortSpecs() data, converts each ImGuiTableColumnSortSpecs entry into a daslang-friendly TableSortSpec (with column_index, column_user_id, sort_order, sort_direction), invokes the body block with the array, and auto-clears the SpecsDirty flag on return. The block only fires when ImGui reports dirty (header click), so the comparator cost is paid once per sort change rather than every frame.

The block-body comparator pattern walks the specs in priority order and returns on the first spec that disambiguates a pair — sort_order = 0 is the primary key, sort_order = 1 is the first tiebreak, and so on. A final tiebreak on a unique field (here: name) keeps the order total.

For a complete standalone example (inventory table with id / name / qty columns and a multi-key comparator), see modules/dasImgui/examples/features/sort_specs.das.

8.19.32.1.5. Why the name

table is a daslang reserved keyword — the type constructor for table<K;V> (the hash-map type). Defining a function or container named table is a parse error. data_table follows the standard UI-library term (Material’s DataTable, Bootstrap’s table, etc.) and disambiguates from the type namespace at every call site.

8.19.32.1.6. Standalone vs live

Same convention as previous tutorials. daslang.exe runs the table once and exits at exit_requested(). daslang-live keeps the window open and reloads on source edits.

See also

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

Sortable inventory example: modules/dasImgui/examples/features/sort_specs.das — the canonical sort_specs() reference with a multi-key comparator.

Integration tests: modules/dasImgui/tests/test_app_small_property_editor.das (uses the same data_table container surface) and modules/dasImgui/tests/test_sort_specs.das (smoke for the sortable rail).

Boost macros — the macro layer.