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
9pub 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#[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
71pub 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}