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}