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_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}