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!(
98 !self.raw.is_null(),
99 "{caller} requires a valid node-editor context"
100 );
101 }
102
103 pub(crate) fn bind_current(&self, caller: &str) -> CurrentEditorGuard<'_> {
104 self.assert_usable(caller);
105 let imgui_guard = ImGuiContextGuard::bind(self.imgui_ctx_raw);
106 let previous = unsafe { sys::dne_get_current_editor_raw() };
107 unsafe { sys::dne_set_current_editor(self.raw) };
108 CurrentEditorGuard {
109 _editor: self,
110 _imgui_guard: imgui_guard,
111 previous,
112 }
113 }
114}
115
116impl Drop for EditorContext {
117 fn drop(&mut self) {
118 if self.raw.is_null() {
119 return;
120 }
121
122 if !self.imgui_alive.is_alive() {
123 debug_assert!(
124 false,
125 "EditorContext was dropped after its owning Dear ImGui context; \
126 declare the editor field before the Context field or drop it explicitly first"
127 );
128 self.raw = ptr::null_mut();
129 return;
130 }
131
132 let _imgui_guard = ImGuiContextGuard::bind(self.imgui_ctx_raw);
133 unsafe { sys::dne_destroy_editor(self.raw) };
134 self.raw = ptr::null_mut();
135 }
136}
137
138pub(crate) struct CurrentEditorGuard<'a> {
139 _editor: &'a EditorContext,
140 _imgui_guard: ImGuiContextGuard,
141 previous: *mut c_void,
142}
143
144impl Drop for CurrentEditorGuard<'_> {
145 fn drop(&mut self) {
146 unsafe { sys::dne_set_current_editor_raw(self.previous) };
147 }
148}
149
150struct ImGuiContextGuard {
151 prev: *mut dear_imgui_rs::sys::ImGuiContext,
152 restore: bool,
153}
154
155impl ImGuiContextGuard {
156 fn bind(ctx: *mut dear_imgui_rs::sys::ImGuiContext) -> Self {
157 let prev = unsafe { dear_imgui_rs::sys::igGetCurrentContext() };
158 let restore = prev != ctx;
159 if restore {
160 unsafe { dear_imgui_rs::sys::igSetCurrentContext(ctx) };
161 }
162 Self { prev, restore }
163 }
164}
165
166impl Drop for ImGuiContextGuard {
167 fn drop(&mut self) {
168 if self.restore {
169 unsafe { dear_imgui_rs::sys::igSetCurrentContext(self.prev) };
170 }
171 }
172}
173
174#[cfg(test)]
175mod tests {
176 use super::*;
177 use crate::{
178 EditorConfig, LinkId, NodeEditorUiExt, NodeId, PinId, PinKind, StyleColor, StyleVar,
179 };
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 frame_tokens_bind_own_editor_before_drop_and_restore_previous_editor() {
336 let _guard = test_guard();
337 let mut imgui = ImGuiContext::create();
338 imgui.io_mut().set_display_size([640.0, 480.0]);
339 imgui.io_mut().set_delta_time(1.0 / 60.0);
340 let _ = imgui.font_atlas_mut().build();
341
342 let editor_a = EditorContext::create(&imgui);
343 let editor_b = EditorContext::create(&imgui);
344 let raw_a = editor_a.as_raw_native();
345 let raw_b = editor_b.as_raw_native();
346
347 let ui = imgui.frame();
348 ui.window("node-editor-token-context").build(|| {
349 let frame = ui.node_editor(&editor_a, "token-context", [320.0, 240.0]);
350
351 let style = frame.push_style_var_float(StyleVar::LinkStrength, 0.75);
352 unsafe { sys::dne_set_current_editor_raw(raw_b) };
353 drop(style);
354 assert_eq!(unsafe { sys::dne_get_current_editor_raw() }, raw_b);
355
356 let node = frame.begin_node(NodeId::new(1));
357 unsafe { sys::dne_set_current_editor_raw(raw_b) };
358 drop(node);
359 assert_eq!(unsafe { sys::dne_get_current_editor_raw() }, raw_b);
360
361 unsafe { sys::dne_set_current_editor_raw(raw_b) };
362 drop(frame);
363 assert_eq!(unsafe { sys::dne_get_current_editor_raw() }, raw_b);
364 });
365 imgui.render();
366
367 unsafe { sys::dne_set_current_editor_raw(ptr::null_mut()) };
368
369 let _ = raw_a;
370 }
371
372 #[test]
373 fn config_accepts_typed_buttons_and_custom_zoom_levels() {
374 let mut config = EditorConfig::new()
375 .drag_button(MouseButton::Left)
376 .select_button(MouseButton::Right)
377 .navigate_button(MouseButton::Middle)
378 .context_menu_button(MouseButton::Extra1)
379 .smooth_zoom(true, 1.25)
380 .custom_zoom_levels(vec![0.5, 1.0, 2.0]);
381
382 let snapshot = config.snapshot();
383 assert_eq!(snapshot.custom_zoom_levels, vec![0.5, 1.0, 2.0]);
384 assert_eq!(snapshot.drag_button, MouseButton::Left);
385 assert_eq!(snapshot.select_button, MouseButton::Right);
386 assert_eq!(snapshot.navigate_button, MouseButton::Middle);
387 assert_eq!(snapshot.context_menu_button, MouseButton::Extra1);
388 assert!(snapshot.enable_smooth_zoom);
389 assert_eq!(snapshot.smooth_zoom_power, 1.25);
390
391 let raw = config.to_sys();
392 assert_eq!(raw.drag_button_index, MouseButton::Left as i32);
393 assert_eq!(raw.select_button_index, MouseButton::Right as i32);
394 assert_eq!(raw.navigate_button_index, MouseButton::Middle as i32);
395 assert_eq!(raw.context_menu_button_index, MouseButton::Extra1 as i32);
396 assert_eq!(raw.custom_zoom_level_count, 3);
397 assert!(!raw.custom_zoom_levels.is_null());
398 }
399
400 #[test]
401 fn editor_exposes_creation_config_snapshot() {
402 let _guard = test_guard();
403 let imgui = ImGuiContext::create();
404 let editor = EditorContext::create_with_config(
405 &imgui,
406 EditorConfig::new()
407 .no_settings_file()
408 .canvas_size_mode(crate::CanvasSizeMode::CenterOnly)
409 .custom_zoom_levels(vec![0.75, 1.0, 1.5])
410 .smooth_zoom(true, 1.4),
411 );
412
413 let snapshot = editor.config();
414 assert_eq!(snapshot.settings_file, None);
415 assert_eq!(snapshot.canvas_size_mode, crate::CanvasSizeMode::CenterOnly);
416 assert_eq!(snapshot.custom_zoom_levels, vec![0.75, 1.0, 1.5]);
417 assert!(snapshot.enable_smooth_zoom);
418 assert_eq!(snapshot.smooth_zoom_power, 1.4);
419 }
420
421 #[test]
422 fn style_snapshot_roundtrips_color() {
423 let _guard = test_guard();
424 let imgui = ImGuiContext::create();
425 let _editor = EditorContext::create(&imgui);
426
427 let original = _editor.style_color(StyleColor::Background);
428 let updated = [0.11, 0.22, 0.33, 0.44];
429 _editor.set_style_color(StyleColor::Background, updated);
430 assert_eq!(_editor.style_color(StyleColor::Background), updated);
431
432 let mut style = _editor.style();
433 style.set_color(StyleColor::Background, original);
434 _editor.set_style(&style);
435 assert_eq!(_editor.style_color(StyleColor::Background), original);
436 }
437}