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