Skip to main content

dear_node_editor/
config.rs

1use crate::{CanvasSizeMode, NodeId, SaveReasonFlags, sys};
2use dear_imgui_rs::MouseButton;
3use std::{
4    ffi::{CString, NulError, c_char, c_void},
5    panic::{AssertUnwindSafe, catch_unwind},
6    ptr,
7};
8
9/// User-defined persistence hooks for an editor context.
10///
11/// The handler is owned by [`EditorContext`](crate::EditorContext), so every
12/// callback remains valid until the native editor has been destroyed.
13pub trait SettingsHandler {
14    fn begin_save_session(&mut self) {}
15    fn end_save_session(&mut self) {}
16
17    fn save_settings(&mut self, _data: &[u8], _reason: SaveReasonFlags) -> bool {
18        false
19    }
20
21    fn load_settings(&mut self) -> Option<Vec<u8>> {
22        None
23    }
24
25    fn save_node_settings(
26        &mut self,
27        _node: NodeId,
28        _data: &[u8],
29        _reason: SaveReasonFlags,
30    ) -> bool {
31        false
32    }
33
34    fn load_node_settings(&mut self, _node: NodeId) -> Option<Vec<u8>> {
35        None
36    }
37}
38
39pub(crate) struct CallbackState {
40    handler: Box<dyn SettingsHandler>,
41    scratch: Vec<u8>,
42}
43
44impl CallbackState {
45    pub(crate) fn new(handler: Box<dyn SettingsHandler>) -> Self {
46        Self {
47            handler,
48            scratch: Vec::new(),
49        }
50    }
51}
52
53/// Immutable view of the configuration used to create an editor context.
54///
55/// Callback function pointers and native storage pointers are intentionally not
56/// exposed. The snapshot only reports whether a settings handler was installed.
57#[derive(Clone, Debug, PartialEq)]
58pub struct EditorConfigSnapshot {
59    pub settings_file: Option<String>,
60    pub has_settings_handler: bool,
61    pub custom_zoom_levels: Vec<f32>,
62    pub canvas_size_mode: CanvasSizeMode,
63    pub drag_button: MouseButton,
64    pub select_button: MouseButton,
65    pub navigate_button: MouseButton,
66    pub context_menu_button: MouseButton,
67    pub enable_smooth_zoom: bool,
68    pub smooth_zoom_power: f32,
69}
70
71/// Configuration used when creating an editor context.
72pub struct EditorConfig {
73    pub(crate) settings_file: Option<CString>,
74    pub(crate) callbacks: Option<Box<CallbackState>>,
75    pub(crate) custom_zoom_levels: Vec<f32>,
76    pub(crate) canvas_size_mode: CanvasSizeMode,
77    pub(crate) drag_button: MouseButton,
78    pub(crate) select_button: MouseButton,
79    pub(crate) navigate_button: MouseButton,
80    pub(crate) context_menu_button: MouseButton,
81    pub(crate) enable_smooth_zoom: bool,
82    pub(crate) smooth_zoom_power: f32,
83}
84
85impl Default for EditorConfig {
86    fn default() -> Self {
87        Self {
88            settings_file: None,
89            callbacks: None,
90            custom_zoom_levels: Vec::new(),
91            canvas_size_mode: CanvasSizeMode::FitVerticalView,
92            drag_button: MouseButton::Left,
93            select_button: MouseButton::Left,
94            navigate_button: MouseButton::Right,
95            context_menu_button: MouseButton::Right,
96            enable_smooth_zoom: false,
97            smooth_zoom_power: if cfg!(target_os = "macos") { 1.1 } else { 1.3 },
98        }
99    }
100}
101
102impl EditorConfig {
103    pub fn new() -> Self {
104        Self::default()
105    }
106
107    pub fn settings_file(mut self, path: impl AsRef<str>) -> Result<Self, NulError> {
108        self.settings_file = Some(CString::new(path.as_ref())?);
109        Ok(self)
110    }
111
112    pub fn no_settings_file(mut self) -> Self {
113        self.settings_file = None;
114        self
115    }
116
117    pub fn settings_handler(mut self, handler: impl SettingsHandler + 'static) -> Self {
118        self.callbacks = Some(Box::new(CallbackState::new(Box::new(handler))));
119        self
120    }
121
122    pub fn canvas_size_mode(mut self, mode: CanvasSizeMode) -> Self {
123        self.canvas_size_mode = mode;
124        self
125    }
126
127    pub fn custom_zoom_levels(mut self, levels: impl Into<Vec<f32>>) -> Self {
128        let levels = levels.into();
129        assert!(
130            levels.iter().all(|value| value.is_finite() && *value > 0.0),
131            "custom zoom levels must be positive finite values"
132        );
133        assert!(
134            levels.windows(2).all(|pair| pair[0] < pair[1]),
135            "custom zoom levels must be strictly increasing"
136        );
137        assert!(
138            levels.len() <= i32::MAX as usize,
139            "custom zoom levels exceed i32::MAX"
140        );
141        self.custom_zoom_levels = levels;
142        self
143    }
144
145    pub fn drag_button(mut self, button: MouseButton) -> Self {
146        self.drag_button = button;
147        self
148    }
149
150    pub fn select_button(mut self, button: MouseButton) -> Self {
151        self.select_button = button;
152        self
153    }
154
155    pub fn navigate_button(mut self, button: MouseButton) -> Self {
156        self.navigate_button = button;
157        self
158    }
159
160    pub fn context_menu_button(mut self, button: MouseButton) -> Self {
161        self.context_menu_button = button;
162        self
163    }
164
165    pub unsafe fn drag_button_index_unchecked(mut self, index: i32) -> Self {
166        self.drag_button = mouse_button_from_index(index);
167        self
168    }
169
170    pub unsafe fn select_button_index_unchecked(mut self, index: i32) -> Self {
171        self.select_button = mouse_button_from_index(index);
172        self
173    }
174
175    pub unsafe fn navigate_button_index_unchecked(mut self, index: i32) -> Self {
176        self.navigate_button = mouse_button_from_index(index);
177        self
178    }
179
180    pub unsafe fn context_menu_button_index_unchecked(mut self, index: i32) -> Self {
181        self.context_menu_button = mouse_button_from_index(index);
182        self
183    }
184
185    pub fn smooth_zoom(mut self, enabled: bool, power: f32) -> Self {
186        assert!(
187            power.is_finite() && power > 0.0,
188            "smooth zoom power must be positive"
189        );
190        self.enable_smooth_zoom = enabled;
191        self.smooth_zoom_power = power;
192        self
193    }
194
195    pub fn snapshot(&self) -> EditorConfigSnapshot {
196        EditorConfigSnapshot {
197            settings_file: self
198                .settings_file
199                .as_ref()
200                .map(|path| path.to_string_lossy().into_owned()),
201            has_settings_handler: self.callbacks.is_some(),
202            custom_zoom_levels: self.custom_zoom_levels.clone(),
203            canvas_size_mode: self.canvas_size_mode,
204            drag_button: self.drag_button,
205            select_button: self.select_button,
206            navigate_button: self.navigate_button,
207            context_menu_button: self.context_menu_button,
208            enable_smooth_zoom: self.enable_smooth_zoom,
209            smooth_zoom_power: self.smooth_zoom_power,
210        }
211    }
212
213    pub(crate) fn to_sys(&mut self) -> sys::DneConfig {
214        let has_callbacks = self.callbacks.is_some();
215        sys::DneConfig {
216            settings_file: self
217                .settings_file
218                .as_ref()
219                .map_or(ptr::null(), |s| s.as_ptr()),
220            begin_save_session: if has_callbacks {
221                Some(begin_save_session)
222            } else {
223                None
224            },
225            end_save_session: if has_callbacks {
226                Some(end_save_session)
227            } else {
228                None
229            },
230            save_settings: if has_callbacks {
231                Some(save_settings)
232            } else {
233                None
234            },
235            load_settings: if has_callbacks {
236                Some(load_settings)
237            } else {
238                None
239            },
240            save_node_settings: if has_callbacks {
241                Some(save_node_settings)
242            } else {
243                None
244            },
245            load_node_settings: if has_callbacks {
246                Some(load_node_settings)
247            } else {
248                None
249            },
250            user_pointer: self
251                .callbacks
252                .as_deref_mut()
253                .map_or(ptr::null_mut(), |state| state as *mut _ as *mut c_void),
254            custom_zoom_levels: if self.custom_zoom_levels.is_empty() {
255                ptr::null()
256            } else {
257                self.custom_zoom_levels.as_ptr()
258            },
259            custom_zoom_level_count: self.custom_zoom_levels.len() as i32,
260            canvas_size_mode: self.canvas_size_mode.raw(),
261            drag_button_index: self.drag_button as i32,
262            select_button_index: self.select_button as i32,
263            navigate_button_index: self.navigate_button as i32,
264            context_menu_button_index: self.context_menu_button as i32,
265            enable_smooth_zoom: self.enable_smooth_zoom,
266            smooth_zoom_power: self.smooth_zoom_power,
267        }
268    }
269}
270
271fn mouse_button_from_index(index: i32) -> MouseButton {
272    match index {
273        value if value == MouseButton::Left as i32 => MouseButton::Left,
274        value if value == MouseButton::Right as i32 => MouseButton::Right,
275        value if value == MouseButton::Middle as i32 => MouseButton::Middle,
276        value if value == MouseButton::Extra1 as i32 => MouseButton::Extra1,
277        value if value == MouseButton::Extra2 as i32 => MouseButton::Extra2,
278        _ => panic!("mouse button index must be a valid Dear ImGui mouse button"),
279    }
280}
281
282unsafe fn callback_state<'a>(user_pointer: *mut c_void) -> Option<&'a mut CallbackState> {
283    if user_pointer.is_null() {
284        None
285    } else {
286        Some(unsafe { &mut *(user_pointer as *mut CallbackState) })
287    }
288}
289
290unsafe extern "C" fn begin_save_session(user_pointer: *mut c_void) {
291    let _ = catch_unwind(AssertUnwindSafe(|| {
292        if let Some(state) = unsafe { callback_state(user_pointer) } {
293            state.handler.begin_save_session();
294        }
295    }));
296}
297
298unsafe extern "C" fn end_save_session(user_pointer: *mut c_void) {
299    let _ = catch_unwind(AssertUnwindSafe(|| {
300        if let Some(state) = unsafe { callback_state(user_pointer) } {
301            state.handler.end_save_session();
302        }
303    }));
304}
305
306unsafe extern "C" fn save_settings(
307    data: *const c_char,
308    size: usize,
309    reason: sys::DneSaveReasonFlags,
310    user_pointer: *mut c_void,
311) -> bool {
312    catch_unwind(AssertUnwindSafe(|| {
313        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
314            return false;
315        };
316        let bytes = if data.is_null() || size == 0 {
317            &[]
318        } else {
319            unsafe { std::slice::from_raw_parts(data as *const u8, size) }
320        };
321        state
322            .handler
323            .save_settings(bytes, SaveReasonFlags::from_bits_retain(reason as u32))
324    }))
325    .unwrap_or(false)
326}
327
328unsafe extern "C" fn load_settings(data: *mut c_char, user_pointer: *mut c_void) -> usize {
329    catch_unwind(AssertUnwindSafe(|| {
330        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
331            return 0;
332        };
333        if data.is_null() {
334            state.scratch = state.handler.load_settings().unwrap_or_default();
335            state.scratch.len()
336        } else {
337            unsafe {
338                ptr::copy_nonoverlapping(
339                    state.scratch.as_ptr(),
340                    data as *mut u8,
341                    state.scratch.len(),
342                );
343            }
344            state.scratch.len()
345        }
346    }))
347    .unwrap_or(0)
348}
349
350unsafe extern "C" fn save_node_settings(
351    node_id: usize,
352    data: *const c_char,
353    size: usize,
354    reason: sys::DneSaveReasonFlags,
355    user_pointer: *mut c_void,
356) -> bool {
357    catch_unwind(AssertUnwindSafe(|| {
358        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
359            return false;
360        };
361        let bytes = if data.is_null() || size == 0 {
362            &[]
363        } else {
364            unsafe { std::slice::from_raw_parts(data as *const u8, size) }
365        };
366        state.handler.save_node_settings(
367            NodeId(node_id),
368            bytes,
369            SaveReasonFlags::from_bits_retain(reason as u32),
370        )
371    }))
372    .unwrap_or(false)
373}
374
375unsafe extern "C" fn load_node_settings(
376    node_id: usize,
377    data: *mut c_char,
378    user_pointer: *mut c_void,
379) -> usize {
380    catch_unwind(AssertUnwindSafe(|| {
381        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
382            return 0;
383        };
384        if data.is_null() {
385            state.scratch = state
386                .handler
387                .load_node_settings(NodeId(node_id))
388                .unwrap_or_default();
389            state.scratch.len()
390        } else {
391            unsafe {
392                ptr::copy_nonoverlapping(
393                    state.scratch.as_ptr(),
394                    data as *mut u8,
395                    state.scratch.len(),
396                );
397            }
398            state.scratch.len()
399        }
400    }))
401    .unwrap_or(0)
402}