data_table

ImGui’s tables API — BeginTable / EndTable with body-internal TableSetupColumn / TableHeadersRow / TableNextRow / TableSetColumnIndex / TableNextColumn cursor primitives — lives behind one boost container plus six 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: examples/tutorial/data_table.das.

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

Requires

One extra module on top of the baseline boost layer:

  • imgui/imgui_table_builtin — the data_table container plus the six table_* snake_case primitives the body calls into.

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

Body primitives

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

  • table_setup_column(label, flags?, init_width?, 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_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.

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.

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, 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 examples/features/sort_specs.das.

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.

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: examples/tutorial/data_table.das

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

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

Boost macros — the macro layer.