Dropdown + select
Four shapes of pick-one-from-a-list. combo collapses choices
into a dropdown popup; selectable stacks them as always-visible
rows. combo_getter serves a procedural list from a lambda;
selectable_label lets the caller own the selected-flag (so you
can wire it through a list_box for single-select):
combo(IDENT, (text = "..", items <- ["A", "B", ...])) // dropdown, items array
combo_getter(IDENT, (text = "..", items_count = N, // dropdown, procedural list
getter = @(idx; var r : string&) : bool { ... }))
selectable(IDENT, (text = "..")) // row, self-toggles
selectable_label(IDENT, text, selected, (flags = ...)) // row, caller owns 'selected'
Source: examples/tutorial/dropdown_select.das.
Walkthrough
1options gen2
2
3require math
4require strings
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: dropdown_select — four shapes of pick-one-from-a-list.
22//
23// combo(IDENT, (text = "..", items <- ["A", "B", ...]))
24// Items-array sugar — pass the strings, get a single-call dropdown.
25// combo_getter(IDENT, (text = "..", items_count = N,
26// getter = @(idx; var result : string&) : bool { ... }))
27// Procedural list — the callback supplies row text on demand. Use when
28// items live in an unusual container (table, generator, large dataset).
29// selectable(IDENT, (text = "..", flags = ..., size = ...))
30// One row, self-toggles via ToggleState. Stack N to make a list.
31// selectable_label(IDENT, text, selected, (flags = ..., size = ...))
32// Row whose selected-look is driven by a CALLER-owned bool. Pair with
33// list_box (+ external index) when only one row may be active.
34//
35// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/dropdown_select.das
36// LIVE: daslang-live modules/dasImgui/examples/tutorial/dropdown_select.das
37//
38// DRIVE (when running live):
39// curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DS_WIN/D_FRUIT","value":2}}' \
40// localhost:9090/command
41// curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DS_WIN/D_FONT","value":1}}' \
42// localhost:9090/command
43// curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DS_WIN/D_SHIRT","value":3}}' \
44// localhost:9090/command
45// =============================================================================
46
47let FRUITS : array<string> <- ["Apple", "Banana", "Cherry", "Durian", "Elderberry"]
48let FONTS = fixed_array("Serif", "Sans", "Mono", "Cursive")
49let TAGS = fixed_array("urgent", "blocked", "review")
50let SHIRTS = fixed_array("XS", "S", "M", "L", "XL", "XXL")
51
52var private D_TAG : table<int; ToggleState>
53var private D_SHIRT_ROW : table<int; ClickState>
54
55[export]
56def init() {
57 live_create_window("dasImgui dropdown_select tutorial", 760, 640)
58 live_imgui_init(live_window)
59 let io & = unsafe(GetIO())
60 GetStyle().FontScaleMain = 1.4
61}
62
63[export]
64def update() {
65 if (!live_begin_frame()) return
66 begin_frame()
67
68 ImGui_ImplGlfw_NewFrame()
69 apply_synth_io_override()
70 NewFrame()
71
72 SetNextWindowPos(ImVec2(20.0f, 20.0f), ImGuiCond.Always)
73 SetNextWindowSize(ImVec2(720.0f, 600.0f), ImGuiCond.Always)
74 window(DS_WIN, (text = "dropdown_select tutorial", closable = false,
75 flags = ImGuiWindowFlags.None)) {
76
77 text("Four shapes of pick-one-from-a-list.")
78 text(D_HINT, (text = "combo collapses; selectable rows stay open."))
79 separator()
80
81 // ---- Stage 1: combo — items as a string array ----
82 combo(D_FRUIT, (text = "Fruit", items <- FRUITS))
83 text("D_FRUIT.value = {D_FRUIT.value} ('{FRUITS[clamp(D_FRUIT.value, 0, length(FRUITS) - 1)]}')")
84 spacing()
85
86 // ---- Stage 2: combo_getter — callback supplies row text on demand ----
87 combo_getter(D_FONT, (text = "Font", items_count = length(FONTS),
88 getter = @(idx : int; var result : string&) : bool {
89 if (idx >= 0 && idx < length(FONTS)) {
90 result = FONTS[idx]
91 return true
92 }
93 return false
94 }))
95 text("D_FONT.value = {D_FONT.value} ('{FONTS[clamp(D_FONT.value, 0, length(FONTS) - 1)]}')")
96 spacing()
97
98 // ---- Stage 3: selectable — N rows, each self-toggles independently ----
99 text("Tags (multi-select - each row toggles its own ToggleState):")
100 for (i in range(length(TAGS))) {
101 selectable(D_TAG[i], (text = TAGS[i]))
102 }
103 text("active tags: {tags_summary()}")
104 spacing()
105
106 // ---- Stage 4: selectable_label inside list_box — single-select ----
107 text("Shirt size (single-select - caller-owned int picks active row):")
108 list_box(D_SHIRT, (text = "size", size = float2(0.0f, 0.0f))) {
109 for (i in range(length(SHIRTS))) {
110 let is_sel = (i == D_SHIRT.value)
111 if (selectable_label(D_SHIRT_ROW[i], SHIRTS[i], is_sel)) {
112 D_SHIRT.value = i
113 }
114 }
115 }
116 text("D_SHIRT.value = {D_SHIRT.value} ('{SHIRTS[clamp(D_SHIRT.value, 0, length(SHIRTS) - 1)]}')")
117 }
118
119 end_of_frame()
120 Render()
121 var w, h : int
122 live_get_framebuffer_size(w, h)
123 glViewport(0, 0, w, h)
124 glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
125 glClear(GL_COLOR_BUFFER_BIT)
126 live_imgui_render()
127
128 live_end_frame()
129}
130
131def tags_summary() : string {
132 var first = true
133 return build_string() $(var w : StringBuilderWriter) {
134 for (i in range(length(TAGS))) {
135 if (D_TAG[i].value) {
136 if (!first) {
137 w |> write(", ")
138 }
139 w |> write(TAGS[i])
140 first = false
141 }
142 }
143 if (first) {
144 w |> write("(none)")
145 }
146 }
147}
148
149[export]
150def shutdown() {
151 live_imgui_shutdown()
152 live_destroy_window()
153}
154
155[export]
156def main() {
157 init()
158 while (!exit_requested()) {
159 update()
160 }
161 shutdown()
162}
Requires
Already in the baseline boost layer:
imgui/imgui_widgets_builtin—combo/combo_getter/selectable/selectable_labelrails.imgui/imgui_containers_builtin—list_boxblock-arg container.imgui/imgui_boost_runtime—ComboState/ToggleState/ClickState/ListBoxStatestate structs.
combo — items array
The simplest form. Pass the labels, get a dropdown; state.value is
the chosen index.
combo(FRUIT, (text = "Fruit",
items <- ["Apple", "Banana", "Cherry"]))
// FRUIT.value == 0 / 1 / 2
items is non-copyable (it’s a moved array<string>) — pass it
inline with <-, or move from a local. Cap a long popup with
popup_max_height_in_items = N to limit the scroller height.
combo_getter — procedural list
When labels don’t live in a flat array<string> (table-backed,
generator-driven, computed on the fly), use the getter form. The
callback returns one label per idx:
combo_getter(FONT, (text = "Font", items_count = length(FONTS),
getter = @(idx : int; var result : string&) : bool {
if (idx >= 0 && idx < length(FONTS)) {
result = FONTS[idx]
return true
}
return false
}))
Return false for out-of-range indices — ImGui treats that as an
empty row, useful for sparse lists. items_count is the upper bound
the dropdown will probe.
selectable — multi-toggle rows
Each selectable owns its own ToggleState — rows toggle
independently, which falls out as multi-select naturally:
var private TAG : table<int; ToggleState>
let TAGS = fixed_array("urgent", "blocked", "review")
for (i in range(length(TAGS))) {
selectable(TAG[i], (text = TAGS[i]))
}
// any subset can be on at once — read TAG[i].value to filter.
The table<int; ToggleState> pattern is required for any indexed
call (one IDENT, many rows). The state allocates lazily on first
touch of each index.
selectable_label inside list_box — single-select
For one-of-N, switch to selectable_label with a caller-owned
selected bool, wrapped in list_box (which adds the bordered
frame + scroller). The list_box’s state.value holds the chosen
index; the loop tags each row’s selected from it:
var private SHIRT_ROW : table<int; ClickState>
let SHIRTS = fixed_array("XS", "S", "M", "L", "XL", "XXL")
list_box(SHIRT, (text = "size", size = float2(0.0f, 0.0f))) {
for (i in range(length(SHIRTS))) {
let is_sel = (i == SHIRT.value)
if (selectable_label(SHIRT_ROW[i], SHIRTS[i], is_sel)) {
SHIRT.value = i
}
}
}
The ClickState rows carry only edge-trigger info — no per-row
toggle persistence. The list_box’s state.value is the single
source of truth, so imgui_force_set on the list_box target works the
same as a combo.
Driving from outside
# combo / combo_getter — int value (the chosen index)
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DS_WIN/D_FRUIT","value":2}}' \
localhost:9090/command
# list_box single-select — int value (the chosen row)
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DS_WIN/D_SHIRT","value":3}}' \
localhost:9090/command
# selectable multi-select — bool per row (use the indexed IDENT)
curl -X POST -d '{"name":"imgui_force_set","args":{"target":"DS_WIN/D_TAG[0]","value":true}}' \
localhost:9090/command
When to use which
combo — known list of choices, screen real estate matters.
itemsis a one-liner.combo_getter — list is procedural / huge / sparse. Same UX, callback-driven content.
selectable — multi-select. Rows always visible (no popup). Owns its own state per row.
selectable_label + list_box — single-select that needs to stay visible (sidebar, settings panel). The framed list_box is the always-visible counterpart to combo’s collapsing popup.
If a strip of 2-5 mutually exclusive options would fit inline,
radio_button_int (Toggles) is lighter than a
combo or list_box.
See also
Full source: examples/tutorial/dropdown_select.das
Features-side demo: examples/features/inputs_choice.das —
combo + list_box in one window for imgui_force_set smoke testing.
Sibling tutorials: Toggles, Selectable hover (hover-only variant of selectable).
Boost macros — the macro layer.