Skip to main content

dear_imgui_rs/widget/
popup.rs

1//! Popups and modals
2//!
3//! Popup windows (context menus, modals) with builders and token helpers to
4//! ensure balanced begin/end calls.
5//!
6#![allow(
7    clippy::cast_possible_truncation,
8    clippy::cast_sign_loss,
9    clippy::as_conversions
10)]
11use crate::sys;
12use crate::ui::Ui;
13use crate::window::WindowFlags;
14
15/// # Popup Widgets
16impl Ui {
17    /// Instructs ImGui that a popup is open.
18    ///
19    /// You should **call this function once** while calling any of the following per-frame:
20    ///
21    /// - [`begin_popup`](Self::begin_popup)
22    /// - [`popup`](Self::popup)
23    /// - [`begin_modal_popup`](Self::begin_modal_popup)
24    /// - [`modal_popup`](Self::modal_popup)
25    ///
26    /// The confusing aspect to popups is that ImGui holds control over the popup itself.
27    #[doc(alias = "OpenPopup")]
28    pub fn open_popup(&self, str_id: impl AsRef<str>) {
29        let str_id_ptr = self.scratch_txt(str_id);
30        unsafe { sys::igOpenPopup_Str(str_id_ptr, PopupFlags::NONE.bits()) }
31    }
32
33    /// Instructs ImGui that a popup is open with flags.
34    #[doc(alias = "OpenPopup")]
35    pub fn open_popup_with_flags(&self, str_id: impl AsRef<str>, flags: PopupFlags) {
36        let str_id_ptr = self.scratch_txt(str_id);
37        unsafe {
38            sys::igOpenPopup_Str(str_id_ptr, flags.bits());
39        }
40    }
41
42    /// Opens a popup when the last item is clicked (typically right-click).
43    ///
44    /// If `str_id` is `None`, the popup is associated with the last item ID.
45    #[doc(alias = "OpenPopupOnItemClick")]
46    pub fn open_popup_on_item_click(&self, str_id: Option<&str>) {
47        self.open_popup_on_item_click_with_flags(str_id, PopupFlags::MOUSE_BUTTON_RIGHT);
48    }
49
50    /// Opens a popup when the last item is clicked, with explicit flags.
51    #[doc(alias = "OpenPopupOnItemClick")]
52    pub fn open_popup_on_item_click_with_flags(&self, str_id: Option<&str>, flags: PopupFlags) {
53        let str_id_ptr = str_id
54            .map(|s| self.scratch_txt(s))
55            .unwrap_or(std::ptr::null());
56        unsafe { sys::igOpenPopupOnItemClick(str_id_ptr, flags.bits()) }
57    }
58
59    /// Construct a popup that can have any kind of content.
60    ///
61    /// This should be called *per frame*, whereas [`open_popup`](Self::open_popup) should be called *once*
62    /// to signal that this popup is active.
63    #[doc(alias = "BeginPopup")]
64    pub fn begin_popup(&self, str_id: impl AsRef<str>) -> Option<PopupToken<'_>> {
65        self.begin_popup_with_flags(str_id, WindowFlags::empty())
66    }
67
68    /// Construct a popup with window flags.
69    #[doc(alias = "BeginPopup")]
70    pub fn begin_popup_with_flags(
71        &self,
72        str_id: impl AsRef<str>,
73        flags: WindowFlags,
74    ) -> Option<PopupToken<'_>> {
75        let str_id_ptr = self.scratch_txt(str_id);
76        let render = unsafe { sys::igBeginPopup(str_id_ptr, flags.bits()) };
77
78        if render {
79            Some(PopupToken::new(self))
80        } else {
81            None
82        }
83    }
84
85    /// Construct a popup that can have any kind of content.
86    ///
87    /// This should be called *per frame*, whereas [`open_popup`](Self::open_popup) should be called *once*
88    /// to signal that this popup is active.
89    #[doc(alias = "BeginPopup")]
90    pub fn popup<F>(&self, str_id: impl AsRef<str>, f: F)
91    where
92        F: FnOnce(),
93    {
94        if let Some(_token) = self.begin_popup(str_id) {
95            f();
96        }
97    }
98
99    /// Creates a modal popup.
100    ///
101    /// Modal popups block interaction with the rest of the application until closed.
102    #[doc(alias = "BeginPopupModal")]
103    pub fn begin_modal_popup(&self, name: impl AsRef<str>) -> Option<ModalPopupToken<'_>> {
104        let name_ptr = self.scratch_txt(name);
105        let render = unsafe {
106            sys::igBeginPopupModal(name_ptr, std::ptr::null_mut(), WindowFlags::empty().bits())
107        };
108
109        if render {
110            Some(ModalPopupToken::new(self))
111        } else {
112            None
113        }
114    }
115
116    /// Creates a modal popup with an opened-state tracking variable.
117    ///
118    /// Passing `opened` enables the title-bar close button (X). When clicked, ImGui will set
119    /// `*opened = false` and close the popup.
120    ///
121    /// Notes:
122    /// - You still need to call [`open_popup`](Self::open_popup) once to open the modal.
123    /// - To pass window flags, use [`begin_modal_popup_config`](Self::begin_modal_popup_config).
124    #[doc(alias = "BeginPopupModal")]
125    pub fn begin_modal_popup_with_opened(
126        &self,
127        name: impl AsRef<str>,
128        opened: &mut bool,
129    ) -> Option<ModalPopupToken<'_>> {
130        let name_ptr = self.scratch_txt(name);
131        let opened_ptr = opened as *mut bool;
132        let render =
133            unsafe { sys::igBeginPopupModal(name_ptr, opened_ptr, WindowFlags::empty().bits()) };
134
135        if render {
136            Some(ModalPopupToken::new(self))
137        } else {
138            None
139        }
140    }
141
142    /// Creates a modal popup builder.
143    pub fn begin_modal_popup_config<'a>(&'a self, name: &'a str) -> ModalPopup<'a> {
144        ModalPopup {
145            name,
146            opened: None,
147            flags: WindowFlags::empty(),
148            ui: self,
149        }
150    }
151
152    /// Creates a modal popup and runs a closure to construct the contents.
153    ///
154    /// Returns the result of the closure if the popup is open.
155    pub fn modal_popup<F, R>(&self, name: impl AsRef<str>, f: F) -> Option<R>
156    where
157        F: FnOnce() -> R,
158    {
159        self.begin_modal_popup(name).map(|_token| f())
160    }
161
162    /// Creates a modal popup with an opened-state tracking variable and runs a closure to
163    /// construct the contents.
164    ///
165    /// Returns the result of the closure if the popup is open.
166    pub fn modal_popup_with_opened<F, R>(
167        &self,
168        name: impl AsRef<str>,
169        opened: &mut bool,
170        f: F,
171    ) -> Option<R>
172    where
173        F: FnOnce() -> R,
174    {
175        self.begin_modal_popup_with_opened(name, opened)
176            .map(|_token| f())
177    }
178
179    /// Closes the current popup.
180    #[doc(alias = "CloseCurrentPopup")]
181    pub fn close_current_popup(&self) {
182        unsafe {
183            sys::igCloseCurrentPopup();
184        }
185    }
186
187    /// Returns true if the popup is open.
188    #[doc(alias = "IsPopupOpen")]
189    pub fn is_popup_open(&self, str_id: impl AsRef<str>) -> bool {
190        let str_id_ptr = self.scratch_txt(str_id);
191        unsafe { sys::igIsPopupOpen_Str(str_id_ptr, PopupFlags::NONE.bits()) }
192    }
193
194    /// Returns true if the popup is open with flags.
195    #[doc(alias = "IsPopupOpen")]
196    pub fn is_popup_open_with_flags(&self, str_id: impl AsRef<str>, flags: PopupFlags) -> bool {
197        let str_id_ptr = self.scratch_txt(str_id);
198        unsafe { sys::igIsPopupOpen_Str(str_id_ptr, flags.bits()) }
199    }
200
201    /// Begin a popup context menu for the last item.
202    /// This is typically used with right-click context menus.
203    #[doc(alias = "BeginPopupContextItem")]
204    pub fn begin_popup_context_item(&self) -> Option<PopupToken<'_>> {
205        self.begin_popup_context_item_with_label(None)
206    }
207
208    /// Begin a popup context menu for the last item with a custom label.
209    #[doc(alias = "BeginPopupContextItem")]
210    pub fn begin_popup_context_item_with_label(
211        &self,
212        str_id: Option<&str>,
213    ) -> Option<PopupToken<'_>> {
214        let str_id_ptr = str_id
215            .map(|s| self.scratch_txt(s))
216            .unwrap_or(std::ptr::null());
217
218        let render = unsafe {
219            sys::igBeginPopupContextItem(str_id_ptr, PopupFlags::MOUSE_BUTTON_RIGHT.bits())
220        };
221
222        if render {
223            Some(PopupToken::new(self))
224        } else {
225            None
226        }
227    }
228
229    /// Begin a popup context menu for the current window.
230    #[doc(alias = "BeginPopupContextWindow")]
231    pub fn begin_popup_context_window(&self) -> Option<PopupToken<'_>> {
232        self.begin_popup_context_window_with_label(None)
233    }
234
235    /// Begin a popup context menu for the current window with a custom label.
236    #[doc(alias = "BeginPopupContextWindow")]
237    pub fn begin_popup_context_window_with_label(
238        &self,
239        str_id: Option<&str>,
240    ) -> Option<PopupToken<'_>> {
241        let str_id_ptr = str_id
242            .map(|s| self.scratch_txt(s))
243            .unwrap_or(std::ptr::null());
244
245        let render = unsafe {
246            sys::igBeginPopupContextWindow(str_id_ptr, PopupFlags::MOUSE_BUTTON_RIGHT.bits())
247        };
248
249        if render {
250            Some(PopupToken::new(self))
251        } else {
252            None
253        }
254    }
255
256    /// Begin a popup context menu for empty space (void).
257    #[doc(alias = "BeginPopupContextVoid")]
258    pub fn begin_popup_context_void(&self) -> Option<PopupToken<'_>> {
259        self.begin_popup_context_void_with_label(None)
260    }
261
262    /// Begin a popup context menu for empty space with a custom label.
263    #[doc(alias = "BeginPopupContextVoid")]
264    pub fn begin_popup_context_void_with_label(
265        &self,
266        str_id: Option<&str>,
267    ) -> Option<PopupToken<'_>> {
268        let str_id_ptr = str_id
269            .map(|s| self.scratch_txt(s))
270            .unwrap_or(std::ptr::null());
271
272        let render = unsafe {
273            sys::igBeginPopupContextVoid(str_id_ptr, PopupFlags::MOUSE_BUTTON_RIGHT.bits())
274        };
275
276        if render {
277            Some(PopupToken::new(self))
278        } else {
279            None
280        }
281    }
282}
283
284bitflags::bitflags! {
285    /// Flags for popup functions
286    #[repr(transparent)]
287    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
288    pub struct PopupFlags: i32 {
289        /// No flags
290        const NONE = sys::ImGuiPopupFlags_None as i32;
291        /// For BeginPopupContext*(): open on Left Mouse release. Guaranteed to always be == 0 (same as ImGuiMouseButton_Left)
292        const MOUSE_BUTTON_LEFT = sys::ImGuiPopupFlags_MouseButtonLeft as i32;
293        /// For BeginPopupContext*(): open on Right Mouse release. Guaranteed to always be == 1 (same as ImGuiMouseButton_Right)
294        const MOUSE_BUTTON_RIGHT = sys::ImGuiPopupFlags_MouseButtonRight as i32;
295        /// For BeginPopupContext*(): open on Middle Mouse release. Guaranteed to always be == 2 (same as ImGuiMouseButton_Middle)
296        const MOUSE_BUTTON_MIDDLE = sys::ImGuiPopupFlags_MouseButtonMiddle as i32;
297        /// For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack
298        const NO_OPEN_OVER_EXISTING_POPUP = sys::ImGuiPopupFlags_NoOpenOverExistingPopup as i32;
299        /// For BeginPopupContext*(): don't return true when hovering items, only when hovering empty space
300        const NO_OPEN_OVER_ITEMS = sys::ImGuiPopupFlags_NoOpenOverItems as i32;
301        /// For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup
302        const ANY_POPUP_ID = sys::ImGuiPopupFlags_AnyPopupId as i32;
303        /// For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level)
304        const ANY_POPUP_LEVEL = sys::ImGuiPopupFlags_AnyPopupLevel as i32;
305        /// For IsPopupOpen(): test for any popup
306        const ANY_POPUP = Self::ANY_POPUP_ID.bits() | Self::ANY_POPUP_LEVEL.bits();
307    }
308}
309
310/// Builder for a modal popup
311#[derive(Debug)]
312#[must_use]
313pub struct ModalPopup<'ui> {
314    name: &'ui str,
315    opened: Option<&'ui mut bool>,
316    flags: WindowFlags,
317    ui: &'ui Ui,
318}
319
320impl<'ui> ModalPopup<'ui> {
321    /// Sets the opened state tracking variable
322    pub fn opened(mut self, opened: &'ui mut bool) -> Self {
323        self.opened = Some(opened);
324        self
325    }
326
327    /// Sets the window flags
328    pub fn flags(mut self, flags: WindowFlags) -> Self {
329        self.flags = flags;
330        self
331    }
332
333    /// Begins the modal popup
334    pub fn begin(self) -> Option<ModalPopupToken<'ui>> {
335        let name_ptr = self.ui.scratch_txt(self.name);
336        let opened_ptr = self
337            .opened
338            .map(|o| o as *mut bool)
339            .unwrap_or(std::ptr::null_mut());
340
341        let render = unsafe { sys::igBeginPopupModal(name_ptr, opened_ptr, self.flags.bits()) };
342
343        if render {
344            Some(ModalPopupToken::new(self.ui))
345        } else {
346            None
347        }
348    }
349}
350
351/// Tracks a popup that can be ended by calling `.end()` or by dropping
352#[must_use]
353pub struct PopupToken<'ui> {
354    ui: &'ui Ui,
355}
356
357impl<'ui> PopupToken<'ui> {
358    /// Creates a new popup token
359    fn new(ui: &'ui Ui) -> Self {
360        PopupToken { ui }
361    }
362
363    /// Ends the popup
364    pub fn end(self) {
365        // The drop implementation will handle the actual ending
366    }
367}
368
369impl<'ui> Drop for PopupToken<'ui> {
370    fn drop(&mut self) {
371        unsafe {
372            sys::igEndPopup();
373        }
374    }
375}
376
377/// Tracks a modal popup that can be ended by calling `.end()` or by dropping
378#[must_use]
379pub struct ModalPopupToken<'ui> {
380    ui: &'ui Ui,
381}
382
383impl<'ui> ModalPopupToken<'ui> {
384    /// Creates a new modal popup token
385    fn new(ui: &'ui Ui) -> Self {
386        ModalPopupToken { ui }
387    }
388
389    /// Ends the modal popup
390    pub fn end(self) {
391        // The drop implementation will handle the actual ending
392    }
393}
394
395impl<'ui> Drop for ModalPopupToken<'ui> {
396    fn drop(&mut self) {
397        unsafe {
398            sys::igEndPopup();
399        }
400    }
401}