Skip to main content

dear_node_editor/
context.rs

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/// Errors returned by the node-editor safe layer.
6#[derive(Debug, thiserror::Error)]
7pub enum NodeEditorError {
8    #[error("imgui-node-editor CreateEditor returned null")]
9    CreateEditorFailed,
10}
11
12/// Owned imgui-node-editor context.
13pub 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}