dear_imgui_reflect/
response.rs

1//! Response types and helpers for dear-imgui-reflect.
2//!
3//! This module defines [`ReflectResponse`] and [`ReflectEvent`], a lightweight
4//! analogue to ImReflect's `ImResponse` that focuses on container-structure
5//! changes (insert/remove/reorder/rename). It also provides internal helpers
6//! used by the derive macro and container editors.
7
8use std::borrow::Cow;
9use std::cell::RefCell;
10
11/// High-level response information collected during a reflection-driven UI pass.
12///
13/// This is a lightweight, ImReflect-style response object that records
14/// container-level structural edits (insert/remove/reorder/rename) while a
15/// reflected editor is rendered. It is designed to complement the boolean
16/// return value from `input`, which already reports whether any value was
17/// modified, without changing existing APIs.
18#[derive(Default, Debug)]
19pub struct ReflectResponse {
20    events: Vec<ReflectEvent>,
21}
22
23impl ReflectResponse {
24    /// Returns `true` if no events were recorded during the last input pass.
25    pub fn is_empty(&self) -> bool {
26        self.events.is_empty()
27    }
28
29    /// Returns a slice of all events recorded so far.
30    pub fn events(&self) -> &[ReflectEvent] {
31        &self.events
32    }
33
34    /// Clears all recorded events.
35    pub fn clear(&mut self) {
36        self.events.clear();
37    }
38}
39
40/// A single structural change observed while rendering reflected UI.
41///
42/// These events focus on container structure (insert/remove/reorder/rename)
43/// rather than low-level pointer or interaction details, providing a
44/// simplified analogue to ImReflect's richer `ImResponse` type.
45#[non_exhaustive]
46#[derive(Clone, Debug)]
47pub enum ReflectEvent {
48    /// A vector had an element inserted at the given index.
49    VecInserted {
50        /// Logical field path associated with the vector, if known.
51        path: Option<String>,
52        /// Index where the new element was inserted.
53        index: usize,
54    },
55    /// A vector element was removed from the given index.
56    VecRemoved {
57        /// Logical field path associated with the vector, if known.
58        path: Option<String>,
59        /// Index from which the element was removed.
60        index: usize,
61    },
62    /// A vector element was moved from `from` to `to` (indices in the final layout).
63    VecReordered {
64        /// Logical field path associated with the vector, if known.
65        path: Option<String>,
66        /// Original index of the moved element.
67        from: usize,
68        /// Final index of the moved element after reordering.
69        to: usize,
70    },
71    /// All elements were removed from a vector that previously contained `previous_len` items.
72    VecCleared {
73        /// Logical field path associated with the vector, if known.
74        path: Option<String>,
75        /// Number of elements that were present before the clear operation.
76        previous_len: usize,
77    },
78    /// A fixed-size array had two elements swapped.
79    ArrayReordered {
80        /// Logical field path associated with the array, if known.
81        path: Option<String>,
82        /// First index in the swap operation.
83        from: usize,
84        /// Second index in the swap operation.
85        to: usize,
86    },
87    /// A map entry with the given key was inserted.
88    MapInserted {
89        /// Logical field path associated with the map, if known.
90        path: Option<String>,
91        /// Key for the newly inserted entry.
92        key: String,
93    },
94    /// A map entry with the given key was removed.
95    MapRemoved {
96        /// Logical field path associated with the map, if known.
97        path: Option<String>,
98        /// Key for the removed entry.
99        key: String,
100    },
101    /// A map entry key was renamed from `from` to `to`.
102    MapRenamed {
103        /// Logical field path associated with the map, if known.
104        path: Option<String>,
105        /// Original key of the entry.
106        from: String,
107        /// New key assigned to the entry.
108        to: String,
109    },
110    /// All entries were removed from a map that previously contained `previous_len` items.
111    MapCleared {
112        /// Logical field path associated with the map, if known.
113        path: Option<String>,
114        /// Number of entries that were present before the clear operation.
115        previous_len: usize,
116    },
117}
118
119thread_local! {
120    /// Stack of active response collectors for the current thread.
121    ///
122    /// This allows `input_with_response` to temporarily install a
123    /// [`ReflectResponse`] that container editors can emit events into without
124    /// changing existing ImGuiValue/ImGuiReflect signatures.
125    static CURRENT_RESPONSE: RefCell<Vec<*mut ReflectResponse>> = const { RefCell::new(Vec::new()) };
126
127    /// Stack of logical field-path segments for the current thread.
128    ///
129    /// This is populated by the derive macro when rendering struct fields so
130    /// that container events can be associated with a stable field path such
131    /// as `"primitives.samples"`. Only code generated by the derive macro is
132    /// expected to interact with this stack.
133    static CURRENT_FIELD_PATH: RefCell<Vec<Cow<'static, str>>> = const { RefCell::new(Vec::new()) };
134}
135
136/// Executes `f` with `response` installed as the current response collector.
137pub(crate) fn with_response<R, F>(response: &mut ReflectResponse, f: F) -> R
138where
139    F: FnOnce() -> R,
140{
141    struct ResponseGuard;
142
143    impl Drop for ResponseGuard {
144        fn drop(&mut self) {
145            CURRENT_RESPONSE.with(|stack| {
146                if let Ok(mut stack) = stack.try_borrow_mut() {
147                    stack.pop();
148                }
149            });
150        }
151    }
152
153    CURRENT_RESPONSE.with(|stack| {
154        stack.borrow_mut().push(response as *mut _);
155    });
156    let _guard = ResponseGuard;
157    f()
158}
159
160/// Returns the current logical field path if one is active.
161pub(crate) fn current_field_path() -> Option<String> {
162    CURRENT_FIELD_PATH.with(|stack| {
163        let stack = stack.borrow();
164        if stack.is_empty() {
165            None
166        } else {
167            let mut path = String::new();
168            for (i, segment) in stack.iter().enumerate() {
169                let segment = segment.as_ref();
170                if i > 0 && !segment.starts_with('[') {
171                    path.push('.');
172                }
173                path.push_str(segment);
174            }
175            Some(path)
176        }
177    })
178}
179
180pub(crate) fn is_field_path_active() -> bool {
181    CURRENT_FIELD_PATH.with(|stack| !stack.borrow().is_empty())
182}
183
184/// Pushes a field-path segment for the duration of the provided closure.
185///
186/// This is intended for use by code generated from the derive macro; regular
187/// users should prefer the higher-level `input_with_response` API.
188#[doc(hidden)]
189pub fn with_field_path<R, F>(segment: &str, f: F) -> R
190where
191    F: FnOnce() -> R,
192{
193    struct FieldPathGuard;
194
195    impl Drop for FieldPathGuard {
196        fn drop(&mut self) {
197            CURRENT_FIELD_PATH.with(|stack| {
198                if let Ok(mut stack) = stack.try_borrow_mut() {
199                    stack.pop();
200                }
201            });
202        }
203    }
204
205    CURRENT_FIELD_PATH.with(|stack| {
206        stack.borrow_mut().push(Cow::Owned(segment.to_owned()));
207    });
208    let _guard = FieldPathGuard;
209    f()
210}
211
212/// Pushes a `'static` field-path segment for the duration of the provided closure.
213///
214/// This is an optimized variant of [`with_field_path`] used by code generated
215/// from the derive macro to avoid per-frame string allocations.
216#[doc(hidden)]
217pub fn with_field_path_static<R, F>(segment: &'static str, f: F) -> R
218where
219    F: FnOnce() -> R,
220{
221    struct FieldPathGuard;
222
223    impl Drop for FieldPathGuard {
224        fn drop(&mut self) {
225            CURRENT_FIELD_PATH.with(|stack| {
226                if let Ok(mut stack) = stack.try_borrow_mut() {
227                    stack.pop();
228                }
229            });
230        }
231    }
232
233    CURRENT_FIELD_PATH.with(|stack| {
234        stack.borrow_mut().push(Cow::Borrowed(segment));
235    });
236    let _guard = FieldPathGuard;
237    f()
238}
239
240/// Records a new event into the currently active response collector, if any.
241pub(crate) fn record_event(event: ReflectEvent) {
242    CURRENT_RESPONSE.with(|stack| {
243        if let Some(ptr) = stack.borrow().last().copied() {
244            // SAFETY: pointers stored in CURRENT_RESPONSE are derived from
245            // &mut ReflectResponse references passed to `with_response` and
246            // remain valid for the duration of the closure.
247            unsafe {
248                (&mut *ptr).events.push(event);
249            }
250        }
251    });
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    #[test]
259    fn collects_events_in_order() {
260        let mut resp = ReflectResponse::default();
261
262        with_response(&mut resp, || {
263            record_event(ReflectEvent::VecInserted {
264                path: None,
265                index: 1,
266            });
267            record_event(ReflectEvent::VecRemoved {
268                path: None,
269                index: 2,
270            });
271        });
272
273        let events = resp.events();
274        assert_eq!(events.len(), 2);
275
276        match &events[0] {
277            ReflectEvent::VecInserted { index, path } => {
278                assert_eq!(*index, 1);
279                assert!(path.is_none());
280            }
281            other => panic!("unexpected first event: {other:?}"),
282        }
283
284        match &events[1] {
285            ReflectEvent::VecRemoved { index, path } => {
286                assert_eq!(*index, 2);
287                assert!(path.is_none());
288            }
289            other => panic!("unexpected second event: {other:?}"),
290        }
291    }
292
293    #[test]
294    fn field_path_stack_builds_nested_paths() {
295        let mut resp = ReflectResponse::default();
296
297        with_response(&mut resp, || {
298            // No active field path.
299            record_event(ReflectEvent::VecInserted {
300                path: current_field_path(),
301                index: 0,
302            });
303
304            // Single segment.
305            with_field_path("outer", || {
306                record_event(ReflectEvent::VecInserted {
307                    path: current_field_path(),
308                    index: 1,
309                });
310
311                // Nested segment.
312                with_field_path("inner[0]", || {
313                    record_event(ReflectEvent::VecInserted {
314                        path: current_field_path(),
315                        index: 2,
316                    });
317                });
318            });
319        });
320
321        let events = resp.events();
322        assert_eq!(events.len(), 3);
323
324        match &events[0] {
325            ReflectEvent::VecInserted { path, index } => {
326                assert_eq!(*index, 0);
327                assert!(path.is_none());
328            }
329            other => panic!("unexpected first event: {other:?}"),
330        }
331
332        match &events[1] {
333            ReflectEvent::VecInserted { path, index } => {
334                assert_eq!(*index, 1);
335                assert_eq!(path.as_deref(), Some("outer"));
336            }
337            other => panic!("unexpected second event: {other:?}"),
338        }
339
340        match &events[2] {
341            ReflectEvent::VecInserted { path, index } => {
342                assert_eq!(*index, 2);
343                assert_eq!(path.as_deref(), Some("outer.inner[0]"));
344            }
345            other => panic!("unexpected third event: {other:?}"),
346        }
347    }
348
349    #[test]
350    fn response_and_field_path_stacks_restore_on_panic() {
351        let mut resp = ReflectResponse::default();
352
353        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
354            with_response(&mut resp, || {
355                with_field_path("a", || {
356                    record_event(ReflectEvent::VecInserted {
357                        path: current_field_path(),
358                        index: 1,
359                    });
360                    panic!("boom");
361                });
362            });
363        }));
364
365        // Stacks must be clean after unwind.
366        assert!(current_field_path().is_none());
367        record_event(ReflectEvent::VecInserted {
368            path: None,
369            index: 2,
370        });
371        assert_eq!(resp.events.len(), 1);
372    }
373
374    #[test]
375    fn field_path_segments_starting_with_bracket_do_not_insert_dots() {
376        let mut resp = ReflectResponse::default();
377
378        with_response(&mut resp, || {
379            with_field_path("outer", || {
380                with_field_path("[0]", || {
381                    record_event(ReflectEvent::VecInserted {
382                        path: current_field_path(),
383                        index: 0,
384                    });
385                });
386                with_field_path("[1]", || {
387                    with_field_path("inner", || {
388                        record_event(ReflectEvent::VecInserted {
389                            path: current_field_path(),
390                            index: 1,
391                        });
392                    });
393                });
394            });
395        });
396
397        let events = resp.events();
398        assert_eq!(events.len(), 2);
399        match &events[0] {
400            ReflectEvent::VecInserted { path, .. } => assert_eq!(path.as_deref(), Some("outer[0]")),
401            other => panic!("unexpected event: {other:?}"),
402        }
403        match &events[1] {
404            ReflectEvent::VecInserted { path, .. } => {
405                assert_eq!(path.as_deref(), Some("outer[1].inner"))
406            }
407            other => panic!("unexpected event: {other:?}"),
408        }
409    }
410}