Skip to main content

dear_implot3d/
context.rs

1use crate::ui::{Plot3DContextBinding, Plot3DUi};
2use crate::{imgui_sys, sys};
3use dear_imgui_rs::{Context, Ui};
4
5/// Plot3D context wrapper
6///
7/// This manages the ImPlot3D context lifetime. Create one instance per application
8/// and keep it alive for the duration of your program.
9///
10/// # Example
11///
12/// ```no_run
13/// use dear_imgui_rs::*;
14/// use dear_implot3d::*;
15///
16/// let mut imgui_ctx = Context::create();
17/// let plot3d_ctx = Plot3DContext::create(&imgui_ctx);
18///
19/// // In your main loop:
20/// let ui = imgui_ctx.frame();
21/// let plot_ui = plot3d_ctx.get_plot_ui(&ui);
22/// ```
23pub struct Plot3DContext {
24    pub(crate) raw: *mut sys::ImPlot3DContext,
25    pub(crate) imgui_ctx_raw: *mut imgui_sys::ImGuiContext,
26    pub(crate) imgui_alive: Option<dear_imgui_rs::ContextAliveToken>,
27    pub(crate) owns_context: bool,
28}
29
30impl Plot3DContext {
31    /// Try to create a new ImPlot3D context.
32    ///
33    /// This should be called once after creating your ImGui context.
34    pub fn try_create(imgui: &Context) -> dear_imgui_rs::ImGuiResult<Self> {
35        let imgui_ctx_raw = imgui.as_raw();
36        let imgui_alive = Some(imgui.alive_token());
37        assert_eq!(
38            unsafe { imgui_sys::igGetCurrentContext() },
39            imgui_ctx_raw,
40            "dear-implot3d: Plot3DContext must be created with the currently-active ImGui context"
41        );
42        unsafe {
43            let ctx = sys::ImPlot3D_CreateContext();
44            if ctx.is_null() {
45                return Err(dear_imgui_rs::ImGuiError::context_creation(
46                    "ImPlot3D_CreateContext returned null",
47                ));
48            }
49
50            // Ensure our new context is set as current even if another existed.
51            sys::ImPlot3D_SetCurrentContext(ctx);
52            Ok(Self {
53                raw: ctx,
54                imgui_ctx_raw,
55                imgui_alive,
56                owns_context: true,
57            })
58        }
59    }
60
61    /// Create a new ImPlot3D context (panics on error).
62    pub fn create(imgui: &Context) -> Self {
63        Self::try_create(imgui).expect("Failed to create ImPlot3D context")
64    }
65
66    /// Get the current ImPlot3D context as a non-owning raw-context wrapper.
67    ///
68    /// Returns None if no context is current.
69    ///
70    /// # Safety
71    ///
72    /// The returned value does not own the current ImPlot3D context and cannot prove that the
73    /// associated ImGui context remains alive. The caller must ensure the raw ImPlot3D and ImGui
74    /// contexts outlive the returned wrapper and are used on the same thread/context stack.
75    pub unsafe fn current() -> Option<Self> {
76        let raw = unsafe { sys::ImPlot3D_GetCurrentContext() };
77        if raw.is_null() {
78            None
79        } else {
80            Some(Self {
81                raw,
82                imgui_ctx_raw: unsafe { imgui_sys::igGetCurrentContext() },
83                imgui_alive: None,
84                owns_context: false,
85            })
86        }
87    }
88
89    /// Set this context as the current ImPlot3D context.
90    pub fn set_as_current(&self) {
91        self.assert_imgui_alive();
92        self.binding().bind();
93    }
94
95    /// Get a raw pointer to the current ImPlot3D style
96    ///
97    /// This is an advanced function for direct style manipulation.
98    /// Prefer using the safe style functions in the `style` module.
99    pub fn raw_style_mut() -> *mut sys::ImPlot3DStyle {
100        unsafe { sys::ImPlot3D_GetStyle() }
101    }
102
103    /// Get the raw ImPlot3D context pointer.
104    ///
105    /// # Safety
106    ///
107    /// The caller must ensure the pointer is used safely and not stored beyond the lifetime of
108    /// this context wrapper.
109    pub unsafe fn raw(&self) -> *mut sys::ImPlot3DContext {
110        self.raw
111    }
112
113    /// Get a per-frame plotting interface
114    ///
115    /// Call this once per frame to get access to plotting functions.
116    /// The returned `Plot3DUi` is tied to the lifetime of the `Ui` frame.
117    pub fn get_plot_ui<'ui>(&self, ui: &'ui Ui) -> Plot3DUi<'ui> {
118        self.set_as_current();
119        Plot3DUi {
120            _ui: ui,
121            binding: Plot3DContextBinding {
122                plot_ctx_raw: self.raw,
123                imgui_ctx_raw: self.imgui_ctx_raw,
124            },
125            imgui_alive: self.imgui_alive.clone(),
126        }
127    }
128
129    fn assert_imgui_alive(&self) {
130        if let Some(alive) = &self.imgui_alive {
131            assert!(
132                alive.is_alive(),
133                "dear-implot3d: ImGui context has been dropped"
134            );
135        }
136    }
137
138    fn binding(&self) -> Plot3DContextBinding {
139        Plot3DContextBinding {
140            plot_ctx_raw: self.raw,
141            imgui_ctx_raw: self.imgui_ctx_raw,
142        }
143    }
144}
145
146impl Drop for Plot3DContext {
147    fn drop(&mut self) {
148        if !self.owns_context || self.raw.is_null() {
149            return;
150        }
151
152        if let Some(alive) = &self.imgui_alive {
153            if !alive.is_alive() {
154                // Avoid calling into ImGui allocators after the context has been dropped.
155                // Best-effort: leak the Plot3D context instead of risking UB.
156                return;
157            }
158        }
159
160        unsafe {
161            let prev_imgui = imgui_sys::igGetCurrentContext();
162            imgui_sys::igSetCurrentContext(self.imgui_ctx_raw);
163
164            if sys::ImPlot3D_GetCurrentContext() == self.raw {
165                sys::ImPlot3D_SetCurrentContext(std::ptr::null_mut());
166            }
167            sys::ImPlot3D_DestroyContext(self.raw);
168
169            imgui_sys::igSetCurrentContext(prev_imgui);
170        }
171    }
172}
173
174#[cfg(test)]
175mod tests {
176    use super::Plot3DContext;
177    use crate::ui::Plot3DContextBinding;
178    use crate::{Context, sys};
179    use std::mem::{align_of, size_of};
180    use std::ptr;
181    use std::sync::{Mutex, OnceLock};
182
183    fn test_guard() -> std::sync::MutexGuard<'static, ()> {
184        static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
185        GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
186    }
187
188    #[test]
189    fn ffi_layout_implot3d_point_is_3_f64() {
190        assert_eq!(size_of::<sys::ImPlot3DPoint>(), 3 * size_of::<f64>());
191        assert_eq!(align_of::<sys::ImPlot3DPoint>(), align_of::<f64>());
192    }
193
194    #[test]
195    fn plot3d_ui_binds_own_context() {
196        let _guard = test_guard();
197        let imgui = Context::create();
198        let plot_a = Plot3DContext::create(&imgui);
199        let raw_a = plot_a.raw;
200        let plot_b = Plot3DContext::create(&imgui);
201        let raw_b = plot_b.raw;
202
203        unsafe { sys::ImPlot3D_SetCurrentContext(raw_b) };
204
205        Plot3DContextBinding {
206            plot_ctx_raw: plot_a.raw,
207            imgui_ctx_raw: plot_a.imgui_ctx_raw,
208        }
209        .bind();
210
211        assert_eq!(unsafe { sys::ImPlot3D_GetCurrentContext() }, raw_a);
212    }
213
214    #[test]
215    fn plot3d_current_returns_none_without_current_context() {
216        let _guard = test_guard();
217        let _imgui = Context::create();
218
219        unsafe { sys::ImPlot3D_SetCurrentContext(ptr::null_mut()) };
220        assert!(unsafe { Plot3DContext::current() }.is_none());
221    }
222
223    #[test]
224    fn plot3d_current_wrapper_reports_current_context() {
225        let _guard = test_guard();
226        let imgui = Context::create();
227        let plot = Plot3DContext::create(&imgui);
228        let raw = plot.raw;
229
230        let current =
231            unsafe { Plot3DContext::current() }.expect("expected current ImPlot3D context");
232
233        assert_eq!(unsafe { current.raw() }, raw);
234    }
235
236    #[test]
237    fn plot3d_ui_rejects_wrong_imgui_context() {
238        let _guard = test_guard();
239        let imgui_a = Context::create();
240        let plot_a = Plot3DContext::create(&imgui_a);
241        let suspended_a = imgui_a.suspend();
242        let imgui_b = Context::create();
243
244        let previous = unsafe { dear_imgui_rs::sys::igGetCurrentContext() };
245        struct RestoreCurrentContext(*mut dear_imgui_rs::sys::ImGuiContext);
246        impl Drop for RestoreCurrentContext {
247            fn drop(&mut self) {
248                unsafe { dear_imgui_rs::sys::igSetCurrentContext(self.0) };
249            }
250        }
251        let _restore = RestoreCurrentContext(previous);
252
253        assert_eq!(
254            unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
255            imgui_b.as_raw()
256        );
257        let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
258            Plot3DContextBinding {
259                plot_ctx_raw: plot_a.raw,
260                imgui_ctx_raw: plot_a.imgui_ctx_raw,
261            }
262            .bind();
263        }))
264        .expect_err("expected wrong ImGui context to panic");
265
266        let message = panic
267            .downcast_ref::<String>()
268            .map(String::as_str)
269            .or_else(|| panic.downcast_ref::<&'static str>().copied())
270            .unwrap_or("");
271        assert!(message.contains("Plot3DUi must be used with the currently-active ImGui context"));
272        drop(plot_a);
273        drop(_restore);
274        drop(imgui_b);
275        drop(suspended_a);
276    }
277}