Skip to main content

dear_imnodes/context/
global.rs

1use super::{Context, EditorContext, ImNodesScope};
2use crate::sys;
3use dear_imgui_rs::Context as ImGuiContext;
4use dear_imgui_rs::sys as imgui_sys;
5use std::marker::PhantomData;
6use std::os::raw::c_char;
7use std::ptr::NonNull;
8
9/// An ImNodes editor context bound to a specific ImNodes context.
10///
11/// Prefer using this type over calling methods directly on `EditorContext` to avoid
12/// accidentally operating on the wrong global ImNodes context.
13pub struct BoundEditor<'a> {
14    scope: ImNodesScope,
15    _ctx: &'a Context,
16    _editor: &'a EditorContext,
17}
18
19impl<'a> BoundEditor<'a> {
20    #[inline]
21    fn bind(&self) {
22        self.scope.bind();
23    }
24
25    #[inline]
26    pub fn set_current(&self) {
27        self.bind();
28    }
29
30    pub fn get_panning(&self) -> [f32; 2] {
31        self.bind();
32        let out = unsafe { crate::compat_ffi::imnodes_EditorContextGetPanning() };
33        [out.x, out.y]
34    }
35
36    pub fn reset_panning(&self, pos: [f32; 2]) {
37        self.bind();
38        unsafe {
39            sys::imnodes_EditorContextResetPanning(sys::ImVec2_c {
40                x: pos[0],
41                y: pos[1],
42            })
43        };
44    }
45
46    pub fn move_to_node(&self, node_id: i32) {
47        self.bind();
48        unsafe { sys::imnodes_EditorContextMoveToNode(node_id) };
49    }
50
51    /// Save this editor's state to an INI string.
52    pub fn save_state_to_ini_string(&self) -> String {
53        self.bind();
54        unsafe {
55            let mut size: usize = 0;
56            let ptr =
57                sys::imnodes_SaveEditorStateToIniString(self._editor.raw, &mut size as *mut usize);
58            if ptr.is_null() || size == 0 {
59                return String::new();
60            }
61            let mut slice = std::slice::from_raw_parts(ptr as *const u8, size);
62            if slice.last() == Some(&0) {
63                slice = &slice[..slice.len().saturating_sub(1)];
64            }
65            String::from_utf8_lossy(slice).into_owned()
66        }
67    }
68
69    /// Load this editor's state from an INI string.
70    pub fn load_state_from_ini_string(&self, data: &str) {
71        self.bind();
72        unsafe {
73            sys::imnodes_LoadEditorStateFromIniString(
74                self._editor.raw,
75                data.as_ptr() as *const c_char,
76                data.len(),
77            )
78        }
79    }
80
81    /// Save this editor's state directly to an INI file.
82    pub fn save_state_to_ini_file(&self, file_name: &str) {
83        self.bind();
84        let file_name = if file_name.contains('\0') {
85            ""
86        } else {
87            file_name
88        };
89        dear_imgui_rs::with_scratch_txt(file_name, |ptr| unsafe {
90            sys::imnodes_SaveEditorStateToIniFile(self._editor.raw, ptr)
91        })
92    }
93
94    /// Load this editor's state from an INI file.
95    pub fn load_state_from_ini_file(&self, file_name: &str) {
96        self.bind();
97        let file_name = if file_name.contains('\0') {
98            ""
99        } else {
100            file_name
101        };
102        dear_imgui_rs::with_scratch_txt(file_name, |ptr| unsafe {
103            sys::imnodes_LoadEditorStateFromIniFile(self._editor.raw, ptr)
104        })
105    }
106}
107
108impl Context {
109    /// Try to create a new ImNodes context bound to the current Dear ImGui context
110    pub fn try_create(imgui: &ImGuiContext) -> dear_imgui_rs::ImGuiResult<Self> {
111        let imgui_ctx_raw = imgui.as_raw();
112        let imgui_alive = imgui.alive_token();
113        unsafe { sys::imnodes_SetImGuiContext(imgui_ctx_raw) };
114        let raw = unsafe { sys::imnodes_CreateContext() };
115        if raw.is_null() {
116            return Err(dear_imgui_rs::ImGuiError::context_creation(
117                "imnodes_CreateContext returned null",
118            ));
119        }
120        unsafe { sys::imnodes_SetCurrentContext(raw) };
121        Ok(Self {
122            raw,
123            imgui_ctx_raw,
124            imgui_alive,
125            _not_send_sync: PhantomData,
126        })
127    }
128
129    /// Create a new ImNodes context (panics on error)
130    pub fn create(imgui: &ImGuiContext) -> Self {
131        Self::try_create(imgui).expect("Failed to create ImNodes context")
132    }
133
134    /// Set as current ImNodes context
135    pub fn set_as_current(&self) {
136        assert!(
137            self.imgui_alive.is_alive(),
138            "dear-imnodes: ImGui context has been dropped"
139        );
140        unsafe { sys::imnodes_SetImGuiContext(self.imgui_ctx_raw) };
141        unsafe { sys::imnodes_SetCurrentContext(self.raw) };
142    }
143
144    /// Return the raw pointer for this context.
145    pub fn as_raw(&self) -> *mut sys::ImNodesContext {
146        self.raw
147    }
148
149    /// Return the raw Dear ImGui context pointer this ImNodes context is bound to.
150    pub fn imgui_context_raw(&self) -> *mut imgui_sys::ImGuiContext {
151        self.imgui_ctx_raw
152    }
153
154    /// Get the currently active ImNodes context.
155    pub fn current_raw() -> Option<NonNull<sys::ImNodesContext>> {
156        let ptr = unsafe { sys::imnodes_GetCurrentContext() };
157        NonNull::new(ptr)
158    }
159
160    /// Bind an `EditorContext` to this ImNodes context.
161    pub fn bind_editor<'a>(&'a self, editor: &'a EditorContext) -> BoundEditor<'a> {
162        if let Some(bound) = editor.bound_ctx_raw {
163            assert_eq!(
164                bound, self.raw,
165                "dear-imnodes: EditorContext is bound to a different ImNodes context"
166            );
167        }
168        let scope = ImNodesScope {
169            imgui_ctx_raw: self.imgui_ctx_raw,
170            imgui_alive: self.imgui_alive.clone(),
171            ctx_raw: self.raw,
172            editor_raw: Some(editor.raw),
173        };
174        BoundEditor {
175            scope,
176            _ctx: self,
177            _editor: editor,
178        }
179    }
180
181    pub fn try_create_editor_context(&self) -> dear_imgui_rs::ImGuiResult<EditorContext> {
182        assert!(
183            self.imgui_alive.is_alive(),
184            "dear-imnodes: ImGui context has been dropped"
185        );
186        unsafe {
187            sys::imnodes_SetImGuiContext(self.imgui_ctx_raw);
188            sys::imnodes_SetCurrentContext(self.raw);
189        }
190        let raw = unsafe { sys::imnodes_EditorContextCreate() };
191        if raw.is_null() {
192            return Err(dear_imgui_rs::ImGuiError::context_creation(
193                "imnodes_EditorContextCreate returned null",
194            ));
195        }
196        Ok(EditorContext {
197            raw,
198            bound_ctx_raw: Some(self.raw),
199            bound_imgui_ctx_raw: Some(self.imgui_ctx_raw),
200            bound_imgui_alive: Some(self.imgui_alive.clone()),
201            _not_send_sync: PhantomData,
202        })
203    }
204
205    pub fn create_editor_context(&self) -> EditorContext {
206        self.try_create_editor_context()
207            .expect("Failed to create ImNodes editor context")
208    }
209}
210
211impl Drop for Context {
212    fn drop(&mut self) {
213        if !self.raw.is_null() {
214            if self.imgui_alive.is_alive() {
215                unsafe {
216                    sys::imnodes_SetImGuiContext(self.imgui_ctx_raw);
217                    if sys::imnodes_GetCurrentContext() == self.raw {
218                        sys::imnodes_SetCurrentContext(std::ptr::null_mut());
219                    }
220                }
221            } else {
222                // Avoid calling `SetImGuiContext` with a dangling pointer.
223                // Best-effort cleanup: destroy the ImNodes context without rebinding ImGui.
224                unsafe {
225                    if sys::imnodes_GetCurrentContext() == self.raw {
226                        sys::imnodes_SetCurrentContext(std::ptr::null_mut());
227                    }
228                }
229            }
230            unsafe { sys::imnodes_DestroyContext(self.raw) };
231        }
232    }
233}
234
235// ImNodes context interacts with Dear ImGui state and is not thread-safe.
236
237impl EditorContext {
238    #[inline]
239    fn bind_current(&self) {
240        let imgui_ctx_raw = unsafe { imgui_sys::igGetCurrentContext() };
241        assert!(
242            !imgui_ctx_raw.is_null(),
243            "dear-imnodes: EditorContext methods require an active ImGui context"
244        );
245        unsafe { sys::imnodes_SetImGuiContext(imgui_ctx_raw) };
246        assert!(
247            !unsafe { sys::imnodes_GetCurrentContext() }.is_null(),
248            "dear-imnodes: EditorContext methods require an active ImNodes context (call Context::set_as_current or use NodesUi)"
249        );
250        unsafe { sys::imnodes_EditorContextSet(self.raw) };
251    }
252
253    #[deprecated(
254        note = "Deprecated: will be removed in 0.11.0. Use `ctx.try_create_editor_context()`."
255    )]
256    pub fn try_create() -> dear_imgui_rs::ImGuiResult<Self> {
257        let raw = unsafe { sys::imnodes_EditorContextCreate() };
258        if raw.is_null() {
259            return Err(dear_imgui_rs::ImGuiError::context_creation(
260                "imnodes_EditorContextCreate returned null",
261            ));
262        }
263        Ok(Self {
264            raw,
265            bound_ctx_raw: None,
266            bound_imgui_ctx_raw: None,
267            bound_imgui_alive: None,
268            _not_send_sync: PhantomData,
269        })
270    }
271
272    #[deprecated(
273        note = "Deprecated: will be removed in 0.11.0. Use `ctx.create_editor_context()`."
274    )]
275    pub fn create() -> Self {
276        #[allow(deprecated)]
277        {
278            Self::try_create().expect("Failed to create ImNodes editor context")
279        }
280    }
281
282    #[deprecated(
283        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).set_current()`."
284    )]
285    pub fn set_current(&self) {
286        self.bind_current();
287    }
288
289    #[deprecated(
290        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).get_panning()`."
291    )]
292    pub fn get_panning(&self) -> [f32; 2] {
293        self.bind_current();
294        let out = unsafe { crate::compat_ffi::imnodes_EditorContextGetPanning() };
295        [out.x, out.y]
296    }
297
298    #[deprecated(
299        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).reset_panning(...)`."
300    )]
301    pub fn reset_panning(&self, pos: [f32; 2]) {
302        self.bind_current();
303        unsafe {
304            sys::imnodes_EditorContextResetPanning(sys::ImVec2_c {
305                x: pos[0],
306                y: pos[1],
307            })
308        };
309    }
310
311    #[deprecated(
312        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).move_to_node(...)`."
313    )]
314    pub fn move_to_node(&self, node_id: i32) {
315        self.bind_current();
316        unsafe { sys::imnodes_EditorContextMoveToNode(node_id) };
317    }
318
319    /// Save this editor's state to an INI string
320    #[deprecated(
321        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).save_state_to_ini_string()`."
322    )]
323    pub fn save_state_to_ini_string(&self) -> String {
324        self.bind_current();
325        unsafe {
326            let mut size: usize = 0;
327            let ptr = sys::imnodes_SaveEditorStateToIniString(self.raw, &mut size as *mut usize);
328            if ptr.is_null() || size == 0 {
329                return String::new();
330            }
331            let mut slice = std::slice::from_raw_parts(ptr as *const u8, size);
332            if slice.last() == Some(&0) {
333                slice = &slice[..slice.len().saturating_sub(1)];
334            }
335            String::from_utf8_lossy(slice).into_owned()
336        }
337    }
338
339    /// Load this editor's state from an INI string
340    #[deprecated(
341        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).load_state_from_ini_string(...)`."
342    )]
343    pub fn load_state_from_ini_string(&self, data: &str) {
344        self.bind_current();
345        unsafe {
346            sys::imnodes_LoadEditorStateFromIniString(
347                self.raw,
348                data.as_ptr() as *const c_char,
349                data.len(),
350            )
351        }
352    }
353
354    /// Save this editor's state directly to an INI file
355    #[deprecated(
356        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).save_state_to_ini_file(...)`."
357    )]
358    pub fn save_state_to_ini_file(&self, file_name: &str) {
359        self.bind_current();
360        let file_name = if file_name.contains('\0') {
361            ""
362        } else {
363            file_name
364        };
365        dear_imgui_rs::with_scratch_txt(file_name, |ptr| unsafe {
366            sys::imnodes_SaveEditorStateToIniFile(self.raw, ptr)
367        })
368    }
369
370    /// Load this editor's state from an INI file
371    #[deprecated(
372        note = "Deprecated: will be removed in 0.11.0. Use `ctx.bind_editor(&editor).load_state_from_ini_file(...)`."
373    )]
374    pub fn load_state_from_ini_file(&self, file_name: &str) {
375        self.bind_current();
376        let file_name = if file_name.contains('\0') {
377            ""
378        } else {
379            file_name
380        };
381        dear_imgui_rs::with_scratch_txt(file_name, |ptr| unsafe {
382            sys::imnodes_LoadEditorStateFromIniFile(self.raw, ptr)
383        })
384    }
385}
386
387impl Drop for EditorContext {
388    fn drop(&mut self) {
389        if !self.raw.is_null() {
390            if let Some(alive) = &self.bound_imgui_alive {
391                if !alive.is_alive() {
392                    // Avoid calling into ImGui allocators after the context has been dropped.
393                    // Best-effort: leak the editor context instead of risking UB.
394                    return;
395                }
396            }
397            if let Some(imgui_ctx_raw) = self.bound_imgui_ctx_raw {
398                unsafe { sys::imnodes_SetImGuiContext(imgui_ctx_raw) };
399            }
400            if let Some(ctx_raw) = self.bound_ctx_raw {
401                unsafe { sys::imnodes_SetCurrentContext(ctx_raw) };
402            }
403            unsafe { sys::imnodes_EditorContextFree(self.raw) };
404        }
405    }
406}
407
408// EditorContext is also not thread-safe to move/share across threads.