8.19.20. Modal popups
popup_modal is the blocking sibling of popup: ImGui dims the
parent window, absorbs every click outside the modal, and routes ESC to
the close path. The wrapper’s signature:
popup_modal(IDENT, (text = "title",
closable = bool,
flags = ImGuiWindowFlags....)) {
// body — runs only while ImGui has the modal open
}
Lifecycle is identical to popup — OpenPopup + BeginPopupModal
+ EndPopup, driven by state.pending_open / pending_close.
The differences are entirely visual + input-blocking, both handled by
ImGui.
closable=true adds an X-button on the title bar wired to
state.open. The wrapper feeds &state.open to
BeginPopupModal’s p_open parameter, so an X-click sets
state.open=false and ImGui closes the modal on the next frame.
Source: modules/dasImgui/examples/tutorial/popup_modal.das.
8.19.20.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: popup_modal — blocking modal dialog with optional X-button.
22//
23// popup_modal(IDENT, (text = "title", closable = bool,
24// flags = ImGuiWindowFlags....)) { body }
25//
26// Lifecycle is identical to popup() — OpenPopup + BeginPopupModal/EndPopup,
27// driven by state.pending_open / pending_close — but ImGui ALSO:
28//
29// 1. Dims the parent window — visual backdrop says "deal with this first".
30// 2. Blocks input outside the modal — clicks elsewhere are absorbed, not
31// passed through. ESC closes the modal (vanilla ImGui behavior).
32// 3. With closable=true, renders an X-button wired to state.open.
33//
34// The boost wrapper threads three control surfaces through state.pending_*:
35// - app code — `MODAL_X.pending_open = true`
36// - live commands — imgui_open / imgui_close
37// - close-button — ImGui flips state.open when X clicked or ESC pressed
38//
39// STANDALONE: daslang.exe modules/dasImgui/examples/tutorial/popup_modal.das
40// LIVE: daslang-live modules/dasImgui/examples/tutorial/popup_modal.das
41//
42// DRIVE (when running live):
43// curl -X POST -d '{"name":"imgui_open","args":{"target":"PM_WIN/CONFIRM_MODAL"}}' \
44// localhost:9090/command
45// =============================================================================
46
47var private CONFIRM_RESULT : string = "(no answer yet)"
48
49[export]
50def init() {
51 live_create_window("dasImgui popup_modal tutorial", 720, 480)
52 live_imgui_init(live_window)
53 let io & = unsafe(GetIO())
54 GetStyle().FontScaleMain = 1.4
55}
56
57[export]
58def update() {
59 if (!live_begin_frame()) return
60 begin_frame()
61
62 ImGui_ImplGlfw_NewFrame()
63 apply_synth_io_override()
64 NewFrame()
65
66 SetNextWindowPos(ImVec2(20.0f, 20.0f), ImGuiCond.Always)
67 SetNextWindowSize(ImVec2(680.0f, 440.0f), ImGuiCond.Always)
68 window(PM_WIN, (text = "popup_modal tutorial", closable = false,
69 flags = ImGuiWindowFlags.None)) {
70
71 text("Two modals - a Yes/No confirm and a closable settings dialog.")
72 separator()
73
74 // ---- A: trigger the confirm modal ----
75 if (button(OPEN_CONFIRM, (text = "Delete file..."))) {
76 CONFIRM_MODAL.pending_open = true
77 }
78 same_line((spacing = 12.0f))
79 if (button(OPEN_SETTINGS, (text = "Open settings (X-button)"))) {
80 SETTINGS_MODAL.pending_open = true
81 }
82
83 separator()
84 text(STATUS_LINE, (text = "Last confirm answer: {CONFIRM_RESULT}"))
85 text("SETTINGS_MODAL.open (closable) = {SETTINGS_MODAL.open}")
86
87 // ---- Modal A: Yes/No confirm ----
88
89 // closable=false → no X-button; Yes/No buttons + ESC close it (ESC is an
90 // ImGui built-in — to disable it the app must intercept the key before
91 // NewFrame()).
92 popup_modal(CONFIRM_MODAL, (text = "Confirm delete",
93 closable = false,
94 flags = ImGuiWindowFlags.AlwaysAutoResize)) {
95 text("Are you sure you want to delete the selected file?")
96 separator()
97 if (button(CONFIRM_YES, (text = "Yes"))) {
98 CONFIRM_RESULT = "Yes"
99 CONFIRM_MODAL.pending_close = true
100 }
101 same_line((spacing = 12.0f))
102 if (button(CONFIRM_NO, (text = "No"))) {
103 CONFIRM_RESULT = "No"
104 CONFIRM_MODAL.pending_close = true
105 }
106 }
107
108 // ---- Modal B: closable settings ----
109 // closable=true → X-button on the title bar wired to state.open.
110 popup_modal(SETTINGS_MODAL, (text = "Settings",
111 closable = true,
112 flags = ImGuiWindowFlags.AlwaysAutoResize)) {
113 text("Background tasks")
114 separator()
115 checkbox(S_AUTOSAVE, (text = "Auto-save every 5 minutes"))
116 checkbox(S_TELEMETRY, (text = "Send anonymous telemetry"))
117 S_MAX_FPS.bounds = (30, 240)
118 slider_int(S_MAX_FPS, (text = "Max FPS cap"))
119 separator()
120 if (button(S_APPLY, (text = "Apply"))) {
121 SETTINGS_MODAL.pending_close = true
122 }
123 }
124 }
125
126 end_of_frame()
127 Render()
128 var w, h : int
129 live_get_framebuffer_size(w, h)
130 glViewport(0, 0, w, h)
131 glClearColor(0.10f, 0.10f, 0.12f, 1.0f)
132 glClear(GL_COLOR_BUFFER_BIT)
133 live_imgui_render()
134
135 live_end_frame()
136}
137
138[export]
139def shutdown() {
140 live_imgui_shutdown()
141 live_destroy_window()
142}
143
144[export]
145def main() {
146 init()
147 while (!exit_requested()) {
148 update()
149 maybe_collect_gc()
150 }
151 shutdown()
152}
8.19.20.1.1. Requires
Already in the baseline boost layer:
imgui/imgui_containers_builtin—popup_modalpluswindow.imgui/imgui_widgets_builtin—button,checkbox,slider_int,text.
8.19.20.1.2. Confirm dialog (non-closable)
The classic “Yes/No before destructive action”:
popup_modal(CONFIRM_MODAL, (text = "Confirm delete",
closable = false,
flags = ImGuiWindowFlags.AlwaysAutoResize)) {
text("Are you sure you want to delete the selected file?")
separator()
if (button(CONFIRM_YES, (text = "Yes"))) {
CONFIRM_RESULT = "Yes"
CONFIRM_MODAL.pending_close = true
}
same_line()
if (button(CONFIRM_NO, (text = "No"))) {
CONFIRM_RESULT = "No"
CONFIRM_MODAL.pending_close = true
}
}
closable=false means there’s no X-button — but ESC still closes the
modal (ImGui’s built-in popup keybinding, unaffected by closable or
any ImGuiWindowFlags bit). The body’s Yes/No buttons are the
explicit answer paths: both write to CONFIRM_RESULT and set
pending_close. To make the modal truly non-dismissable, the app must
intercept the ESC key before NewFrame() and avoid issuing any
pending_close from app code — outside the scope of this tutorial.
The AlwaysAutoResize flag (passed via flags) sizes the modal to
its content — saves the caller from picking width/height by hand.
8.19.20.1.3. Closable settings modal
When the user might want to close without “saving”:
popup_modal(SETTINGS_MODAL, (text = "Settings",
closable = true,
flags = ImGuiWindowFlags.AlwaysAutoResize)) {
checkbox(S_AUTOSAVE, (text = "Auto-save every 5 minutes"))
checkbox(S_TELEMETRY, (text = "Send anonymous telemetry"))
slider_int(S_MAX_FPS, (text = "Max FPS cap"))
separator()
if (button(S_APPLY, (text = "Apply"))) {
SETTINGS_MODAL.pending_close = true
}
}
closable=true activates the X-button. The wrapper passes
&state.open to ImGui as p_open; ImGui flips it false on
X-click or ESC, the next frame’s BeginPopupModal returns false,
and the wrapper finalizes the close. SETTINGS_MODAL.open mirrors
ImGui’s view so the snapshot reports whether the modal is currently up.
8.19.20.1.4. popup vs popup_modal
Same machinery, different UX:
popup— clicks outside the popup close it (auto-close behavior). Useful for dropdowns, context menus, transient widgets.popup_modal— clicks outside are absorbed. The user MUST resolve the modal before doing anything else. Use for destructive confirms, multi-step wizards, blocking error dialogs.
The two share PopupState exactly — same open / flags /
pending_open / pending_close fields. Switching between forms
is a one-word edit.
8.19.20.1.5. Pending-flag channels
Three control surfaces converge on the same state:
App code —
MODAL.pending_open = truefrom a button handler or any event.Live commands —
imgui_open/imgui_closeagainst the registered path.Close button / ESC (closable only) — ImGui flips
state.open, the next frame’s wrapper applies the close.
The wrapper bridges them: pending_open=true triggers OpenPopup,
pending_close=true triggers CloseCurrentPopup inside the active
modal body, X-click flips open=false and the wrapper closes on
next frame.
8.19.20.1.6. Standalone vs live
Same convention as the other tutorials.
8.19.20.1.7. Driving from outside
The walkthrough above opens each modal by clicking its trigger button and dismisses it by clicking Yes / No / Apply — every gesture is a real click, self-verified by the modal body appearing or vanishing. The live commands below drive the same lifecycle directly, for remote or scripted control:
curl -X POST -d '{"name":"imgui_open","args":{"target":"PM_WIN/CONFIRM_MODAL"}}' \
localhost:9090/command
curl -X POST -d '{"name":"imgui_click","args":{"target":"PM_WIN/CONFIRM_MODAL/CONFIRM_YES"}}' \
localhost:9090/command
curl -X POST -d '{"name":"imgui_close","args":{"target":"PM_WIN/CONFIRM_MODAL"}}' \
localhost:9090/command
The path for body widgets composes off the modal IDENT — clicks against
PM_WIN/CONFIRM_MODAL/CONFIRM_YES resolve correctly only while the
modal is open (the body’s widgets aren’t in the registry otherwise).
See also
Full source: modules/dasImgui/examples/tutorial/popup_modal.das
Features-side demo: modules/dasImgui/examples/features/containers_overlay.das —
popup + popup_modal + tooltip overlay family showcase.
Sibling: Popup window — manual-trigger popup pattern for shared-str_id under PushID scopes.
Boost macros — the macro layer.