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::MouseButton;
12use crate::sys;
13use crate::ui::Ui;
14use crate::window::WindowFlags;
15
16/// # Popup Widgets
17impl Ui {
18    /// Instructs ImGui that a popup is open.
19    ///
20    /// You should **call this function once** while calling any of the following per-frame:
21    ///
22    /// - [`begin_popup`](Self::begin_popup)
23    /// - [`popup`](Self::popup)
24    /// - [`begin_modal_popup`](Self::begin_modal_popup)
25    /// - [`modal_popup`](Self::modal_popup)
26    ///
27    /// The confusing aspect to popups is that ImGui holds control over the popup itself.
28    #[doc(alias = "OpenPopup")]
29    pub fn open_popup(&self, str_id: impl AsRef<str>) {
30        let str_id_ptr = self.scratch_txt(str_id);
31        unsafe { sys::igOpenPopup_Str(str_id_ptr, PopupFlags::NONE.bits()) }
32    }
33
34    /// Instructs ImGui that a popup is open with flags.
35    #[doc(alias = "OpenPopup")]
36    pub fn open_popup_with_flags(&self, str_id: impl AsRef<str>, flags: PopupFlags) {
37        let str_id_ptr = self.scratch_txt(str_id);
38        unsafe { sys::igOpenPopup_Str(str_id_ptr, flags.bits()) }
39    }
40
41    /// Opens a popup when the last item is clicked (typically right-click).
42    ///
43    /// If `str_id` is `None`, the popup is associated with the last item ID.
44    #[doc(alias = "OpenPopupOnItemClick")]
45    pub fn open_popup_on_item_click(&self, str_id: Option<&str>) {
46        self.open_popup_on_item_click_with_flags(str_id, PopupContextOptions::new());
47    }
48
49    /// Opens a popup when the last item is clicked, with explicit flags.
50    #[doc(alias = "OpenPopupOnItemClick")]
51    pub fn open_popup_on_item_click_with_flags(
52        &self,
53        str_id: Option<&str>,
54        flags: impl Into<PopupContextOptions>,
55    ) {
56        let options = flags.into();
57        let str_id_ptr = str_id
58            .map(|s| self.scratch_txt(s))
59            .unwrap_or(std::ptr::null());
60        unsafe { sys::igOpenPopupOnItemClick(str_id_ptr, options.raw()) }
61    }
62
63    /// Construct a popup that can have any kind of content.
64    ///
65    /// This should be called *per frame*, whereas [`open_popup`](Self::open_popup) should be called *once*
66    /// to signal that this popup is active.
67    #[doc(alias = "BeginPopup")]
68    pub fn begin_popup(&self, str_id: impl AsRef<str>) -> Option<PopupToken<'_>> {
69        self.begin_popup_with_flags(str_id, WindowFlags::empty())
70    }
71
72    /// Construct a popup with window flags.
73    #[doc(alias = "BeginPopup")]
74    pub fn begin_popup_with_flags(
75        &self,
76        str_id: impl AsRef<str>,
77        flags: WindowFlags,
78    ) -> Option<PopupToken<'_>> {
79        let str_id_ptr = self.scratch_txt(str_id);
80        let render = unsafe { sys::igBeginPopup(str_id_ptr, flags.bits()) };
81
82        if render {
83            Some(PopupToken::new(self))
84        } else {
85            None
86        }
87    }
88
89    /// Construct a popup that can have any kind of content.
90    ///
91    /// This should be called *per frame*, whereas [`open_popup`](Self::open_popup) should be called *once*
92    /// to signal that this popup is active.
93    #[doc(alias = "BeginPopup")]
94    pub fn popup<F>(&self, str_id: impl AsRef<str>, f: F)
95    where
96        F: FnOnce(),
97    {
98        if let Some(_token) = self.begin_popup(str_id) {
99            f();
100        }
101    }
102
103    /// Creates a modal popup.
104    ///
105    /// Modal popups block interaction with the rest of the application until closed.
106    #[doc(alias = "BeginPopupModal")]
107    pub fn begin_modal_popup(&self, name: impl AsRef<str>) -> Option<ModalPopupToken<'_>> {
108        let name_ptr = self.scratch_txt(name);
109        let render = unsafe {
110            sys::igBeginPopupModal(name_ptr, std::ptr::null_mut(), WindowFlags::empty().bits())
111        };
112
113        if render {
114            Some(ModalPopupToken::new(self))
115        } else {
116            None
117        }
118    }
119
120    /// Creates a modal popup with an opened-state tracking variable.
121    ///
122    /// Passing `opened` enables the title-bar close button (X). When clicked, ImGui will set
123    /// `*opened = false` and close the popup.
124    ///
125    /// Notes:
126    /// - You still need to call [`open_popup`](Self::open_popup) once to open the modal.
127    /// - To pass window flags, use [`begin_modal_popup_config`](Self::begin_modal_popup_config).
128    #[doc(alias = "BeginPopupModal")]
129    pub fn begin_modal_popup_with_opened(
130        &self,
131        name: impl AsRef<str>,
132        opened: &mut bool,
133    ) -> Option<ModalPopupToken<'_>> {
134        let name_ptr = self.scratch_txt(name);
135        let opened_ptr = opened as *mut bool;
136        let render =
137            unsafe { sys::igBeginPopupModal(name_ptr, opened_ptr, WindowFlags::empty().bits()) };
138
139        if render {
140            Some(ModalPopupToken::new(self))
141        } else {
142            None
143        }
144    }
145
146    /// Creates a modal popup builder.
147    pub fn begin_modal_popup_config<'a>(&'a self, name: &'a str) -> ModalPopup<'a> {
148        ModalPopup {
149            name,
150            opened: None,
151            flags: WindowFlags::empty(),
152            ui: self,
153        }
154    }
155
156    /// Creates a modal popup and runs a closure to construct the contents.
157    ///
158    /// Returns the result of the closure if the popup is open.
159    pub fn modal_popup<F, R>(&self, name: impl AsRef<str>, f: F) -> Option<R>
160    where
161        F: FnOnce() -> R,
162    {
163        self.begin_modal_popup(name).map(|_token| f())
164    }
165
166    /// Creates a modal popup with an opened-state tracking variable and runs a closure to
167    /// construct the contents.
168    ///
169    /// Returns the result of the closure if the popup is open.
170    pub fn modal_popup_with_opened<F, R>(
171        &self,
172        name: impl AsRef<str>,
173        opened: &mut bool,
174        f: F,
175    ) -> Option<R>
176    where
177        F: FnOnce() -> R,
178    {
179        self.begin_modal_popup_with_opened(name, opened)
180            .map(|_token| f())
181    }
182
183    /// Closes the current popup.
184    #[doc(alias = "CloseCurrentPopup")]
185    pub fn close_current_popup(&self) {
186        unsafe {
187            sys::igCloseCurrentPopup();
188        }
189    }
190
191    /// Returns true if the popup is open.
192    #[doc(alias = "IsPopupOpen")]
193    pub fn is_popup_open(&self, str_id: impl AsRef<str>) -> bool {
194        let str_id_ptr = self.scratch_txt(str_id);
195        unsafe { sys::igIsPopupOpen_Str(str_id_ptr, PopupFlags::NONE.bits()) }
196    }
197
198    /// Returns true if the popup is open with flags.
199    #[doc(alias = "IsPopupOpen")]
200    pub fn is_popup_open_with_flags(&self, str_id: impl AsRef<str>, flags: PopupFlags) -> bool {
201        let str_id_ptr = self.scratch_txt(str_id);
202        unsafe { sys::igIsPopupOpen_Str(str_id_ptr, flags.bits()) }
203    }
204
205    /// Begin a popup context menu for the last item.
206    #[doc(alias = "BeginPopupContextItem")]
207    pub fn begin_popup_context_item(&self) -> Option<PopupToken<'_>> {
208        self.begin_popup_context_item_with_flags(None, PopupContextOptions::new())
209    }
210
211    /// Begin a popup context menu for the last item with a custom label.
212    #[doc(alias = "BeginPopupContextItem")]
213    pub fn begin_popup_context_item_with_label(
214        &self,
215        str_id: Option<&str>,
216    ) -> Option<PopupToken<'_>> {
217        self.begin_popup_context_item_with_flags(str_id, PopupContextOptions::new())
218    }
219
220    /// Begin a popup context menu for the last item with explicit popup flags.
221    #[doc(alias = "BeginPopupContextItem")]
222    pub fn begin_popup_context_item_with_flags(
223        &self,
224        str_id: Option<&str>,
225        flags: impl Into<PopupContextOptions>,
226    ) -> Option<PopupToken<'_>> {
227        let options = flags.into();
228        let str_id_ptr = str_id
229            .map(|s| self.scratch_txt(s))
230            .unwrap_or(std::ptr::null());
231
232        let render = unsafe { sys::igBeginPopupContextItem(str_id_ptr, options.raw()) };
233
234        render.then(|| PopupToken::new(self))
235    }
236
237    /// Begin a popup context menu for the current window.
238    #[doc(alias = "BeginPopupContextWindow")]
239    pub fn begin_popup_context_window(&self) -> Option<PopupToken<'_>> {
240        self.begin_popup_context_window_with_flags(None, PopupContextOptions::new())
241    }
242
243    /// Begin a popup context menu for the current window with a custom label.
244    #[doc(alias = "BeginPopupContextWindow")]
245    pub fn begin_popup_context_window_with_label(
246        &self,
247        str_id: Option<&str>,
248    ) -> Option<PopupToken<'_>> {
249        self.begin_popup_context_window_with_flags(str_id, PopupContextOptions::new())
250    }
251
252    /// Begin a popup context menu for the current window with explicit popup flags.
253    #[doc(alias = "BeginPopupContextWindow")]
254    pub fn begin_popup_context_window_with_flags(
255        &self,
256        str_id: Option<&str>,
257        flags: impl Into<PopupContextOptions>,
258    ) -> Option<PopupToken<'_>> {
259        let options = flags.into();
260        let str_id_ptr = str_id
261            .map(|s| self.scratch_txt(s))
262            .unwrap_or(std::ptr::null());
263
264        let render = unsafe { sys::igBeginPopupContextWindow(str_id_ptr, options.raw()) };
265
266        render.then(|| PopupToken::new(self))
267    }
268
269    /// Begin a popup context menu for empty space (void).
270    #[doc(alias = "BeginPopupContextVoid")]
271    pub fn begin_popup_context_void(&self) -> Option<PopupToken<'_>> {
272        self.begin_popup_context_void_with_flags(None, PopupContextOptions::new())
273    }
274
275    /// Begin a popup context menu for empty space with a custom label.
276    #[doc(alias = "BeginPopupContextVoid")]
277    pub fn begin_popup_context_void_with_label(
278        &self,
279        str_id: Option<&str>,
280    ) -> Option<PopupToken<'_>> {
281        self.begin_popup_context_void_with_flags(str_id, PopupContextOptions::new())
282    }
283
284    /// Begin a popup context menu for empty space (void) with explicit popup flags.
285    #[doc(alias = "BeginPopupContextVoid")]
286    pub fn begin_popup_context_void_with_flags(
287        &self,
288        str_id: Option<&str>,
289        flags: impl Into<PopupContextOptions>,
290    ) -> Option<PopupToken<'_>> {
291        let options = flags.into();
292        let str_id_ptr = str_id
293            .map(|s| self.scratch_txt(s))
294            .unwrap_or(std::ptr::null());
295
296        let render = unsafe { sys::igBeginPopupContextVoid(str_id_ptr, options.raw()) };
297
298        render.then(|| PopupToken::new(self))
299    }
300}
301
302bitflags::bitflags! {
303    /// Independent flags for popup functions.
304    ///
305    /// Context popup mouse button selection is a single-choice setting
306    /// represented by [`PopupContextMouseButton`].
307    #[repr(transparent)]
308    #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
309    pub struct PopupFlags: i32 {
310        /// No flags
311        const NONE = sys::ImGuiPopupFlags_None as i32;
312        /// Do not reopen the same popup if already open.
313        const NO_REOPEN = sys::ImGuiPopupFlags_NoReopen as i32;
314        /// For OpenPopup*(), BeginPopupContext*(): don't open if there's already a popup at the same level of the popup stack
315        const NO_OPEN_OVER_EXISTING_POPUP = sys::ImGuiPopupFlags_NoOpenOverExistingPopup as i32;
316        /// For BeginPopupContext*(): don't return true when hovering items, only when hovering empty space
317        const NO_OPEN_OVER_ITEMS = sys::ImGuiPopupFlags_NoOpenOverItems as i32;
318        /// For IsPopupOpen(): ignore the ImGuiID parameter and test for any popup
319        const ANY_POPUP_ID = sys::ImGuiPopupFlags_AnyPopupId as i32;
320        /// For IsPopupOpen(): search/test at any level of the popup stack (default test in the current level)
321        const ANY_POPUP_LEVEL = sys::ImGuiPopupFlags_AnyPopupLevel as i32;
322        /// For IsPopupOpen(): test for any popup
323        const ANY_POPUP = Self::ANY_POPUP_ID.bits() | Self::ANY_POPUP_LEVEL.bits();
324    }
325}
326
327/// Single mouse button used by popup context helpers.
328#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
329pub enum PopupContextMouseButton {
330    /// Open on left mouse release.
331    Left,
332    /// Open on right mouse release.
333    #[default]
334    Right,
335    /// Open on middle mouse release.
336    Middle,
337}
338
339impl PopupContextMouseButton {
340    #[inline]
341    const fn raw(self) -> i32 {
342        match self {
343            Self::Left => sys::ImGuiPopupFlags_MouseButtonLeft as i32,
344            Self::Right => sys::ImGuiPopupFlags_MouseButtonRight as i32,
345            Self::Middle => sys::ImGuiPopupFlags_MouseButtonMiddle as i32,
346        }
347    }
348}
349
350impl From<MouseButton> for PopupContextMouseButton {
351    fn from(button: MouseButton) -> Self {
352        match button {
353            MouseButton::Left => Self::Left,
354            MouseButton::Right => Self::Right,
355            MouseButton::Middle => Self::Middle,
356            MouseButton::Extra1 | MouseButton::Extra2 => {
357                panic!(
358                    "Dear ImGui popup context helpers only support left, right, and middle buttons"
359                )
360            }
361        }
362    }
363}
364
365/// Complete popup options assembled from independent flags and optional
366/// single mouse button.
367#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
368pub struct PopupContextOptions {
369    pub flags: PopupFlags,
370    pub mouse_button: PopupContextMouseButton,
371}
372
373impl PopupContextOptions {
374    pub const fn new() -> Self {
375        Self {
376            flags: PopupFlags::NONE,
377            mouse_button: PopupContextMouseButton::Right,
378        }
379    }
380
381    pub fn flags(mut self, flags: PopupFlags) -> Self {
382        self.flags = flags;
383        self
384    }
385
386    pub fn mouse_button(mut self, button: impl Into<PopupContextMouseButton>) -> Self {
387        self.mouse_button = button.into();
388        self
389    }
390
391    pub fn bits(self) -> i32 {
392        self.raw()
393    }
394
395    #[inline]
396    pub(crate) fn raw(self) -> i32 {
397        self.flags.bits() | self.mouse_button.raw()
398    }
399}
400
401impl Default for PopupContextOptions {
402    fn default() -> Self {
403        Self::new()
404    }
405}
406
407impl From<PopupFlags> for PopupContextOptions {
408    fn from(flags: PopupFlags) -> Self {
409        Self::new().flags(flags)
410    }
411}
412
413impl From<PopupContextMouseButton> for PopupContextOptions {
414    fn from(button: PopupContextMouseButton) -> Self {
415        Self::new().mouse_button(button)
416    }
417}
418
419impl From<MouseButton> for PopupContextOptions {
420    fn from(button: MouseButton) -> Self {
421        Self::new().mouse_button(button)
422    }
423}
424
425/// Builder for a modal popup
426#[derive(Debug)]
427#[must_use]
428pub struct ModalPopup<'ui> {
429    name: &'ui str,
430    opened: Option<&'ui mut bool>,
431    flags: WindowFlags,
432    ui: &'ui Ui,
433}
434
435impl<'ui> ModalPopup<'ui> {
436    /// Sets the opened state tracking variable
437    pub fn opened(mut self, opened: &'ui mut bool) -> Self {
438        self.opened = Some(opened);
439        self
440    }
441
442    /// Sets the window flags
443    pub fn flags(mut self, flags: WindowFlags) -> Self {
444        self.flags = flags;
445        self
446    }
447
448    /// Begins the modal popup
449    pub fn begin(self) -> Option<ModalPopupToken<'ui>> {
450        let name_ptr = self.ui.scratch_txt(self.name);
451        let opened_ptr = self
452            .opened
453            .map(|o| o as *mut bool)
454            .unwrap_or(std::ptr::null_mut());
455
456        let render = unsafe { sys::igBeginPopupModal(name_ptr, opened_ptr, self.flags.bits()) };
457
458        if render {
459            Some(ModalPopupToken::new(self.ui))
460        } else {
461            None
462        }
463    }
464}
465
466/// Tracks a popup that can be ended by calling `.end()` or by dropping
467#[must_use]
468pub struct PopupToken<'ui> {
469    _ui: &'ui Ui,
470}
471
472impl<'ui> PopupToken<'ui> {
473    /// Creates a new popup token
474    fn new(ui: &'ui Ui) -> Self {
475        PopupToken { _ui: ui }
476    }
477
478    /// Ends the popup
479    pub fn end(self) {
480        // The drop implementation will handle the actual ending
481    }
482}
483
484impl<'ui> Drop for PopupToken<'ui> {
485    fn drop(&mut self) {
486        unsafe {
487            sys::igEndPopup();
488        }
489    }
490}
491
492/// Tracks a modal popup that can be ended by calling `.end()` or by dropping
493#[must_use]
494pub struct ModalPopupToken<'ui> {
495    _ui: &'ui Ui,
496}
497
498impl<'ui> ModalPopupToken<'ui> {
499    /// Creates a new modal popup token
500    fn new(ui: &'ui Ui) -> Self {
501        ModalPopupToken { _ui: ui }
502    }
503
504    /// Ends the modal popup
505    pub fn end(self) {
506        // The drop implementation will handle the actual ending
507    }
508}
509
510impl<'ui> Drop for ModalPopupToken<'ui> {
511    fn drop(&mut self) {
512        unsafe {
513            sys::igEndPopup();
514        }
515    }
516}