1use crate::{EditorConfig, EditorConfigSnapshot, NodeEditorStyle, StyleColor, sys};
2use dear_imgui_rs::{Context as ImGuiContext, ContextAliveToken};
3use std::{ffi::c_void, marker::PhantomData, ptr, rc::Rc};
4
5#[derive(Debug, thiserror::Error)]
7pub enum NodeEditorError {
8 #[error("imgui-node-editor CreateEditor returned null")]
9 CreateEditorFailed,
10}
11
12pub struct EditorContext {
14 raw: *mut sys::DneEditorContext,
15 imgui_ctx_raw: *mut dear_imgui_rs::sys::ImGuiContext,
16 imgui_alive: ContextAliveToken,
17 config: EditorConfigSnapshot,
18 _settings_file: Option<std::ffi::CString>,
19 _callbacks: Option<Box<crate::config::CallbackState>>,
20 _not_send_sync: PhantomData<Rc<()>>,
21}
22
23impl EditorContext {
24 pub fn create(imgui: &ImGuiContext) -> Self {
25 Self::try_create_with_config(imgui, EditorConfig::default())
26 .expect("failed to create imgui-node-editor context")
27 }
28
29 pub fn create_with_config(imgui: &ImGuiContext, config: EditorConfig) -> Self {
30 Self::try_create_with_config(imgui, config)
31 .expect("failed to create imgui-node-editor context")
32 }
33
34 pub fn try_create_with_config(
35 imgui: &ImGuiContext,
36 mut config: EditorConfig,
37 ) -> Result<Self, NodeEditorError> {
38 let imgui_ctx_raw = imgui.as_raw();
39 let _imgui_guard = ImGuiContextGuard::bind(imgui_ctx_raw);
40 let config_snapshot = config.snapshot();
41 let raw_config = config.to_sys();
42 let raw = unsafe { sys::dne_create_editor(&raw_config) };
43 if raw.is_null() {
44 return Err(NodeEditorError::CreateEditorFailed);
45 }
46
47 Ok(Self {
48 raw,
49 imgui_ctx_raw,
50 imgui_alive: imgui.alive_token(),
51 config: config_snapshot,
52 _settings_file: config.settings_file.take(),
53 _callbacks: config.callbacks.take(),
54 _not_send_sync: PhantomData,
55 })
56 }
57
58 pub fn as_raw(&self) -> *mut sys::DneEditorContext {
59 self.raw
60 }
61
62 pub fn as_raw_native(&self) -> *mut c_void {
63 unsafe { sys::dne_editor_context_raw(self.raw) }
64 }
65
66 #[doc(alias = "GetConfig")]
67 pub fn config(&self) -> &EditorConfigSnapshot {
68 &self.config
69 }
70
71 #[doc(alias = "GetStyle")]
72 pub fn style(&self) -> NodeEditorStyle {
73 let _current = self.bind_current("EditorContext::style");
74 NodeEditorStyle::current()
75 }
76
77 pub fn set_style(&self, style: &NodeEditorStyle) {
78 let _current = self.bind_current("EditorContext::set_style");
79 style.apply();
80 }
81
82 pub fn style_color(&self, color: StyleColor) -> [f32; 4] {
83 let _current = self.bind_current("EditorContext::style_color");
84 crate::style::current_style_color(color)
85 }
86
87 pub fn set_style_color(&self, color: StyleColor, value: [f32; 4]) {
88 let _current = self.bind_current("EditorContext::set_style_color");
89 crate::style::apply_style_color(color, value);
90 }
91
92 pub(crate) fn assert_usable(&self, caller: &str) {
93 assert!(
94 self.imgui_alive.is_alive(),
95 "{caller} requires the owning Dear ImGui context to be alive"
96 );
97 assert_eq!(
98 unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
99 self.imgui_ctx_raw,
100 "{caller} must be used while the owning Dear ImGui context is current"
101 );
102 assert!(
103 !self.raw.is_null(),
104 "{caller} requires a valid node-editor context"
105 );
106 }
107
108 pub(crate) fn bind_current(&self, caller: &str) -> CurrentEditorGuard<'_> {
109 self.assert_usable(caller);
110 let previous = unsafe { sys::dne_get_current_editor_raw() };
111 unsafe { sys::dne_set_current_editor(self.raw) };
112 CurrentEditorGuard {
113 _editor: self,
114 previous,
115 }
116 }
117}
118
119impl Drop for EditorContext {
120 fn drop(&mut self) {
121 if self.raw.is_null() {
122 return;
123 }
124
125 if !self.imgui_alive.is_alive() {
126 debug_assert!(
127 false,
128 "EditorContext was dropped after its owning Dear ImGui context; \
129 declare the editor field before the Context field or drop it explicitly first"
130 );
131 self.raw = ptr::null_mut();
132 return;
133 }
134
135 let _imgui_guard = ImGuiContextGuard::bind(self.imgui_ctx_raw);
136 unsafe { sys::dne_destroy_editor(self.raw) };
137 self.raw = ptr::null_mut();
138 }
139}
140
141pub(crate) struct CurrentEditorGuard<'a> {
142 _editor: &'a EditorContext,
143 previous: *mut c_void,
144}
145
146impl Drop for CurrentEditorGuard<'_> {
147 fn drop(&mut self) {
148 unsafe { sys::dne_set_current_editor_raw(self.previous) };
149 }
150}
151
152struct ImGuiContextGuard {
153 prev: *mut dear_imgui_rs::sys::ImGuiContext,
154 restore: bool,
155}
156
157impl ImGuiContextGuard {
158 fn bind(ctx: *mut dear_imgui_rs::sys::ImGuiContext) -> Self {
159 let prev = unsafe { dear_imgui_rs::sys::igGetCurrentContext() };
160 let restore = prev != ctx;
161 if restore {
162 unsafe { dear_imgui_rs::sys::igSetCurrentContext(ctx) };
163 }
164 Self { prev, restore }
165 }
166}
167
168impl Drop for ImGuiContextGuard {
169 fn drop(&mut self) {
170 if self.restore {
171 unsafe { dear_imgui_rs::sys::igSetCurrentContext(self.prev) };
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::{EditorConfig, LinkId, NodeEditorUiExt, NodeId, PinId, PinKind, StyleColor};
180 use dear_imgui_rs::MouseButton;
181 use std::{
182 ptr,
183 sync::{Mutex, OnceLock},
184 };
185
186 fn test_guard() -> std::sync::MutexGuard<'static, ()> {
187 static GUARD: OnceLock<Mutex<()>> = OnceLock::new();
188 GUARD.get_or_init(|| Mutex::new(())).lock().unwrap()
189 }
190
191 #[test]
192 fn drop_restores_previous_imgui_context() {
193 let _guard = test_guard();
194 let imgui = ImGuiContext::create();
195 let raw_imgui = imgui.as_raw();
196 let editor = EditorContext::create(&imgui);
197
198 unsafe { dear_imgui_rs::sys::igSetCurrentContext(ptr::null_mut()) };
199 drop(editor);
200
201 assert_eq!(
202 unsafe { dear_imgui_rs::sys::igGetCurrentContext() },
203 ptr::null_mut()
204 );
205 unsafe { dear_imgui_rs::sys::igSetCurrentContext(raw_imgui) };
206 }
207
208 #[test]
209 fn current_editor_guard_restores_previous_editor() {
210 let _guard = test_guard();
211 let imgui = ImGuiContext::create();
212 let editor_a = EditorContext::create(&imgui);
213 let editor_b = EditorContext::create(&imgui);
214 let raw_a = editor_a.as_raw_native();
215 let raw_b = editor_b.as_raw_native();
216
217 unsafe { sys::dne_set_current_editor_raw(raw_a) };
218 {
219 let _current = editor_b.bind_current("test");
220 assert_eq!(unsafe { sys::dne_get_current_editor_raw() }, raw_b);
221 }
222 assert_eq!(unsafe { sys::dne_get_current_editor_raw() }, raw_a);
223
224 unsafe { sys::dne_set_current_editor_raw(ptr::null_mut()) };
225 }
226
227 #[test]
228 fn creating_editor_does_not_break_imgui_frame() {
229 let _guard = test_guard();
230 let mut imgui = ImGuiContext::create();
231 imgui.io_mut().set_display_size([640.0, 480.0]);
232 imgui.io_mut().set_delta_time(1.0 / 60.0);
233 let _ = imgui.font_atlas_mut().build();
234
235 let _editor = EditorContext::create(&imgui);
236
237 imgui.frame();
238 imgui.render();
239 }
240
241 #[test]
242 fn frame_safe_api_calls_do_not_break_imgui_frame() {
243 let _guard = test_guard();
244 let mut imgui = ImGuiContext::create();
245 imgui.io_mut().set_display_size([640.0, 480.0]);
246 imgui.io_mut().set_delta_time(1.0 / 60.0);
247 let _ = imgui.font_atlas_mut().build();
248
249 let editor_context = EditorContext::create(&imgui);
250 let node_a = NodeId::new(1);
251 let node_b = NodeId::new(2);
252 let output_pin = PinId::new(11);
253 let input_pin = PinId::new(21);
254 let link = LinkId::new(100);
255
256 let ui = imgui.frame();
257 ui.window("node-editor-frame-api").build(|| {
258 let editor = ui.node_editor(&editor_context, "frame-api", [320.0, 240.0]);
259
260 assert!(!editor.is_suspended());
261 {
262 let suspension = editor.suspend();
263 assert!(editor.is_suspended());
264 suspension.resume();
265 }
266 assert!(!editor.is_suspended());
267
268 editor.set_shortcuts_enabled(false);
269 assert!(!editor.shortcuts_enabled());
270 editor.set_shortcuts_enabled(true);
271
272 editor.set_node_position(node_a, [20.0, 30.0]);
273 editor.set_node_z_position(node_a, 2.0);
274 let _ = editor.node_z_position(node_a);
275 editor.restore_node_state(node_a);
276
277 {
278 let node = editor.begin_node(node_a);
279 let pin = node.begin_pin(output_pin, PinKind::Output);
280 ui.text("out");
281 let cursor = ui.cursor_screen_pos();
282 pin.rect(cursor, [cursor[0] + 8.0, cursor[1] + 8.0]);
283 pin.pivot_rect(cursor, [cursor[0] + 8.0, cursor[1] + 8.0]);
284 pin.pivot_size([8.0, 8.0]);
285 pin.pivot_scale([1.0, 1.0]);
286 pin.pivot_alignment([0.5, 0.5]);
287 pin.end();
288 node.end();
289 }
290 {
291 let node = editor.begin_node(node_b);
292 let pin = node.begin_pin(input_pin, PinKind::Input);
293 ui.text("in");
294 pin.end();
295 node.end();
296 }
297
298 let _ = editor.begin_group_hint(node_a);
299 let _ = editor.node_background_draw_list(node_a);
300 let _ = editor.link(link, output_pin, input_pin);
301 let _ = editor.link_pins(link);
302 let _ = editor.node_has_any_links(node_a);
303 let _ = editor.pin_has_any_links(output_pin);
304 let _ = editor.pin_had_any_links(output_pin);
305
306 editor.select_node(node_a);
307 editor.add_node_to_selection(node_b);
308 let _ = editor.is_node_selected(node_a);
309 editor.deselect_node(node_a);
310 editor.select_link(link);
311 editor.add_link_to_selection(link);
312 let _ = editor.is_link_selected(link);
313 editor.deselect_link(link);
314 editor.clear_selection();
315
316 let _ = editor.has_selection_changed();
317 let _ = editor.selected_object_count();
318 let _ = editor.is_active();
319 let _ = editor.is_background_clicked();
320 let _ = editor.is_background_double_clicked();
321 let _ = editor.background_click_button();
322 let _ = editor.background_double_click_button();
323 let _ = editor.screen_size();
324 let _ = editor.screen_to_canvas([10.0, 10.0]);
325 let _ = editor.canvas_to_screen([10.0, 10.0]);
326 let _ = editor.node_count();
327 let _ = editor.ordered_node_ids();
328
329 editor.end();
330 });
331 imgui.render();
332 }
333
334 #[test]
335 fn config_accepts_typed_buttons_and_custom_zoom_levels() {
336 let mut config = EditorConfig::new()
337 .drag_button(MouseButton::Left)
338 .select_button(MouseButton::Right)
339 .navigate_button(MouseButton::Middle)
340 .context_menu_button(MouseButton::Extra1)
341 .smooth_zoom(true, 1.25)
342 .custom_zoom_levels(vec![0.5, 1.0, 2.0]);
343
344 let snapshot = config.snapshot();
345 assert_eq!(snapshot.custom_zoom_levels, vec![0.5, 1.0, 2.0]);
346 assert_eq!(snapshot.drag_button, MouseButton::Left);
347 assert_eq!(snapshot.select_button, MouseButton::Right);
348 assert_eq!(snapshot.navigate_button, MouseButton::Middle);
349 assert_eq!(snapshot.context_menu_button, MouseButton::Extra1);
350 assert!(snapshot.enable_smooth_zoom);
351 assert_eq!(snapshot.smooth_zoom_power, 1.25);
352
353 let raw = config.to_sys();
354 assert_eq!(raw.drag_button_index, MouseButton::Left as i32);
355 assert_eq!(raw.select_button_index, MouseButton::Right as i32);
356 assert_eq!(raw.navigate_button_index, MouseButton::Middle as i32);
357 assert_eq!(raw.context_menu_button_index, MouseButton::Extra1 as i32);
358 assert_eq!(raw.custom_zoom_level_count, 3);
359 assert!(!raw.custom_zoom_levels.is_null());
360 }
361
362 #[test]
363 fn editor_exposes_creation_config_snapshot() {
364 let _guard = test_guard();
365 let imgui = ImGuiContext::create();
366 let editor = EditorContext::create_with_config(
367 &imgui,
368 EditorConfig::new()
369 .no_settings_file()
370 .canvas_size_mode(crate::CanvasSizeMode::CenterOnly)
371 .custom_zoom_levels(vec![0.75, 1.0, 1.5])
372 .smooth_zoom(true, 1.4),
373 );
374
375 let snapshot = editor.config();
376 assert_eq!(snapshot.settings_file, None);
377 assert_eq!(snapshot.canvas_size_mode, crate::CanvasSizeMode::CenterOnly);
378 assert_eq!(snapshot.custom_zoom_levels, vec![0.75, 1.0, 1.5]);
379 assert!(snapshot.enable_smooth_zoom);
380 assert_eq!(snapshot.smooth_zoom_power, 1.4);
381 }
382
383 #[test]
384 fn style_snapshot_roundtrips_color() {
385 let _guard = test_guard();
386 let imgui = ImGuiContext::create();
387 let _editor = EditorContext::create(&imgui);
388
389 let original = _editor.style_color(StyleColor::Background);
390 let updated = [0.11, 0.22, 0.33, 0.44];
391 _editor.set_style_color(StyleColor::Background, updated);
392 assert_eq!(_editor.style_color(StyleColor::Background), updated);
393
394 let mut style = _editor.style();
395 style.set_color(StyleColor::Background, original);
396 _editor.set_style(&style);
397 assert_eq!(_editor.style_color(StyleColor::Background), original);
398 }
399}