arcdps_imgui/
context.rs

1use parking_lot::ReentrantMutex;
2use std::cell::{RefCell, UnsafeCell};
3use std::ffi::{CStr, CString};
4use std::ops::Drop;
5use std::path::PathBuf;
6use std::ptr;
7use std::rc::Rc;
8
9use crate::clipboard::{ClipboardBackend, ClipboardContext};
10use crate::fonts::atlas::{FontAtlas, FontAtlasRefMut, FontId, SharedFontAtlas};
11use crate::io::Io;
12use crate::style::Style;
13use crate::sys;
14use crate::Ui;
15
16/// An imgui-rs context.
17///
18/// A context needs to be created to access most library functions. Due to current Dear ImGui
19/// design choices, at most one active Context can exist at any time. This limitation will likely
20/// be removed in a future Dear ImGui version.
21///
22/// If you need more than one context, you can use suspended contexts. As long as only one context
23/// is active at a time, it's possible to have multiple independent contexts.
24///
25/// # Examples
26///
27/// Creating a new active context:
28/// ```
29/// let ctx = arcdps_imgui::Context::create();
30/// // ctx is dropped naturally when it goes out of scope, which deactivates and destroys the
31/// // context
32/// ```
33///
34/// Never try to create an active context when another one is active:
35///
36/// ```should_panic
37/// let ctx1 = arcdps_imgui::Context::create();
38///
39/// let ctx2 = arcdps_imgui::Context::create(); // PANIC
40/// ```
41///
42/// Suspending an active context allows you to create another active context:
43///
44/// ```
45/// let ctx1 = arcdps_imgui::Context::create();
46/// let suspended1 = ctx1.suspend();
47/// let ctx2 = arcdps_imgui::Context::create(); // this is now OK
48/// ```
49
50#[derive(Debug)]
51pub struct Context {
52    raw: *mut sys::ImGuiContext,
53    shared_font_atlas: Option<Rc<RefCell<SharedFontAtlas>>>,
54    ini_filename: Option<CString>,
55    log_filename: Option<CString>,
56    platform_name: Option<CString>,
57    renderer_name: Option<CString>,
58    // we need to box this because we hand imgui a pointer to it,
59    // and we don't want to deal with finding `clipboard_ctx`.
60    // we also put it in an unsafecell since we're going to give
61    // imgui a mutable pointer to it.
62    clipboard_ctx: Box<UnsafeCell<ClipboardContext>>,
63}
64
65// This mutex needs to be used to guard all public functions that can affect the underlying
66// Dear ImGui active context
67static CTX_MUTEX: ReentrantMutex<()> = parking_lot::const_reentrant_mutex(());
68
69fn clear_current_context() {
70    unsafe {
71        sys::igSetCurrentContext(ptr::null_mut());
72    }
73}
74fn no_current_context() -> bool {
75    let ctx = unsafe { sys::igGetCurrentContext() };
76    ctx.is_null()
77}
78
79impl Context {
80    /// Creates a new active imgui-rs context.
81    ///
82    /// # Panics
83    ///
84    /// Panics if an active context already exists
85    #[doc(alias = "CreateContext")]
86    pub fn create() -> Self {
87        Self::create_internal(None)
88    }
89    /// Creates a new active imgui-rs context with a shared font atlas.
90    ///
91    /// # Panics
92    ///
93    /// Panics if an active context already exists
94    #[doc(alias = "CreateContext")]
95    pub fn create_with_shared_font_atlas(shared_font_atlas: Rc<RefCell<SharedFontAtlas>>) -> Self {
96        Self::create_internal(Some(shared_font_atlas))
97    }
98    /// Gets the current context
99    pub fn current() -> Self {
100        let ctx = unsafe { sys::igGetCurrentContext() };
101        Self {
102            raw: ctx,
103            shared_font_atlas: None,
104            ini_filename: None,
105            log_filename: None,
106            platform_name: None,
107            renderer_name: None,
108            clipboard_ctx: Box::new(ClipboardContext::dummy().into()),
109        }
110    }
111    /// Suspends this context so another context can be the active context.
112    #[doc(alias = "CreateContext")]
113    pub fn suspend(self) -> SuspendedContext {
114        let _guard = CTX_MUTEX.lock();
115        assert!(
116            self.is_current_context(),
117            "context to be suspended is not the active context"
118        );
119        clear_current_context();
120        SuspendedContext(self)
121    }
122    /// Returns the path to the ini file, or None if not set
123    pub fn ini_filename(&self) -> Option<PathBuf> {
124        let io = self.io();
125        if io.ini_filename.is_null() {
126            None
127        } else {
128            let s = unsafe { CStr::from_ptr(io.ini_filename) };
129            Some(PathBuf::from(s.to_str().ok()?))
130        }
131    }
132    /// Sets the path to the ini file (default is "imgui.ini")
133    ///
134    /// Pass None to disable automatic .Ini saving.
135    pub fn set_ini_filename<T: Into<Option<PathBuf>>>(&mut self, ini_filename: T) {
136        let ini_filename: Option<PathBuf> = ini_filename.into();
137        let ini_filename = ini_filename.and_then(|v| CString::new(v.to_str()?).ok());
138
139        self.io_mut().ini_filename = ini_filename
140            .as_ref()
141            .map(|x| x.as_ptr())
142            .unwrap_or(ptr::null());
143        self.ini_filename = ini_filename;
144    }
145    /// Returns the path to the log file, or None if not set
146    // TODO: why do we return an `Option<PathBuf>` instead of an `Option<&Path>`?
147    pub fn log_filename(&self) -> Option<PathBuf> {
148        let io = self.io();
149        if io.log_filename.is_null() {
150            None
151        } else {
152            let cstr = unsafe { CStr::from_ptr(io.log_filename) };
153            Some(PathBuf::from(cstr.to_str().ok()?))
154        }
155    }
156    /// Sets the log filename (default is "imgui_log.txt").
157    pub fn set_log_filename<T: Into<Option<PathBuf>>>(&mut self, log_filename: T) {
158        let log_filename = log_filename
159            .into()
160            .and_then(|v| CString::new(v.to_str()?).ok());
161
162        self.io_mut().log_filename = log_filename
163            .as_ref()
164            .map(|x| x.as_ptr())
165            .unwrap_or(ptr::null());
166        self.log_filename = log_filename;
167    }
168    /// Returns the backend platform name, or None if not set
169    pub fn platform_name(&self) -> Option<&str> {
170        let io = self.io();
171        if io.backend_platform_name.is_null() {
172            None
173        } else {
174            let cstr = unsafe { CStr::from_ptr(io.backend_platform_name) };
175            cstr.to_str().ok()
176        }
177    }
178    /// Sets the backend platform name
179    pub fn set_platform_name<T: Into<Option<String>>>(&mut self, platform_name: T) {
180        let platform_name: Option<CString> =
181            platform_name.into().and_then(|v| CString::new(v).ok());
182        self.io_mut().backend_platform_name = platform_name
183            .as_ref()
184            .map(|x| x.as_ptr())
185            .unwrap_or(ptr::null());
186        self.platform_name = platform_name;
187    }
188    /// Returns the backend renderer name, or None if not set
189    pub fn renderer_name(&self) -> Option<&str> {
190        let io = self.io();
191        if io.backend_renderer_name.is_null() {
192            None
193        } else {
194            let cstr = unsafe { CStr::from_ptr(io.backend_renderer_name) };
195            cstr.to_str().ok()
196        }
197    }
198    /// Sets the backend renderer name
199    pub fn set_renderer_name<T: Into<Option<String>>>(&mut self, renderer_name: T) {
200        let renderer_name: Option<CString> =
201            renderer_name.into().and_then(|v| CString::new(v).ok());
202
203        self.io_mut().backend_renderer_name = renderer_name
204            .as_ref()
205            .map(|x| x.as_ptr())
206            .unwrap_or(ptr::null());
207
208        self.renderer_name = renderer_name;
209    }
210    /// Loads settings from a string slice containing settings in .Ini file format
211    #[doc(alias = "LoadIniSettingsFromMemory")]
212    pub fn load_ini_settings(&mut self, data: &str) {
213        unsafe { sys::igLoadIniSettingsFromMemory(data.as_ptr() as *const _, data.len()) }
214    }
215    /// Saves settings to a mutable string buffer in .Ini file format
216    #[doc(alias = "SaveInitSettingsToMemory")]
217    pub fn save_ini_settings(&mut self, buf: &mut String) {
218        let data = unsafe { CStr::from_ptr(sys::igSaveIniSettingsToMemory(ptr::null_mut())) };
219        buf.push_str(&data.to_string_lossy());
220    }
221    /// Sets the clipboard backend used for clipboard operations
222    pub fn set_clipboard_backend<T: ClipboardBackend>(&mut self, backend: T) {
223        let clipboard_ctx: Box<UnsafeCell<_>> = Box::new(ClipboardContext::new(backend).into());
224        let io = self.io_mut();
225        io.set_clipboard_text_fn = Some(crate::clipboard::set_clipboard_text);
226        io.get_clipboard_text_fn = Some(crate::clipboard::get_clipboard_text);
227
228        io.clipboard_user_data = clipboard_ctx.get() as *mut _;
229        self.clipboard_ctx = clipboard_ctx;
230    }
231    fn create_internal(shared_font_atlas: Option<Rc<RefCell<SharedFontAtlas>>>) -> Self {
232        let _guard = CTX_MUTEX.lock();
233        assert!(
234            no_current_context(),
235            "A new active context cannot be created, because another one already exists"
236        );
237
238        let shared_font_atlas_ptr = match &shared_font_atlas {
239            Some(shared_font_atlas) => {
240                let borrowed_font_atlas = shared_font_atlas.borrow();
241                borrowed_font_atlas.0
242            }
243            None => ptr::null_mut(),
244        };
245        // Dear ImGui implicitly sets the current context during igCreateContext if the current
246        // context doesn't exist
247        let raw = unsafe { sys::igCreateContext(shared_font_atlas_ptr) };
248
249        Context {
250            raw,
251            shared_font_atlas,
252            ini_filename: None,
253            log_filename: None,
254            platform_name: None,
255            renderer_name: None,
256            clipboard_ctx: Box::new(ClipboardContext::dummy().into()),
257        }
258    }
259    fn is_current_context(&self) -> bool {
260        let ctx = unsafe { sys::igGetCurrentContext() };
261        self.raw == ctx
262    }
263}
264
265impl Drop for Context {
266    #[doc(alias = "DestroyContext")]
267    fn drop(&mut self) {
268        let _guard = CTX_MUTEX.lock();
269        // If this context is the active context, Dear ImGui automatically deactivates it during
270        // destruction
271        unsafe {
272            sys::igDestroyContext(self.raw);
273        }
274    }
275}
276
277/// A suspended imgui-rs context.
278///
279/// A suspended context retains its state, but is not usable without activating it first.
280///
281/// # Examples
282///
283/// Suspended contexts are not directly very useful, but you can activate them:
284///
285/// ```
286/// let suspended = arcdps_imgui::SuspendedContext::create();
287/// match suspended.activate() {
288///   Ok(ctx) => {
289///     // ctx is now the active context
290///   },
291///   Err(suspended) => {
292///     // activation failed, so you get the suspended context back
293///   }
294/// }
295/// ```
296#[derive(Debug)]
297pub struct SuspendedContext(Context);
298
299impl SuspendedContext {
300    /// Creates a new suspended imgui-rs context.
301    #[doc(alias = "CreateContext")]
302    pub fn create() -> Self {
303        Self::create_internal(None)
304    }
305    /// Creates a new suspended imgui-rs context with a shared font atlas.
306    pub fn create_with_shared_font_atlas(shared_font_atlas: Rc<RefCell<SharedFontAtlas>>) -> Self {
307        Self::create_internal(Some(shared_font_atlas))
308    }
309    /// Attempts to activate this suspended context.
310    ///
311    /// If there is no active context, this suspended context is activated and `Ok` is returned,
312    /// containing the activated context.
313    /// If there is already an active context, nothing happens and `Err` is returned, containing
314    /// the original suspended context.
315    #[doc(alias = "SetCurrentContext")]
316    pub fn activate(self) -> Result<Context, SuspendedContext> {
317        let _guard = CTX_MUTEX.lock();
318        if no_current_context() {
319            unsafe {
320                sys::igSetCurrentContext(self.0.raw);
321            }
322            Ok(self.0)
323        } else {
324            Err(self)
325        }
326    }
327    fn create_internal(shared_font_atlas: Option<Rc<RefCell<SharedFontAtlas>>>) -> Self {
328        let _guard = CTX_MUTEX.lock();
329        let raw = unsafe { sys::igCreateContext(ptr::null_mut()) };
330        let ctx = Context {
331            raw,
332            shared_font_atlas,
333            ini_filename: None,
334            log_filename: None,
335            platform_name: None,
336            renderer_name: None,
337            clipboard_ctx: Box::new(ClipboardContext::dummy().into()),
338        };
339        if ctx.is_current_context() {
340            // Oops, the context was activated -> deactivate
341            clear_current_context();
342        }
343        SuspendedContext(ctx)
344    }
345}
346
347#[test]
348fn test_one_context() {
349    let _guard = crate::test::TEST_MUTEX.lock();
350    let _ctx = Context::create();
351    assert!(!no_current_context());
352}
353
354#[test]
355fn test_drop_clears_current_context() {
356    let _guard = crate::test::TEST_MUTEX.lock();
357    {
358        let _ctx1 = Context::create();
359        assert!(!no_current_context());
360    }
361    assert!(no_current_context());
362    {
363        let _ctx2 = Context::create();
364        assert!(!no_current_context());
365    }
366    assert!(no_current_context());
367}
368
369#[test]
370fn test_new_suspended() {
371    let _guard = crate::test::TEST_MUTEX.lock();
372    let ctx = Context::create();
373    let _suspended = SuspendedContext::create();
374    assert!(ctx.is_current_context());
375    ::std::mem::drop(_suspended);
376    assert!(ctx.is_current_context());
377}
378
379#[test]
380fn test_suspend() {
381    let _guard = crate::test::TEST_MUTEX.lock();
382    let ctx = Context::create();
383    assert!(!no_current_context());
384    let _suspended = ctx.suspend();
385    assert!(no_current_context());
386    let _ctx2 = Context::create();
387}
388
389#[test]
390fn test_drop_suspended() {
391    let _guard = crate::test::TEST_MUTEX.lock();
392    let suspended = Context::create().suspend();
393    assert!(no_current_context());
394    let ctx2 = Context::create();
395    ::std::mem::drop(suspended);
396    assert!(ctx2.is_current_context());
397}
398
399#[test]
400fn test_suspend_activate() {
401    let _guard = crate::test::TEST_MUTEX.lock();
402    let suspended = Context::create().suspend();
403    assert!(no_current_context());
404    let ctx = suspended.activate().unwrap();
405    assert!(ctx.is_current_context());
406}
407
408#[test]
409fn test_suspend_failure() {
410    let _guard = crate::test::TEST_MUTEX.lock();
411    let suspended = Context::create().suspend();
412    let _ctx = Context::create();
413    assert!(suspended.activate().is_err());
414}
415
416#[test]
417fn test_shared_font_atlas() {
418    let _guard = crate::test::TEST_MUTEX.lock();
419    let atlas = Rc::new(RefCell::new(SharedFontAtlas::create()));
420    let suspended1 = SuspendedContext::create_with_shared_font_atlas(atlas.clone());
421    let mut ctx2 = Context::create_with_shared_font_atlas(atlas);
422    {
423        let _borrow = ctx2.fonts();
424    }
425    let _suspended2 = ctx2.suspend();
426    let mut ctx = suspended1.activate().unwrap();
427    let _borrow = ctx.fonts();
428}
429
430#[test]
431#[should_panic]
432fn test_shared_font_atlas_borrow_panic() {
433    let _guard = crate::test::TEST_MUTEX.lock();
434    let atlas = Rc::new(RefCell::new(SharedFontAtlas::create()));
435    let _suspended = SuspendedContext::create_with_shared_font_atlas(atlas.clone());
436    let mut ctx = Context::create_with_shared_font_atlas(atlas.clone());
437    let _borrow1 = atlas.borrow();
438    let _borrow2 = ctx.fonts();
439}
440
441#[test]
442fn test_ini_load_save() {
443    let (_guard, mut ctx) = crate::test::test_ctx();
444    let data = "[Window][Debug##Default]
445Pos=60,60
446Size=400,400
447Collapsed=0";
448    ctx.load_ini_settings(data);
449    let mut buf = String::new();
450    ctx.save_ini_settings(&mut buf);
451    assert_eq!(data.trim(), buf.trim());
452}
453
454#[test]
455fn test_default_ini_filename() {
456    let _guard = crate::test::TEST_MUTEX.lock();
457    let ctx = Context::create();
458    assert_eq!(ctx.ini_filename(), Some(PathBuf::from("imgui.ini")));
459}
460
461#[test]
462fn test_set_ini_filename() {
463    let (_guard, mut ctx) = crate::test::test_ctx();
464    ctx.set_ini_filename(Some(PathBuf::from("test.ini")));
465    assert_eq!(ctx.ini_filename(), Some(PathBuf::from("test.ini")));
466}
467
468#[test]
469fn test_default_log_filename() {
470    let _guard = crate::test::TEST_MUTEX.lock();
471    let ctx = Context::create();
472    assert_eq!(ctx.log_filename(), Some(PathBuf::from("imgui_log.txt")));
473}
474
475#[test]
476fn test_set_log_filename() {
477    let (_guard, mut ctx) = crate::test::test_ctx();
478    ctx.set_log_filename(Some(PathBuf::from("test.log")));
479    assert_eq!(ctx.log_filename(), Some(PathBuf::from("test.log")));
480}
481
482impl Context {
483    /// Returns an immutable reference to the inputs/outputs object
484    pub fn io(&self) -> &Io {
485        unsafe {
486            // safe because Io is a transparent wrapper around sys::ImGuiIO
487            &*(sys::igGetIO() as *const Io)
488        }
489    }
490    /// Returns a mutable reference to the inputs/outputs object
491    pub fn io_mut(&mut self) -> &mut Io {
492        unsafe {
493            // safe because Io is a transparent wrapper around sys::ImGuiIO
494            &mut *(sys::igGetIO() as *mut Io)
495        }
496    }
497    /// Returns an immutable reference to the user interface style
498    #[doc(alias = "GetStyle")]
499    pub fn style(&self) -> &Style {
500        unsafe {
501            // safe because Style is a transparent wrapper around sys::ImGuiStyle
502            &*(sys::igGetStyle() as *const Style)
503        }
504    }
505    /// Returns a mutable reference to the user interface style
506    #[doc(alias = "GetStyle")]
507    pub fn style_mut(&mut self) -> &mut Style {
508        unsafe {
509            // safe because Style is a transparent wrapper around sys::ImGuiStyle
510            &mut *(sys::igGetStyle() as *mut Style)
511        }
512    }
513    /// Returns a mutable reference to the font atlas.
514    ///
515    /// # Panics
516    ///
517    /// Panics if the context uses a shared font atlas that is already borrowed
518    pub fn fonts(&mut self) -> FontAtlasRefMut<'_> {
519        match self.shared_font_atlas {
520            Some(ref font_atlas) => FontAtlasRefMut::Shared(font_atlas.borrow_mut()),
521            None => unsafe {
522                // safe because FontAtlas is a transparent wrapper around sys::ImFontAtlas
523                let fonts = &mut *(self.io_mut().fonts as *mut FontAtlas);
524                FontAtlasRefMut::Owned(fonts)
525            },
526        }
527    }
528    /// Starts a new frame and returns an `Ui` instance for constructing a user interface.
529    ///
530    /// # Panics
531    ///
532    /// Panics if the context uses a shared font atlas that is already borrowed
533    #[doc(alias = "NewFame")]
534    pub fn frame(&mut self) -> Ui<'_> {
535        // Clear default font if it no longer exists. This could be an error in the future
536        let default_font = self.io().font_default;
537        if !default_font.is_null() && self.fonts().get_font(FontId(default_font)).is_none() {
538            self.io_mut().font_default = ptr::null_mut();
539        }
540        // NewFrame/Render/EndFrame mutate the font atlas so we need exclusive access to it
541        let font_atlas = self
542            .shared_font_atlas
543            .as_ref()
544            .map(|font_atlas| font_atlas.borrow_mut());
545        // TODO: precondition checks
546        unsafe {
547            sys::igNewFrame();
548        }
549        Ui {
550            ctx: self,
551            font_atlas,
552            buffer: crate::UiBuffer::new(1024).into(),
553        }
554    }
555}