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 fn smooth_zoom(mut self, enabled: bool, power: f32) -> Self {
166        assert!(
167            power.is_finite() && power > 0.0,
168            "smooth zoom power must be positive"
169        );
170        self.enable_smooth_zoom = enabled;
171        self.smooth_zoom_power = power;
172        self
173    }
174
175    pub fn snapshot(&self) -> EditorConfigSnapshot {
176        EditorConfigSnapshot {
177            settings_file: self
178                .settings_file
179                .as_ref()
180                .map(|path| path.to_string_lossy().into_owned()),
181            has_settings_handler: self.callbacks.is_some(),
182            custom_zoom_levels: self.custom_zoom_levels.clone(),
183            canvas_size_mode: self.canvas_size_mode,
184            drag_button: self.drag_button,
185            select_button: self.select_button,
186            navigate_button: self.navigate_button,
187            context_menu_button: self.context_menu_button,
188            enable_smooth_zoom: self.enable_smooth_zoom,
189            smooth_zoom_power: self.smooth_zoom_power,
190        }
191    }
192
193    pub(crate) fn to_sys(&mut self) -> sys::DneConfig {
194        let has_callbacks = self.callbacks.is_some();
195        sys::DneConfig {
196            settings_file: self
197                .settings_file
198                .as_ref()
199                .map_or(ptr::null(), |s| s.as_ptr()),
200            begin_save_session: if has_callbacks {
201                Some(begin_save_session)
202            } else {
203                None
204            },
205            end_save_session: if has_callbacks {
206                Some(end_save_session)
207            } else {
208                None
209            },
210            save_settings: if has_callbacks {
211                Some(save_settings)
212            } else {
213                None
214            },
215            load_settings: if has_callbacks {
216                Some(load_settings)
217            } else {
218                None
219            },
220            save_node_settings: if has_callbacks {
221                Some(save_node_settings)
222            } else {
223                None
224            },
225            load_node_settings: if has_callbacks {
226                Some(load_node_settings)
227            } else {
228                None
229            },
230            user_pointer: self
231                .callbacks
232                .as_deref_mut()
233                .map_or(ptr::null_mut(), |state| state as *mut _ as *mut c_void),
234            custom_zoom_levels: if self.custom_zoom_levels.is_empty() {
235                ptr::null()
236            } else {
237                self.custom_zoom_levels.as_ptr()
238            },
239            custom_zoom_level_count: self.custom_zoom_levels.len() as i32,
240            canvas_size_mode: self.canvas_size_mode.raw(),
241            drag_button_index: self.drag_button as i32,
242            select_button_index: self.select_button as i32,
243            navigate_button_index: self.navigate_button as i32,
244            context_menu_button_index: self.context_menu_button as i32,
245            enable_smooth_zoom: self.enable_smooth_zoom,
246            smooth_zoom_power: self.smooth_zoom_power,
247        }
248    }
249}
250
251unsafe fn callback_state<'a>(user_pointer: *mut c_void) -> Option<&'a mut CallbackState> {
252    if user_pointer.is_null() {
253        None
254    } else {
255        Some(unsafe { &mut *(user_pointer as *mut CallbackState) })
256    }
257}
258
259unsafe extern "C" fn begin_save_session(user_pointer: *mut c_void) {
260    let _ = catch_unwind(AssertUnwindSafe(|| {
261        if let Some(state) = unsafe { callback_state(user_pointer) } {
262            state.handler.begin_save_session();
263        }
264    }));
265}
266
267unsafe extern "C" fn end_save_session(user_pointer: *mut c_void) {
268    let _ = catch_unwind(AssertUnwindSafe(|| {
269        if let Some(state) = unsafe { callback_state(user_pointer) } {
270            state.handler.end_save_session();
271        }
272    }));
273}
274
275unsafe extern "C" fn save_settings(
276    data: *const c_char,
277    size: usize,
278    reason: sys::DneSaveReasonFlags,
279    user_pointer: *mut c_void,
280) -> bool {
281    catch_unwind(AssertUnwindSafe(|| {
282        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
283            return false;
284        };
285        let bytes = if data.is_null() || size == 0 {
286            &[]
287        } else {
288            unsafe { std::slice::from_raw_parts(data as *const u8, size) }
289        };
290        state
291            .handler
292            .save_settings(bytes, SaveReasonFlags::from_bits_retain(reason as u32))
293    }))
294    .unwrap_or(false)
295}
296
297unsafe extern "C" fn load_settings(data: *mut c_char, user_pointer: *mut c_void) -> usize {
298    catch_unwind(AssertUnwindSafe(|| {
299        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
300            return 0;
301        };
302        if data.is_null() {
303            state.scratch = state.handler.load_settings().unwrap_or_default();
304            state.scratch.len()
305        } else {
306            unsafe {
307                ptr::copy_nonoverlapping(
308                    state.scratch.as_ptr(),
309                    data as *mut u8,
310                    state.scratch.len(),
311                );
312            }
313            state.scratch.len()
314        }
315    }))
316    .unwrap_or(0)
317}
318
319unsafe extern "C" fn save_node_settings(
320    node_id: usize,
321    data: *const c_char,
322    size: usize,
323    reason: sys::DneSaveReasonFlags,
324    user_pointer: *mut c_void,
325) -> bool {
326    catch_unwind(AssertUnwindSafe(|| {
327        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
328            return false;
329        };
330        let bytes = if data.is_null() || size == 0 {
331            &[]
332        } else {
333            unsafe { std::slice::from_raw_parts(data as *const u8, size) }
334        };
335        state.handler.save_node_settings(
336            NodeId(node_id),
337            bytes,
338            SaveReasonFlags::from_bits_retain(reason as u32),
339        )
340    }))
341    .unwrap_or(false)
342}
343
344unsafe extern "C" fn load_node_settings(
345    node_id: usize,
346    data: *mut c_char,
347    user_pointer: *mut c_void,
348) -> usize {
349    catch_unwind(AssertUnwindSafe(|| {
350        let Some(state) = (unsafe { callback_state(user_pointer) }) else {
351            return 0;
352        };
353        if data.is_null() {
354            state.scratch = state
355                .handler
356                .load_node_settings(NodeId(node_id))
357                .unwrap_or_default();
358            state.scratch.len()
359        } else {
360            unsafe {
361                ptr::copy_nonoverlapping(
362                    state.scratch.as_ptr(),
363                    data as *mut u8,
364                    state.scratch.len(),
365                );
366            }
367            state.scratch.len()
368        }
369    }))
370    .unwrap_or(0)
371}