dear_imnodes/
context.rs

1use crate::sys;
2use dear_imgui_rs::sys as imgui_sys;
3use dear_imgui_rs::{Context as ImGuiContext, Ui};
4use std::os::raw::c_void;
5
6/// Global ImNodes context
7pub struct Context {
8    raw: *mut sys::ImNodesContext,
9}
10
11impl Context {
12    /// Try to create a new ImNodes context bound to the current Dear ImGui context
13    pub fn try_create(_imgui: &ImGuiContext) -> dear_imgui_rs::ImGuiResult<Self> {
14        unsafe {
15            sys::imnodes_SetImGuiContext(imgui_sys::igGetCurrentContext());
16        }
17        let raw = unsafe { sys::imnodes_CreateContext() };
18        if raw.is_null() {
19            return Err(dear_imgui_rs::ImGuiError::context_creation(
20                "imnodes_CreateContext returned null",
21            ));
22        }
23        unsafe { sys::imnodes_SetCurrentContext(raw) };
24        Ok(Self { raw })
25    }
26
27    /// Create a new ImNodes context (panics on error)
28    pub fn create(imgui: &ImGuiContext) -> Self {
29        Self::try_create(imgui).expect("Failed to create ImNodes context")
30    }
31
32    /// Set as current ImNodes context
33    pub fn set_as_current(&self) {
34        unsafe { sys::imnodes_SetCurrentContext(self.raw) };
35    }
36}
37
38impl Drop for Context {
39    fn drop(&mut self) {
40        if !self.raw.is_null() {
41            unsafe { sys::imnodes_DestroyContext(self.raw) };
42        }
43    }
44}
45
46// ImNodes context interacts with Dear ImGui state and is not thread-safe.
47
48/// An editor context allows multiple independent editors
49pub struct EditorContext {
50    raw: *mut sys::ImNodesEditorContext,
51}
52
53impl EditorContext {
54    pub fn try_create() -> dear_imgui_rs::ImGuiResult<Self> {
55        let raw = unsafe { sys::imnodes_EditorContextCreate() };
56        if raw.is_null() {
57            return Err(dear_imgui_rs::ImGuiError::context_creation(
58                "imnodes_EditorContextCreate returned null",
59            ));
60        }
61        Ok(Self { raw })
62    }
63
64    pub fn create() -> Self {
65        Self::try_create().expect("Failed to create ImNodes editor context")
66    }
67
68    pub fn set_current(&self) {
69        unsafe { sys::imnodes_EditorContextSet(self.raw) };
70    }
71
72    pub fn get_panning(&self) -> [f32; 2] {
73        unsafe { sys::imnodes_EditorContextSet(self.raw) };
74        let out = unsafe { crate::compat_ffi::imnodes_EditorContextGetPanning() };
75        [out.x, out.y]
76    }
77
78    pub fn reset_panning(&self, pos: [f32; 2]) {
79        unsafe { sys::imnodes_EditorContextSet(self.raw) };
80        unsafe {
81            sys::imnodes_EditorContextResetPanning(sys::ImVec2_c {
82                x: pos[0],
83                y: pos[1],
84            })
85        };
86    }
87
88    pub fn move_to_node(&self, node_id: i32) {
89        unsafe { sys::imnodes_EditorContextSet(self.raw) };
90        unsafe { sys::imnodes_EditorContextMoveToNode(node_id) };
91    }
92
93    /// Save this editor's state to an INI string
94    pub fn save_state_to_ini_string(&self) -> String {
95        unsafe {
96            let mut size: usize = 0;
97            let ptr = sys::imnodes_SaveEditorStateToIniString(self.raw, &mut size as *mut usize);
98            if ptr.is_null() || size == 0 {
99                return String::new();
100            }
101            let slice = std::slice::from_raw_parts(ptr as *const u8, size);
102            String::from_utf8_lossy(slice).into_owned()
103        }
104    }
105
106    /// Load this editor's state from an INI string
107    pub fn load_state_from_ini_string(&self, data: &str) {
108        unsafe {
109            sys::imnodes_LoadEditorStateFromIniString(
110                self.raw,
111                data.as_ptr() as *const i8,
112                data.len(),
113            )
114        }
115    }
116
117    /// Save this editor's state directly to an INI file
118    pub fn save_state_to_ini_file(&self, file_name: &str) {
119        let c = std::ffi::CString::new(file_name).unwrap_or_default();
120        unsafe { sys::imnodes_SaveEditorStateToIniFile(self.raw, c.as_ptr()) }
121    }
122
123    /// Load this editor's state from an INI file
124    pub fn load_state_from_ini_file(&self, file_name: &str) {
125        let c = std::ffi::CString::new(file_name).unwrap_or_default();
126        unsafe { sys::imnodes_LoadEditorStateFromIniFile(self.raw, c.as_ptr()) }
127    }
128}
129
130impl Drop for EditorContext {
131    fn drop(&mut self) {
132        if !self.raw.is_null() {
133            unsafe { sys::imnodes_EditorContextFree(self.raw) };
134        }
135    }
136}
137
138// EditorContext is also not thread-safe to move/share across threads.
139
140/// Per-frame Ui extension entry point
141pub struct NodesUi<'ui> {
142    _ui: &'ui Ui,
143    _ctx: &'ui Context,
144}
145
146impl<'ui> NodesUi<'ui> {
147    pub(crate) fn new(ui: &'ui Ui, ctx: &'ui Context) -> Self {
148        // ensure current context
149        ctx.set_as_current();
150        Self { _ui: ui, _ctx: ctx }
151    }
152
153    /// Begin a node editor with an optional EditorContext
154    pub fn editor(&self, editor: Option<&'ui EditorContext>) -> NodeEditor<'ui> {
155        NodeEditor::begin(self._ui, editor)
156    }
157}
158
159/// RAII token for a node editor frame
160pub struct NodeEditor<'ui> {
161    _ui: &'ui Ui,
162    ended: bool,
163}
164
165impl<'ui> NodeEditor<'ui> {
166    pub(crate) fn begin(ui: &'ui Ui, editor: Option<&EditorContext>) -> Self {
167        if let Some(ed) = editor {
168            unsafe { sys::imnodes_EditorContextSet(ed.raw) }
169        }
170        unsafe { sys::imnodes_BeginNodeEditor() };
171        Self {
172            _ui: ui,
173            ended: false,
174        }
175    }
176
177    /// Draw a minimap in the editor
178    pub fn minimap(&self, size_fraction: f32, location: crate::MiniMapLocation) {
179        unsafe {
180            sys::imnodes_MiniMap(
181                size_fraction,
182                location as sys::ImNodesMiniMapLocation,
183                None,
184                std::ptr::null_mut(),
185            )
186        }
187    }
188
189    /// Draw a minimap with a node-hover callback (invoked during this call)
190    pub fn minimap_with_callback<F: FnMut(i32)>(
191        &self,
192        size_fraction: f32,
193        location: crate::MiniMapLocation,
194        callback: &mut F,
195    ) {
196        unsafe extern "C" fn trampoline(node_id: i32, user: *mut c_void) {
197            unsafe {
198                let closure = &mut *(user as *mut &mut dyn FnMut(i32));
199                (closure)(node_id);
200            }
201        }
202        let mut cb_obj: &mut dyn FnMut(i32) = callback;
203        let user_ptr = &mut cb_obj as *mut _ as *mut c_void;
204        unsafe {
205            sys::imnodes_MiniMap(
206                size_fraction,
207                location as sys::ImNodesMiniMapLocation,
208                Some(trampoline),
209                user_ptr,
210            )
211        }
212    }
213
214    /// Begin a node
215    pub fn node(&self, id: i32) -> NodeToken<'_> {
216        unsafe { sys::imnodes_BeginNode(id) };
217        NodeToken {
218            _phantom: std::marker::PhantomData,
219        }
220    }
221
222    /// Begin an input attribute pin
223    pub fn input_attr(&self, id: i32, shape: crate::PinShape) -> AttributeToken<'_> {
224        unsafe { sys::imnodes_BeginInputAttribute(id, shape as sys::ImNodesPinShape) };
225        AttributeToken {
226            kind: AttrKind::Input,
227            _phantom: std::marker::PhantomData,
228        }
229    }
230
231    /// Begin an output attribute pin
232    pub fn output_attr(&self, id: i32, shape: crate::PinShape) -> AttributeToken<'_> {
233        unsafe { sys::imnodes_BeginOutputAttribute(id, shape as i32) };
234        AttributeToken {
235            kind: AttrKind::Output,
236            _phantom: std::marker::PhantomData,
237        }
238    }
239
240    /// Begin a static attribute region
241    pub fn static_attr(&self, id: i32) -> AttributeToken<'_> {
242        unsafe { sys::imnodes_BeginStaticAttribute(id) };
243        AttributeToken {
244            kind: AttrKind::Static,
245            _phantom: std::marker::PhantomData,
246        }
247    }
248
249    /// Draw a link between two attributes
250    pub fn link(&self, id: i32, start_attr: i32, end_attr: i32) {
251        unsafe { sys::imnodes_Link(id, start_attr, end_attr) }
252    }
253
254    /// Query if a link was created this frame (attribute-id version)
255    pub fn is_link_created(&self) -> Option<crate::LinkCreated> {
256        let mut start_attr = 0i32;
257        let mut end_attr = 0i32;
258        let mut from_snap = false;
259        let created = unsafe {
260            sys::imnodes_IsLinkCreated_BoolPtr(
261                &mut start_attr as *mut i32,
262                &mut end_attr as *mut i32,
263                &mut from_snap as *mut bool,
264            )
265        };
266        if created {
267            Some(crate::LinkCreated {
268                start_attr,
269                end_attr,
270                from_snap,
271            })
272        } else {
273            None
274        }
275    }
276
277    /// Query link created with node ids and attribute ids
278    pub fn is_link_created_with_nodes(&self) -> Option<crate::LinkCreatedEx> {
279        let mut start_node = 0i32;
280        let mut start_attr = 0i32;
281        let mut end_node = 0i32;
282        let mut end_attr = 0i32;
283        let mut from_snap = false;
284        let created = unsafe {
285            sys::imnodes_IsLinkCreated_IntPtr(
286                &mut start_node as *mut i32,
287                &mut start_attr as *mut i32,
288                &mut end_node as *mut i32,
289                &mut end_attr as *mut i32,
290                &mut from_snap as *mut bool,
291            )
292        };
293        if created {
294            Some(crate::LinkCreatedEx {
295                start_node,
296                start_attr,
297                end_node,
298                end_attr,
299                from_snap,
300            })
301        } else {
302            None
303        }
304    }
305
306    /// Selection helpers
307    pub fn selected_nodes(&self) -> Vec<i32> {
308        let n = unsafe { sys::imnodes_NumSelectedNodes() };
309        if n <= 0 {
310            return Vec::new();
311        }
312        let mut buf = vec![0i32; n as usize];
313        unsafe { sys::imnodes_GetSelectedNodes(buf.as_mut_ptr()) };
314        buf
315    }
316
317    pub fn selected_links(&self) -> Vec<i32> {
318        let n = unsafe { sys::imnodes_NumSelectedLinks() };
319        if n <= 0 {
320            return Vec::new();
321        }
322        let mut buf = vec![0i32; n as usize];
323        unsafe { sys::imnodes_GetSelectedLinks(buf.as_mut_ptr()) };
324        buf
325    }
326
327    pub fn clear_selection(&self) {
328        unsafe {
329            sys::imnodes_ClearNodeSelection_Nil();
330            sys::imnodes_ClearLinkSelection_Nil();
331        }
332    }
333
334    /// Hover queries
335    pub fn is_editor_hovered(&self) -> bool {
336        unsafe { sys::imnodes_IsEditorHovered() }
337    }
338    pub fn hovered_node(&self) -> Option<i32> {
339        let mut id = 0;
340        if unsafe { sys::imnodes_IsNodeHovered(&mut id) } {
341            Some(id)
342        } else {
343            None
344        }
345    }
346    pub fn hovered_link(&self) -> Option<i32> {
347        let mut id = 0;
348        if unsafe { sys::imnodes_IsLinkHovered(&mut id) } {
349            Some(id)
350        } else {
351            None
352        }
353    }
354    pub fn hovered_pin(&self) -> Option<i32> {
355        let mut id = 0;
356        if unsafe { sys::imnodes_IsPinHovered(&mut id) } {
357            Some(id)
358        } else {
359            None
360        }
361    }
362
363    /// Attribute active state
364    pub fn is_attribute_active(&self) -> bool {
365        unsafe { sys::imnodes_IsAttributeActive() }
366    }
367    pub fn any_attribute_active(&self) -> Option<i32> {
368        let mut id = 0;
369        if unsafe { sys::imnodes_IsAnyAttributeActive(&mut id) } {
370            Some(id)
371        } else {
372            None
373        }
374    }
375
376    /// Link drag/drop lifecycle
377    pub fn is_link_started(&self) -> Option<i32> {
378        let mut id = 0;
379        if unsafe { sys::imnodes_IsLinkStarted(&mut id) } {
380            Some(id)
381        } else {
382            None
383        }
384    }
385    pub fn is_link_dropped(&self, including_detached: bool) -> Option<i32> {
386        let mut id = 0;
387        if unsafe { sys::imnodes_IsLinkDropped(&mut id, including_detached) } {
388            Some(id)
389        } else {
390            None
391        }
392    }
393
394    /// Toggle style flags (GridLines, GridLinesPrimary, GridSnapping, NodeOutline)
395    pub fn set_style_flag(&self, flag: crate::StyleFlags, enabled: bool) {
396        unsafe {
397            let style = &mut *sys::imnodes_GetStyle();
398            let mut f = style.Flags as i32;
399            let bit = flag.bits();
400            if enabled {
401                f |= bit;
402            } else {
403                f &= !bit;
404            }
405            style.Flags = f;
406        }
407    }
408
409    /// Enable link detach with Ctrl by binding to ImGui IO KeyCtrl
410    pub fn enable_link_detach_with_ctrl(&self) {
411        unsafe {
412            let io = &mut *sys::imnodes_GetIO();
413            io.LinkDetachWithModifierClick.Modifier = sys::getIOKeyCtrlPtr();
414        }
415    }
416    /// Enable multiple select modifier as Ctrl
417    pub fn enable_multiple_select_with_ctrl(&self) {
418        unsafe {
419            let io = &mut *sys::imnodes_GetIO();
420            io.MultipleSelectModifier.Modifier = sys::getIOKeyCtrlPtr();
421        }
422    }
423    /// Enable multiple select modifier as Shift
424    pub fn enable_multiple_select_with_shift(&self) {
425        unsafe {
426            let io = &mut *sys::imnodes_GetIO();
427            io.MultipleSelectModifier.Modifier = sys::imnodes_getIOKeyShiftPtr();
428        }
429    }
430    /// Emulate three-button mouse with Alt
431    pub fn emulate_three_button_mouse_with_alt(&self) {
432        unsafe {
433            let io = &mut *sys::imnodes_GetIO();
434            io.EmulateThreeButtonMouse.Modifier = sys::imnodes_getIOKeyAltPtr();
435        }
436    }
437    /// IO tweaks
438    pub fn set_alt_mouse_button(&self, button: i32) {
439        unsafe {
440            (*sys::imnodes_GetIO()).AltMouseButton = button;
441        }
442    }
443    pub fn set_auto_panning_speed(&self, speed: f32) {
444        unsafe {
445            (*sys::imnodes_GetIO()).AutoPanningSpeed = speed;
446        }
447    }
448    /// Style preset helpers
449    pub fn style_colors_dark(&self) {
450        unsafe { sys::imnodes_StyleColorsDark(sys::imnodes_GetStyle()) }
451    }
452    pub fn style_colors_classic(&self) {
453        unsafe { sys::imnodes_StyleColorsClassic(sys::imnodes_GetStyle()) }
454    }
455    pub fn style_colors_light(&self) {
456        unsafe { sys::imnodes_StyleColorsLight(sys::imnodes_GetStyle()) }
457    }
458
459    // state save/load moved to PostEditor
460
461    /// Node positions in grid space
462    pub fn set_node_pos_grid(&self, node_id: i32, pos: [f32; 2]) {
463        unsafe {
464            sys::imnodes_SetNodeGridSpacePos(
465                node_id,
466                sys::ImVec2_c {
467                    x: pos[0],
468                    y: pos[1],
469                },
470            )
471        }
472    }
473
474    pub fn get_node_pos_grid(&self, node_id: i32) -> [f32; 2] {
475        let out = unsafe { sys::imnodes_GetNodeGridSpacePos(node_id) };
476        [out.x, out.y]
477    }
478
479    /// Persistent style setters
480    pub fn set_grid_spacing(&self, spacing: f32) {
481        unsafe {
482            (*sys::imnodes_GetStyle()).GridSpacing = spacing;
483        }
484    }
485    pub fn set_link_thickness(&self, thickness: f32) {
486        unsafe {
487            (*sys::imnodes_GetStyle()).LinkThickness = thickness;
488        }
489    }
490    pub fn set_node_corner_rounding(&self, rounding: f32) {
491        unsafe {
492            (*sys::imnodes_GetStyle()).NodeCornerRounding = rounding;
493        }
494    }
495    pub fn set_node_padding(&self, padding: [f32; 2]) {
496        unsafe {
497            (*sys::imnodes_GetStyle()).NodePadding = sys::ImVec2_c {
498                x: padding[0],
499                y: padding[1],
500            };
501        }
502    }
503    pub fn set_pin_circle_radius(&self, r: f32) {
504        unsafe {
505            (*sys::imnodes_GetStyle()).PinCircleRadius = r;
506        }
507    }
508    pub fn set_pin_quad_side_length(&self, v: f32) {
509        unsafe {
510            (*sys::imnodes_GetStyle()).PinQuadSideLength = v;
511        }
512    }
513    pub fn set_pin_triangle_side_length(&self, v: f32) {
514        unsafe {
515            (*sys::imnodes_GetStyle()).PinTriangleSideLength = v;
516        }
517    }
518    pub fn set_pin_line_thickness(&self, v: f32) {
519        unsafe {
520            (*sys::imnodes_GetStyle()).PinLineThickness = v;
521        }
522    }
523    pub fn set_pin_hover_radius(&self, v: f32) {
524        unsafe {
525            (*sys::imnodes_GetStyle()).PinHoverRadius = v;
526        }
527    }
528    pub fn set_pin_offset(&self, offset: f32) {
529        unsafe {
530            (*sys::imnodes_GetStyle()).PinOffset = offset;
531        }
532    }
533    pub fn set_link_hover_distance(&self, v: f32) {
534        unsafe {
535            (*sys::imnodes_GetStyle()).LinkHoverDistance = v;
536        }
537    }
538    pub fn set_link_line_segments_per_length(&self, v: f32) {
539        unsafe {
540            (*sys::imnodes_GetStyle()).LinkLineSegmentsPerLength = v;
541        }
542    }
543    pub fn set_node_border_thickness(&self, v: f32) {
544        unsafe {
545            (*sys::imnodes_GetStyle()).NodeBorderThickness = v;
546        }
547    }
548    pub fn set_minimap_padding(&self, padding: [f32; 2]) {
549        unsafe {
550            (*sys::imnodes_GetStyle()).MiniMapPadding = sys::ImVec2_c {
551                x: padding[0],
552                y: padding[1],
553            };
554        }
555    }
556    pub fn set_minimap_offset(&self, offset: [f32; 2]) {
557        unsafe {
558            (*sys::imnodes_GetStyle()).MiniMapOffset = sys::ImVec2_c {
559                x: offset[0],
560                y: offset[1],
561            };
562        }
563    }
564
565    pub fn set_color(&self, elem: crate::style::ColorElement, color: [f32; 4]) {
566        let abgr = unsafe {
567            imgui_sys::igColorConvertFloat4ToU32(imgui_sys::ImVec4 {
568                x: color[0],
569                y: color[1],
570                z: color[2],
571                w: color[3],
572            })
573        };
574        unsafe { (*sys::imnodes_GetStyle()).Colors[elem as u32 as usize] = abgr };
575    }
576
577    /// Get a style color as RGBA floats [0,1]
578    pub fn get_color(&self, elem: crate::style::ColorElement) -> [f32; 4] {
579        let col = unsafe { (*sys::imnodes_GetStyle()).Colors[elem as u32 as usize] };
580        let out = unsafe { imgui_sys::igColorConvertU32ToFloat4(col) };
581        [out.x, out.y, out.z, out.w]
582    }
583
584    /// Node positions in screen/editor space
585    pub fn set_node_pos_screen(&self, node_id: i32, pos: [f32; 2]) {
586        unsafe {
587            sys::imnodes_SetNodeScreenSpacePos(
588                node_id,
589                sys::ImVec2_c {
590                    x: pos[0],
591                    y: pos[1],
592                },
593            )
594        }
595    }
596    pub fn set_node_pos_editor(&self, node_id: i32, pos: [f32; 2]) {
597        unsafe {
598            sys::imnodes_SetNodeEditorSpacePos(
599                node_id,
600                sys::ImVec2_c {
601                    x: pos[0],
602                    y: pos[1],
603                },
604            )
605        }
606    }
607    pub fn get_node_pos_screen(&self, node_id: i32) -> [f32; 2] {
608        let out = unsafe { crate::compat_ffi::imnodes_GetNodeScreenSpacePos(node_id) };
609        [out.x, out.y]
610    }
611    pub fn get_node_pos_editor(&self, node_id: i32) -> [f32; 2] {
612        let out = unsafe { crate::compat_ffi::imnodes_GetNodeEditorSpacePos(node_id) };
613        [out.x, out.y]
614    }
615
616    /// Node drag/size helpers
617    pub fn set_node_draggable(&self, node_id: i32, draggable: bool) {
618        unsafe { sys::imnodes_SetNodeDraggable(node_id, draggable) }
619    }
620    pub fn snap_node_to_grid(&self, node_id: i32) {
621        unsafe { sys::imnodes_SnapNodeToGrid(node_id) }
622    }
623    pub fn get_node_dimensions(&self, node_id: i32) -> [f32; 2] {
624        let out = unsafe { crate::compat_ffi::imnodes_GetNodeDimensions(node_id) };
625        [out.x, out.y]
626    }
627
628    /// Check if a link was destroyed this frame, returning its id
629    pub fn is_link_destroyed(&self) -> Option<i32> {
630        let mut id = 0i32;
631        let destroyed = unsafe { sys::imnodes_IsLinkDestroyed(&mut id as *mut i32) };
632        if destroyed { Some(id) } else { None }
633    }
634}
635
636impl<'ui> Drop for NodeEditor<'ui> {
637    fn drop(&mut self) {
638        if !self.ended {
639            unsafe { sys::imnodes_EndNodeEditor() };
640            self.ended = true;
641        }
642    }
643}
644
645/// RAII token for a node block
646pub struct NodeToken<'a> {
647    pub(crate) _phantom: std::marker::PhantomData<&'a ()>,
648}
649impl<'a> NodeToken<'a> {
650    pub fn title_bar<F: FnOnce()>(&self, f: F) {
651        unsafe { sys::imnodes_BeginNodeTitleBar() };
652        f();
653        unsafe { sys::imnodes_EndNodeTitleBar() };
654    }
655    pub fn end(self) {}
656}
657impl<'a> Drop for NodeToken<'a> {
658    fn drop(&mut self) {
659        unsafe { sys::imnodes_EndNode() }
660    }
661}
662
663pub(crate) enum AttrKind {
664    Input,
665    Output,
666    Static,
667}
668
669pub struct AttributeToken<'a> {
670    pub(crate) kind: AttrKind,
671    pub(crate) _phantom: std::marker::PhantomData<&'a ()>,
672}
673
674impl<'a> AttributeToken<'a> {
675    pub fn end(self) {}
676}
677
678impl<'a> Drop for AttributeToken<'a> {
679    fn drop(&mut self) {
680        unsafe {
681            match self.kind {
682                AttrKind::Input => sys::imnodes_EndInputAttribute(),
683                AttrKind::Output => sys::imnodes_EndOutputAttribute(),
684                AttrKind::Static => sys::imnodes_EndStaticAttribute(),
685            }
686        }
687    }
688}
689
690/// Post-editor queries (must be called after EndNodeEditor)
691pub struct PostEditor;
692
693impl<'ui> NodeEditor<'ui> {
694    /// Explicitly end the node editor and return post-editor query handle
695    pub fn end(mut self) -> PostEditor {
696        if !self.ended {
697            unsafe { sys::imnodes_EndNodeEditor() };
698            self.ended = true;
699        }
700        PostEditor
701    }
702}
703
704impl PostEditor {
705    /// Save current editor state to an INI string
706    pub fn save_state_to_ini_string(&self) -> String {
707        // Safety: ImNodes returns a pointer to an internal, null-terminated INI
708        // buffer and writes its size into `size`. The pointer remains valid
709        // until the next save/load call on the same editor, which we do not
710        // perform while this slice is alive.
711        unsafe {
712            let mut size: usize = 0;
713            let ptr = sys::imnodes_SaveCurrentEditorStateToIniString(&mut size as *mut usize);
714            if ptr.is_null() || size == 0 {
715                return String::new();
716            }
717            let slice = std::slice::from_raw_parts(ptr as *const u8, size);
718            String::from_utf8_lossy(slice).into_owned()
719        }
720    }
721
722    /// Load editor state from an INI string
723    pub fn load_state_from_ini_string(&self, data: &str) {
724        // Safety: ImNodes expects a pointer to a valid UTF-8 buffer and its
725        // length; `data.as_ptr()` and `data.len()` satisfy this for the
726        // duration of the call.
727        unsafe {
728            sys::imnodes_LoadCurrentEditorStateFromIniString(
729                data.as_ptr() as *const i8,
730                data.len(),
731            );
732        }
733    }
734    /// Save/Load current editor state to/from INI file
735    pub fn save_state_to_ini_file(&self, file_name: &str) {
736        let c = std::ffi::CString::new(file_name).unwrap_or_default();
737        // Safety: `CString` guarantees a NUL-terminated string for the
738        // duration of the call; ImNodes reads it as a const char*.
739        unsafe { sys::imnodes_SaveCurrentEditorStateToIniFile(c.as_ptr()) }
740    }
741    pub fn load_state_from_ini_file(&self, file_name: &str) {
742        let c = std::ffi::CString::new(file_name).unwrap_or_default();
743        // Safety: see `save_state_to_ini_file`.
744        unsafe { sys::imnodes_LoadCurrentEditorStateFromIniFile(c.as_ptr()) }
745    }
746    /// Selection helpers per id
747    pub fn select_node(&self, node_id: i32) {
748        unsafe { sys::imnodes_SelectNode(node_id) }
749    }
750    pub fn clear_node_selection_of(&self, node_id: i32) {
751        unsafe { sys::imnodes_ClearNodeSelection_Int(node_id) }
752    }
753    pub fn is_node_selected(&self, node_id: i32) -> bool {
754        unsafe { sys::imnodes_IsNodeSelected(node_id) }
755    }
756    pub fn select_link(&self, link_id: i32) {
757        unsafe { sys::imnodes_SelectLink(link_id) }
758    }
759    pub fn clear_link_selection_of(&self, link_id: i32) {
760        unsafe { sys::imnodes_ClearLinkSelection_Int(link_id) }
761    }
762    pub fn is_link_selected(&self, link_id: i32) -> bool {
763        unsafe { sys::imnodes_IsLinkSelected(link_id) }
764    }
765    pub fn selected_nodes(&self) -> Vec<i32> {
766        // Safety: ImNodes returns the current count of selected nodes, and
767        // `GetSelectedNodes` writes exactly that many IDs into the buffer.
768        let n = unsafe { sys::imnodes_NumSelectedNodes() };
769        if n <= 0 {
770            return Vec::new();
771        }
772        let mut buf = vec![0i32; n as usize];
773        unsafe { sys::imnodes_GetSelectedNodes(buf.as_mut_ptr()) };
774        buf
775    }
776
777    pub fn selected_links(&self) -> Vec<i32> {
778        // Safety: ImNodes returns the current count of selected links, and
779        // `GetSelectedLinks` writes exactly that many IDs into the buffer.
780        let n = unsafe { sys::imnodes_NumSelectedLinks() };
781        if n <= 0 {
782            return Vec::new();
783        }
784        let mut buf = vec![0i32; n as usize];
785        unsafe { sys::imnodes_GetSelectedLinks(buf.as_mut_ptr()) };
786        buf
787    }
788
789    pub fn clear_selection(&self) {
790        unsafe {
791            sys::imnodes_ClearNodeSelection_Nil();
792            sys::imnodes_ClearLinkSelection_Nil();
793        }
794    }
795
796    pub fn is_link_created(&self) -> Option<crate::LinkCreated> {
797        let mut start_attr = 0i32;
798        let mut end_attr = 0i32;
799        let mut from_snap = false;
800        let created = unsafe {
801            sys::imnodes_IsLinkCreated_BoolPtr(
802                &mut start_attr as *mut i32,
803                &mut end_attr as *mut i32,
804                &mut from_snap as *mut bool,
805            )
806        };
807        if created {
808            Some(crate::LinkCreated {
809                start_attr,
810                end_attr,
811                from_snap,
812            })
813        } else {
814            None
815        }
816    }
817
818    pub fn is_link_created_with_nodes(&self) -> Option<crate::LinkCreatedEx> {
819        let mut start_node = 0i32;
820        let mut start_attr = 0i32;
821        let mut end_node = 0i32;
822        let mut end_attr = 0i32;
823        let mut from_snap = false;
824        let created = unsafe {
825            sys::imnodes_IsLinkCreated_IntPtr(
826                &mut start_node as *mut i32,
827                &mut start_attr as *mut i32,
828                &mut end_node as *mut i32,
829                &mut end_attr as *mut i32,
830                &mut from_snap as *mut bool,
831            )
832        };
833        if created {
834            Some(crate::LinkCreatedEx {
835                start_node,
836                start_attr,
837                end_node,
838                end_attr,
839                from_snap,
840            })
841        } else {
842            None
843        }
844    }
845
846    pub fn is_link_destroyed(&self) -> Option<i32> {
847        let mut id = 0i32;
848        let destroyed = unsafe { sys::imnodes_IsLinkDestroyed(&mut id as *mut i32) };
849        if destroyed { Some(id) } else { None }
850    }
851
852    pub fn is_editor_hovered(&self) -> bool {
853        unsafe { sys::imnodes_IsEditorHovered() }
854    }
855    pub fn hovered_node(&self) -> Option<i32> {
856        let mut id = 0;
857        if unsafe { sys::imnodes_IsNodeHovered(&mut id) } {
858            Some(id)
859        } else {
860            None
861        }
862    }
863    pub fn hovered_link(&self) -> Option<i32> {
864        let mut id = 0;
865        if unsafe { sys::imnodes_IsLinkHovered(&mut id) } {
866            Some(id)
867        } else {
868            None
869        }
870    }
871    pub fn hovered_pin(&self) -> Option<i32> {
872        let mut id = 0;
873        if unsafe { sys::imnodes_IsPinHovered(&mut id) } {
874            Some(id)
875        } else {
876            None
877        }
878    }
879    pub fn is_attribute_active(&self) -> bool {
880        unsafe { sys::imnodes_IsAttributeActive() }
881    }
882    pub fn any_attribute_active(&self) -> Option<i32> {
883        let mut id = 0;
884        if unsafe { sys::imnodes_IsAnyAttributeActive(&mut id) } {
885            Some(id)
886        } else {
887            None
888        }
889    }
890    pub fn is_link_started(&self) -> Option<i32> {
891        let mut id = 0;
892        if unsafe { sys::imnodes_IsLinkStarted(&mut id) } {
893            Some(id)
894        } else {
895            None
896        }
897    }
898    pub fn is_link_dropped(&self, including_detached: bool) -> Option<i32> {
899        let mut id = 0;
900        if unsafe { sys::imnodes_IsLinkDropped(&mut id, including_detached) } {
901            Some(id)
902        } else {
903            None
904        }
905    }
906}