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