1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
use crate::{ImNodesIO, Style, sys};
use std::ffi::{CStr, CString};
use std::path::Path;
/// An editor context corresponds to a set of nodes in a single workspace
///
/// By default, the library creates an editor context behind the scenes, so using any of the imnodes
/// functions doesn't require you to explicitly create a context. However, creating explicit contexts
/// allows for multiple editor instances.
#[derive(Debug)]
pub struct EditorContext {
raw: *mut sys::ImNodesEditorContext,
}
impl EditorContext {
/// Associates this editor context with the current ImGui window.
///
/// This should be called before [`crate::editor()`].
#[doc(alias = "EditorContextSet")]
#[must_use]
pub fn set_as_current_editor(&self) -> &Self {
// Safety: C API call. Sets the thread-local current editor context.
unsafe { sys::imnodes_EditorContextSet(self.raw) };
self
}
/// Creates a new identifier generator associated with this editor context.
///
/// Each editor should ideally use its own generator to avoid ID clashes
/// if multiple editors are used in the same application.
#[must_use]
pub fn new_identifier_generator(&self) -> crate::IdentifierGenerator {
// The generator itself is context-agnostic, but creating it via the context
// encourages correct usage patterns.
crate::IdentifierGenerator::new()
}
/// Returns a mutable reference to the global style variables shared across all editor contexts.
///
/// Use this to modify the *currently active* style. To get a copy of the default style,
/// use [`crate::Style::default()`].
#[doc(alias = "GetStyle")]
pub fn get_style(&mut self) -> &mut Style {
// Safety: This accesses the global style object managed by imnodes.
// We cast the raw pointer to our wrapper struct.
unsafe { &mut *(sys::imnodes_GetStyle() as *mut Style) }
}
/// Returns a mutable reference to the global IO settings shared across all editor contexts.
///
/// Use this to configure input behaviors like modifier keys.
#[doc(alias = "GetIO")]
pub fn get_io(&mut self) -> &mut ImNodesIO {
// Safety: This accesses the global IO object managed by imnodes.
unsafe { &mut *sys::imnodes_GetIO() }
}
/// Saves the state of the currently active editor context to a string.
///
/// Returns `None` if saving fails or the resulting string is invalid UTF-8.
#[doc(alias = "SaveCurrentEditorStateToIniString")]
#[must_use]
pub fn save_current_editor_state_to_string(&self) -> Option<String> {
// Ensure this context is set before saving 'current' state
let _ = self.set_as_current_editor();
let mut data_size: usize = 0;
// Safety: C API call. `data_size` is written to by the function.
// The returned pointer points to memory managed by imnodes.
let char_ptr =
unsafe { sys::imnodes_SaveCurrentEditorStateToIniString(&mut data_size as *mut _) };
if char_ptr.is_null() || data_size == 0 {
return None;
}
// Safety: We assume the pointer is valid for `data_size` bytes and contains UTF-8 data.
// We create a CStr slice from it. Using CStr::from_bytes_until_nul as imnodes might not guarantee null termination exactly at data_size.
// If the data is not null terminated *within* data_size, this could read out of bounds, but imnodes *should* null terminate.
// A potentially safer approach would involve checking for null termination explicitly if needed.
unsafe {
CStr::from_bytes_with_nul_unchecked(std::slice::from_raw_parts(
char_ptr.cast(),
data_size,
))
}
.to_str()
.ok()
.map(String::from)
}
/// Saves the state of *this specific* editor context to a string.
///
/// Returns `None` if saving fails or the resulting string is invalid UTF-8.
#[doc(alias = "SaveEditorStateToIniString")]
#[must_use]
pub fn save_editor_state_to_string(&self) -> Option<String> {
let mut data_size: usize = 0;
// Safety: C API call. `data_size` is written to by the function.
// The returned pointer points to memory managed by imnodes.
let char_ptr =
unsafe { sys::imnodes_SaveEditorStateToIniString(self.raw, &mut data_size as *mut _) };
if char_ptr.is_null() || data_size == 0 {
return None;
}
// Safety: See above.
unsafe {
CStr::from_bytes_with_nul_unchecked(std::slice::from_raw_parts(
char_ptr.cast(),
data_size,
))
}
.to_str()
.ok()
.map(String::from)
}
/// Loads state into the currently active editor context from a string.
#[doc(alias = "LoadCurrentEditorStateFromIniString")]
pub fn load_current_editor_state_from_string(&self, data: &str) {
// Ensure this context is set before loading 'current' state
let _ = self.set_as_current_editor();
// Safety: C API call. Assumes `data` points to valid memory for its length.
unsafe {
sys::imnodes_LoadCurrentEditorStateFromIniString(data.as_ptr().cast(), data.len())
}
}
/// Loads state into *this specific* editor context from a string.
#[doc(alias = "LoadEditorStateFromIniString")]
pub fn load_editor_state_from_string(&self, data: &str) {
// Safety: C API call. Assumes `data` points to valid memory for its length.
unsafe {
sys::imnodes_LoadEditorStateFromIniString(self.raw, data.as_ptr().cast(), data.len())
}
}
/// Saves the state of the currently active editor context to an INI file.
#[doc(alias = "SaveCurrentEditorStateToIniFile")]
pub fn save_current_editor_state_to_file<P: AsRef<Path>>(
&self,
file_path: P,
) -> std::io::Result<()> {
// Ensure this context is set before saving 'current' state
let _ = self.set_as_current_editor();
let path_str = file_path.as_ref().to_string_lossy();
// Use CString for null termination guarantee
let c_path = CString::new(path_str.as_bytes())?;
// Safety: C API call with a valid CString pointer.
unsafe { sys::imnodes_SaveCurrentEditorStateToIniFile(c_path.as_ptr()) };
Ok(())
}
/// Saves the state of *this specific* editor context to an INI file.
#[doc(alias = "SaveEditorStateToIniFile")]
pub fn save_editor_state_to_file<P: AsRef<Path>>(&self, file_path: P) -> std::io::Result<()> {
let path_str = file_path.as_ref().to_string_lossy();
let c_path = CString::new(path_str.as_bytes())?;
// Safety: C API call with a valid CString pointer.
unsafe { sys::imnodes_SaveEditorStateToIniFile(self.raw, c_path.as_ptr()) };
Ok(())
}
/// Loads state into the currently active editor context from an INI file.
#[doc(alias = "LoadCurrentEditorStateFromIniFile")]
pub fn load_current_editor_state_from_file<P: AsRef<Path>>(
&self,
file_path: P,
) -> std::io::Result<()> {
// Ensure this context is set before loading 'current' state
let _ = self.set_as_current_editor();
let path_str = file_path.as_ref().to_string_lossy();
let c_path = CString::new(path_str.as_bytes())?;
// Safety: C API call with a valid CString pointer.
unsafe { sys::imnodes_LoadCurrentEditorStateFromIniFile(c_path.as_ptr()) };
Ok(())
}
/// Loads state into *this specific* editor context from an INI file.
#[doc(alias = "LoadEditorStateFromIniFile")]
pub fn load_editor_state_from_file<P: AsRef<Path>>(&self, file_path: P) -> std::io::Result<()> {
let path_str = file_path.as_ref().to_string_lossy();
let c_path = CString::new(path_str.as_bytes())?;
// Safety: C API call with a valid CString pointer.
unsafe { sys::imnodes_LoadEditorStateFromIniFile(self.raw, c_path.as_ptr()) };
Ok(())
}
}
impl Drop for EditorContext {
/// Frees the editor context if it was created explicitly via `Context::create_editor`.
#[doc(alias = "EditorContextFree")]
fn drop(&mut self) {
// Safety: Frees the context created by `imnodes_EditorContextCreate`.
// Only called if `owned` is true.
unsafe {
sys::imnodes_EditorContextFree(self.raw);
}
}
}
/// Represents the global imnodes context.
///
/// This should be created once at the start of the application, typically alongside
/// the `imgui::Context`.
#[doc(alias = "imnodes_CreateContext")]
#[derive(Debug)]
pub struct Context {
// Renamed from ImnodesContext to avoid confusion
context: *mut sys::ImNodesContext,
}
impl Default for Context {
/// Creates a new global imnodes context. Equivalent to `Context::new()`.
fn default() -> Self {
Self::new()
}
}
impl Context {
/// Creates a new global imnodes context.
#[must_use]
pub fn new() -> Self {
// Safety: Creates the global imnodes context. Should be called once.
let context = unsafe { sys::imnodes_CreateContext() };
// Safety: Ensure the associated ImGui context is also set for imnodes.
// We need to cast the pointer type because imgui-sys and imnodes-sys
// define their own (but compatible) ImGuiContext types.
unsafe {
sys::imnodes_SetImGuiContext(imgui::sys::igGetCurrentContext() as *mut sys::ImGuiContext)
};
Self { context }
}
/// Creates an editor context for managing a single node editor workspace.
///
/// This allows for multiple independent node editor instances.
#[must_use]
pub fn create_editor(&self) -> EditorContext {
EditorContext {
// Safety: Creates a new editor context associated with the global context.
raw: unsafe { sys::imnodes_EditorContextCreate() },
}
}
}
impl Drop for Context {
/// Destroys the global imnodes context.
fn drop(&mut self) {
// Safety: Destroys the global context created by `imnodes_CreateContext`.
unsafe { sys::imnodes_DestroyContext(self.context) }
}
}