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