Skip to main content

dear_imnodes/context/
post.rs

1use super::{Context, ImNodesScope, NodeEditor};
2use crate::sys;
3use dear_imgui_rs::Ui;
4use std::marker::PhantomData;
5use std::os::raw::c_char;
6use std::rc::Rc;
7
8/// Post-editor queries (must be called after EndNodeEditor)
9pub struct PostEditor<'ui> {
10    #[allow(dead_code)]
11    pub(super) _ui: &'ui Ui,
12    #[allow(dead_code)]
13    pub(super) _ctx: &'ui Context,
14    pub(super) scope: ImNodesScope,
15    pub(super) editor_hovered: bool,
16    pub(super) hovered_node: Option<i32>,
17    pub(super) hovered_link: Option<i32>,
18    pub(super) hovered_pin: Option<i32>,
19    pub(super) link_created: Option<crate::LinkCreated>,
20    pub(super) link_created_ex: Option<crate::LinkCreatedEx>,
21    pub(super) link_destroyed: Option<i32>,
22    pub(super) any_attribute_active: Option<i32>,
23    pub(super) link_started: Option<i32>,
24    pub(super) link_dropped_excluding_detached: Option<i32>,
25    pub(super) link_dropped_including_detached: Option<i32>,
26    pub(super) _not_send_sync: PhantomData<Rc<()>>,
27}
28
29impl<'ui> NodeEditor<'ui> {
30    /// Explicitly end the node editor and return post-editor query handle
31    pub fn end(mut self) -> PostEditor<'ui> {
32        if !self.ended {
33            self.bind();
34            unsafe { sys::imnodes_EndNodeEditor() };
35            self.ended = true;
36        }
37
38        // Capture hover state immediately after EndNodeEditor while the current ImGui window
39        // is still the editor host window. This avoids calling ImNodes hover queries later
40        // from a different window (e.g. a popup), which can lead to inconsistent behavior.
41        self.bind();
42        let editor_hovered = unsafe { sys::imnodes_IsEditorHovered() };
43        let mut hovered_node = 0i32;
44        let hovered_node = if unsafe { sys::imnodes_IsNodeHovered(&mut hovered_node) } {
45            Some(hovered_node)
46        } else {
47            None
48        };
49        let mut hovered_link = 0i32;
50        let hovered_link = if unsafe { sys::imnodes_IsLinkHovered(&mut hovered_link) } {
51            Some(hovered_link)
52        } else {
53            None
54        };
55        let mut hovered_pin = 0i32;
56        let hovered_pin = if unsafe { sys::imnodes_IsPinHovered(&mut hovered_pin) } {
57            Some(hovered_pin)
58        } else {
59            None
60        };
61
62        // Capture post-editor interaction events immediately after EndNodeEditor for the same reason
63        // as hover state (avoid calling these queries from a different ImGui window later in the frame).
64        let link_created_ex = {
65            let mut start_node = 0i32;
66            let mut start_attr = 0i32;
67            let mut end_node = 0i32;
68            let mut end_attr = 0i32;
69            let mut from_snap = false;
70            let created = unsafe {
71                sys::imnodes_IsLinkCreated_IntPtr(
72                    &mut start_node as *mut i32,
73                    &mut start_attr as *mut i32,
74                    &mut end_node as *mut i32,
75                    &mut end_attr as *mut i32,
76                    &mut from_snap as *mut bool,
77                )
78            };
79            if created {
80                Some(crate::LinkCreatedEx {
81                    start_node,
82                    start_attr,
83                    end_node,
84                    end_attr,
85                    from_snap,
86                })
87            } else {
88                None
89            }
90        };
91        let link_created = link_created_ex.map(|ex| crate::LinkCreated {
92            start_attr: ex.start_attr,
93            end_attr: ex.end_attr,
94            from_snap: ex.from_snap,
95        });
96
97        let link_destroyed = {
98            let mut id = 0i32;
99            if unsafe { sys::imnodes_IsLinkDestroyed(&mut id as *mut i32) } {
100                Some(id)
101            } else {
102                None
103            }
104        };
105
106        let any_attribute_active = {
107            let mut id = 0i32;
108            if unsafe { sys::imnodes_IsAnyAttributeActive(&mut id) } {
109                Some(id)
110            } else {
111                None
112            }
113        };
114
115        let link_started = {
116            let mut id = 0i32;
117            if unsafe { sys::imnodes_IsLinkStarted(&mut id) } {
118                Some(id)
119            } else {
120                None
121            }
122        };
123
124        // Only call `IsLinkDropped` twice if the first query returned false, to avoid any
125        // potential "consume-on-true" behavior in upstream implementations.
126        let link_dropped_excluding_detached = {
127            let mut id = 0i32;
128            if unsafe { sys::imnodes_IsLinkDropped(&mut id, false) } {
129                Some(id)
130            } else {
131                None
132            }
133        };
134        let link_dropped_including_detached = if let Some(id) = link_dropped_excluding_detached {
135            Some(id)
136        } else {
137            let mut id = 0i32;
138            if unsafe { sys::imnodes_IsLinkDropped(&mut id, true) } {
139                Some(id)
140            } else {
141                None
142            }
143        };
144
145        PostEditor {
146            _ui: self._ui,
147            _ctx: self._ctx,
148            scope: self.scope.clone(),
149            editor_hovered,
150            hovered_node,
151            hovered_link,
152            hovered_pin,
153            link_created,
154            link_created_ex,
155            link_destroyed,
156            any_attribute_active,
157            link_started,
158            link_dropped_excluding_detached,
159            link_dropped_including_detached,
160            _not_send_sync: PhantomData,
161        }
162    }
163}
164
165impl<'ui> PostEditor<'ui> {
166    #[inline]
167    fn bind(&self) {
168        self.scope.bind();
169    }
170
171    /// Save current editor state to an INI string
172    pub fn save_state_to_ini_string(&self) -> String {
173        // Safety: ImNodes returns a pointer to an internal, null-terminated INI
174        // buffer and writes its size into `size`. The pointer remains valid
175        // until the next save/load call on the same editor, which we do not
176        // perform while this slice is alive.
177        unsafe {
178            self.bind();
179            let mut size: usize = 0;
180            let ptr = sys::imnodes_SaveCurrentEditorStateToIniString(&mut size as *mut usize);
181            if ptr.is_null() || size == 0 {
182                return String::new();
183            }
184            let mut slice = std::slice::from_raw_parts(ptr as *const u8, size);
185            if slice.last() == Some(&0) {
186                slice = &slice[..slice.len().saturating_sub(1)];
187            }
188            String::from_utf8_lossy(slice).into_owned()
189        }
190    }
191
192    /// Load editor state from an INI string
193    pub fn load_state_from_ini_string(&self, data: &str) {
194        // Safety: ImNodes expects a pointer to a valid UTF-8 buffer and its
195        // length; `data.as_ptr()` and `data.len()` satisfy this for the
196        // duration of the call.
197        unsafe {
198            self.bind();
199            sys::imnodes_LoadCurrentEditorStateFromIniString(
200                data.as_ptr() as *const c_char,
201                data.len(),
202            );
203        }
204    }
205
206    /// Save/Load current editor state to/from INI file
207    pub fn save_state_to_ini_file(&self, file_name: &str) {
208        let file_name = if file_name.contains('\0') {
209            ""
210        } else {
211            file_name
212        };
213        // Safety: ImNodes reads a NUL-terminated string for the duration of the call.
214        self.bind();
215        dear_imgui_rs::with_scratch_txt(file_name, |ptr| unsafe {
216            sys::imnodes_SaveCurrentEditorStateToIniFile(ptr)
217        })
218    }
219
220    pub fn load_state_from_ini_file(&self, file_name: &str) {
221        let file_name = if file_name.contains('\0') {
222            ""
223        } else {
224            file_name
225        };
226        // Safety: see `save_state_to_ini_file`.
227        self.bind();
228        dear_imgui_rs::with_scratch_txt(file_name, |ptr| unsafe {
229            sys::imnodes_LoadCurrentEditorStateFromIniFile(ptr)
230        })
231    }
232
233    /// Selection helpers per id
234    pub fn select_node(&self, node_id: i32) {
235        self.bind();
236        unsafe { sys::imnodes_SelectNode(node_id) }
237    }
238
239    pub fn clear_node_selection_of(&self, node_id: i32) {
240        self.bind();
241        unsafe { sys::imnodes_ClearNodeSelection_Int(node_id) }
242    }
243
244    pub fn is_node_selected(&self, node_id: i32) -> bool {
245        self.bind();
246        unsafe { sys::imnodes_IsNodeSelected(node_id) }
247    }
248
249    pub fn select_link(&self, link_id: i32) {
250        self.bind();
251        unsafe { sys::imnodes_SelectLink(link_id) }
252    }
253
254    pub fn clear_link_selection_of(&self, link_id: i32) {
255        self.bind();
256        unsafe { sys::imnodes_ClearLinkSelection_Int(link_id) }
257    }
258
259    pub fn is_link_selected(&self, link_id: i32) -> bool {
260        self.bind();
261        unsafe { sys::imnodes_IsLinkSelected(link_id) }
262    }
263
264    pub fn selected_nodes(&self) -> Vec<i32> {
265        // Safety: ImNodes returns the current count of selected nodes, and
266        // `GetSelectedNodes` writes exactly that many IDs into the buffer.
267        self.bind();
268        let n = unsafe { sys::imnodes_NumSelectedNodes() };
269        if n <= 0 {
270            return Vec::new();
271        }
272        let mut buf = vec![0i32; n as usize];
273        unsafe { sys::imnodes_GetSelectedNodes(buf.as_mut_ptr()) };
274        buf
275    }
276
277    pub fn selected_links(&self) -> Vec<i32> {
278        // Safety: ImNodes returns the current count of selected links, and
279        // `GetSelectedLinks` writes exactly that many IDs into the buffer.
280        self.bind();
281        let n = unsafe { sys::imnodes_NumSelectedLinks() };
282        if n <= 0 {
283            return Vec::new();
284        }
285        let mut buf = vec![0i32; n as usize];
286        unsafe { sys::imnodes_GetSelectedLinks(buf.as_mut_ptr()) };
287        buf
288    }
289
290    pub fn clear_selection(&self) {
291        self.bind();
292        unsafe {
293            sys::imnodes_ClearNodeSelection_Nil();
294            sys::imnodes_ClearLinkSelection_Nil();
295        }
296    }
297
298    pub fn is_link_created(&self) -> Option<crate::LinkCreated> {
299        self.link_created
300    }
301
302    pub fn is_link_created_with_nodes(&self) -> Option<crate::LinkCreatedEx> {
303        self.link_created_ex
304    }
305
306    pub fn is_link_destroyed(&self) -> Option<i32> {
307        self.link_destroyed
308    }
309
310    pub fn is_editor_hovered(&self) -> bool {
311        self.editor_hovered
312    }
313
314    pub fn hovered_node(&self) -> Option<i32> {
315        self.hovered_node
316    }
317
318    pub fn hovered_link(&self) -> Option<i32> {
319        self.hovered_link
320    }
321
322    pub fn hovered_pin(&self) -> Option<i32> {
323        self.hovered_pin
324    }
325
326    /// Set a node's position in screen space for the current editor context.
327    pub fn set_node_pos_screen(&self, node_id: i32, pos: [f32; 2]) {
328        self.bind();
329        unsafe {
330            sys::imnodes_SetNodeScreenSpacePos(
331                node_id,
332                sys::ImVec2_c {
333                    x: pos[0],
334                    y: pos[1],
335                },
336            )
337        }
338    }
339
340    /// Set a node's position in grid space for the current editor context.
341    pub fn set_node_pos_grid(&self, node_id: i32, pos: [f32; 2]) {
342        self.bind();
343        unsafe {
344            sys::imnodes_SetNodeGridSpacePos(
345                node_id,
346                sys::ImVec2_c {
347                    x: pos[0],
348                    y: pos[1],
349                },
350            )
351        }
352    }
353
354    pub fn is_attribute_active(&self) -> bool {
355        self.any_attribute_active.is_some()
356    }
357
358    pub fn any_attribute_active(&self) -> Option<i32> {
359        self.any_attribute_active
360    }
361
362    pub fn is_link_started(&self) -> Option<i32> {
363        self.link_started
364    }
365
366    pub fn is_link_dropped(&self, including_detached: bool) -> Option<i32> {
367        if including_detached {
368            self.link_dropped_including_detached
369        } else {
370            self.link_dropped_excluding_detached
371        }
372    }
373}