egui_arbor/response.rs
1//! Response types for the outliner widget.
2//!
3//! This module provides types that represent the result of rendering an outliner widget,
4//! including information about user interactions and state changes.
5
6use crate::traits::DropPosition;
7use std::hash::Hash;
8use std::ops::Deref;
9
10/// The response from rendering an outliner widget.
11///
12/// This type wraps an [`egui::Response`] and provides additional information about
13/// outliner-specific events that occurred during the frame, such as node selection,
14/// double-clicks, context menu requests, renaming, and drag-drop operations.
15///
16/// # Generic Parameters
17///
18/// * `Id` - The type used to identify nodes in the outliner. Must implement
19/// [`Hash`], [`Eq`], and [`Clone`].
20///
21/// # Examples
22///
23/// ```ignore
24/// let response = outliner.show(ui, &mut state);
25///
26/// if let Some(id) = response.selected() {
27/// println!("Node selected: {:?}", id);
28/// }
29///
30/// if let Some((id, new_name)) = response.renamed() {
31/// println!("Node {} renamed to: {}", id, new_name);
32/// }
33///
34/// if let Some(drop_event) = response.drop_event() {
35/// println!("Dropped {:?} onto {:?}", drop_event.source, drop_event.target);
36/// }
37/// ```
38#[derive(Debug)]
39pub struct OutlinerResponse<Id>
40where
41 Id: Hash + Eq + Clone,
42{
43 /// The underlying egui widget response.
44 ///
45 /// This can be accessed directly via [`Deref`] to check standard widget properties
46 /// like hover state, clicks, etc.
47 pub inner: egui::Response,
48
49 /// Whether any outliner state changed this frame.
50 ///
51 /// This includes selection changes, expansion/collapse, renaming, etc.
52 /// Useful for determining if you need to save state or trigger updates.
53 pub changed: bool,
54
55 /// ID of the node that was newly selected this frame, if any.
56 ///
57 /// This is `Some` only when the selection changes, not on every frame
58 /// where a node is selected.
59 pub selected: Option<Id>,
60
61 /// ID of the node that was double-clicked this frame, if any.
62 ///
63 /// Double-clicking typically triggers an action like opening or editing a node.
64 pub double_clicked: Option<Id>,
65
66 /// ID of the node for which a context menu was requested this frame, if any.
67 ///
68 /// This is typically triggered by right-clicking on a node.
69 pub context_menu: Option<Id>,
70
71 /// ID and new name of a node that was renamed this frame, if any.
72 ///
73 /// The tuple contains `(node_id, new_name)`.
74 pub renamed: Option<(Id, String)>,
75
76 /// ID of the node where a drag operation started this frame, if any.
77 ///
78 /// This indicates the user began dragging a node.
79 pub drag_started: Option<Id>,
80
81 /// IDs of all nodes being dragged (includes the primary drag node and any selected nodes).
82 ///
83 /// When dragging with multiple selections, this contains all selected node IDs.
84 pub dragging_nodes: Vec<Id>,
85
86 /// Details of a drop event that occurred this frame, if any.
87 ///
88 /// This contains information about the source node, target node, and drop position.
89 pub drop_event: Option<DropEvent<Id>>,
90}
91
92impl<Id> OutlinerResponse<Id>
93where
94 Id: Hash + Eq + Clone,
95{
96 /// Creates a new outliner response with no events.
97 ///
98 /// All event fields are initialized to `None` and `changed` is set to `false`.
99 /// The widget implementation will populate these fields as events occur.
100 ///
101 /// # Arguments
102 ///
103 /// * `inner` - The underlying egui response from the widget
104 ///
105 /// # Examples
106 ///
107 /// ```ignore
108 /// let response = OutlinerResponse::new(ui.allocate_response(size, Sense::click()));
109 /// ```
110 pub fn new(inner: egui::Response) -> Self {
111 Self {
112 inner,
113 changed: false,
114 selected: None,
115 double_clicked: None,
116 context_menu: None,
117 renamed: None,
118 drag_started: None,
119 dragging_nodes: Vec::new(),
120 drop_event: None,
121 }
122 }
123
124 /// Returns whether any outliner state changed this frame.
125 ///
126 /// This includes selection changes, expansion/collapse, renaming, etc.
127 ///
128 /// # Examples
129 ///
130 /// ```ignore
131 /// if response.changed() {
132 /// save_state(&state);
133 /// }
134 /// ```
135 #[inline]
136 pub fn changed(&self) -> bool {
137 self.changed
138 }
139
140 /// Returns the ID of the node that was newly selected this frame, if any.
141 ///
142 /// # Examples
143 ///
144 /// ```ignore
145 /// if let Some(id) = response.selected() {
146 /// println!("Selected node: {:?}", id);
147 /// }
148 /// ```
149 #[inline]
150 pub fn selected(&self) -> Option<&Id> {
151 self.selected.as_ref()
152 }
153
154 /// Returns the ID of the node that was double-clicked this frame, if any.
155 ///
156 /// # Examples
157 ///
158 /// ```ignore
159 /// if let Some(id) = response.double_clicked() {
160 /// open_node(id);
161 /// }
162 /// ```
163 #[inline]
164 pub fn double_clicked(&self) -> Option<&Id> {
165 self.double_clicked.as_ref()
166 }
167
168 /// Returns the ID of the node for which a context menu was requested, if any.
169 ///
170 /// # Examples
171 ///
172 /// ```ignore
173 /// if let Some(id) = response.context_menu() {
174 /// show_context_menu(ui, id);
175 /// }
176 /// ```
177 #[inline]
178 pub fn context_menu(&self) -> Option<&Id> {
179 self.context_menu.as_ref()
180 }
181
182 /// Returns the ID and new name of a node that was renamed this frame, if any.
183 ///
184 /// # Examples
185 ///
186 /// ```ignore
187 /// if let Some((id, new_name)) = response.renamed() {
188 /// update_node_name(id, new_name);
189 /// }
190 /// ```
191 #[inline]
192 pub fn renamed(&self) -> Option<(&Id, &str)> {
193 self.renamed.as_ref().map(|(id, name)| (id, name.as_str()))
194 }
195
196 /// Returns the ID of the node where a drag operation started, if any.
197 ///
198 /// # Examples
199 ///
200 /// ```ignore
201 /// if let Some(id) = response.drag_started() {
202 /// begin_drag_operation(id);
203 /// }
204 /// ```
205 #[inline]
206 pub fn drag_started(&self) -> Option<&Id> {
207 self.drag_started.as_ref()
208 }
209
210 /// Returns the IDs of all nodes being dragged (primary + selected nodes).
211 ///
212 /// # Examples
213 ///
214 /// ```ignore
215 /// if !response.dragging_nodes().is_empty() {
216 /// for id in response.dragging_nodes() {
217 /// highlight_dragging_node(id);
218 /// }
219 /// }
220 /// ```
221 #[inline]
222 pub fn dragging_nodes(&self) -> &[Id] {
223 &self.dragging_nodes
224 }
225
226 /// Returns details of a drop event that occurred this frame, if any.
227 ///
228 /// # Examples
229 ///
230 /// ```ignore
231 /// if let Some(drop_event) = response.drop_event() {
232 /// move_node(drop_event.source, drop_event.target, drop_event.position);
233 /// }
234 /// ```
235 #[inline]
236 pub fn drop_event(&self) -> Option<&DropEvent<Id>> {
237 self.drop_event.as_ref()
238 }
239}
240
241impl<Id> Deref for OutlinerResponse<Id>
242where
243 Id: Hash + Eq + Clone,
244{
245 type Target = egui::Response;
246
247 /// Dereferences to the underlying [`egui::Response`].
248 ///
249 /// This allows convenient access to standard response methods like
250 /// `hovered()`, `clicked()`, `rect`, etc.
251 ///
252 /// # Examples
253 ///
254 /// ```ignore
255 /// let response = outliner.show(ui, &mut state);
256 ///
257 /// // Access egui::Response methods directly
258 /// if response.hovered() {
259 /// ui.ctx().set_cursor_icon(egui::CursorIcon::PointingHand);
260 /// }
261 /// ```
262 fn deref(&self) -> &Self::Target {
263 &self.inner
264 }
265}
266
267/// Details of a drag-and-drop event in the outliner.
268///
269/// This struct contains information about a completed drop operation,
270/// including the source node that was dragged, the target node it was
271/// dropped onto, and the position relative to the target.
272///
273/// # Generic Parameters
274///
275/// * `Id` - The type used to identify nodes in the outliner. Must implement
276/// [`Hash`], [`Eq`], and [`Clone`].
277///
278/// # Examples
279///
280/// ```ignore
281/// if let Some(drop_event) = response.drop_event() {
282/// match drop_event.position {
283/// DropPosition::Before => {
284/// insert_before(drop_event.source, drop_event.target);
285/// }
286/// DropPosition::After => {
287/// insert_after(drop_event.source, drop_event.target);
288/// }
289/// DropPosition::Inside => {
290/// make_child_of(drop_event.source, drop_event.target);
291/// }
292/// }
293/// }
294/// ```
295#[derive(Debug, Clone, PartialEq, Eq)]
296pub struct DropEvent<Id>
297where
298 Id: Hash + Eq + Clone,
299{
300 /// The ID of the node that was dragged.
301 pub source: Id,
302
303 /// The ID of the node that the source was dropped onto.
304 pub target: Id,
305
306 /// The position where the source should be placed relative to the target.
307 pub position: DropPosition,
308}
309
310impl<Id> DropEvent<Id>
311where
312 Id: Hash + Eq + Clone,
313{
314 /// Creates a new drop event.
315 ///
316 /// # Arguments
317 ///
318 /// * `source` - The ID of the node that was dragged
319 /// * `target` - The ID of the node that was dropped onto
320 /// * `position` - Where to place the source relative to the target
321 ///
322 /// # Examples
323 ///
324 /// ```ignore
325 /// let drop_event = DropEvent::new(
326 /// dragged_id,
327 /// target_id,
328 /// DropPosition::Inside,
329 /// );
330 /// ```
331 pub fn new(source: Id, target: Id, position: DropPosition) -> Self {
332 Self {
333 source,
334 target,
335 position,
336 }
337 }
338}
339
340#[cfg(test)]
341mod tests {
342 use super::*;
343 use crate::traits::DropPosition;
344
345 // Since we can't easily construct egui::Response in tests, we'll test
346 // the OutlinerResponse fields and methods directly
347
348 #[test]
349 fn test_drop_event_new() {
350 let event = DropEvent::new(10, 20, DropPosition::Before);
351
352 assert_eq!(event.source, 10);
353 assert_eq!(event.target, 20);
354 assert_eq!(event.position, DropPosition::Before);
355 }
356
357 #[test]
358 fn test_drop_event_positions() {
359 let event_before = DropEvent::new(1, 2, DropPosition::Before);
360 let event_after = DropEvent::new(1, 2, DropPosition::After);
361 let event_inside = DropEvent::new(1, 2, DropPosition::Inside);
362
363 assert_eq!(event_before.position, DropPosition::Before);
364 assert_eq!(event_after.position, DropPosition::After);
365 assert_eq!(event_inside.position, DropPosition::Inside);
366 }
367
368 #[test]
369 fn test_drop_event_equality() {
370 let event1 = DropEvent::new(1, 2, DropPosition::Inside);
371 let event2 = DropEvent::new(1, 2, DropPosition::Inside);
372 let event3 = DropEvent::new(1, 2, DropPosition::Before);
373 let event4 = DropEvent::new(2, 3, DropPosition::Inside);
374
375 assert_eq!(event1, event2);
376 assert_ne!(event1, event3);
377 assert_ne!(event1, event4);
378 }
379
380 #[test]
381 fn test_drop_event_clone() {
382 let event = DropEvent::new(5, 10, DropPosition::After);
383 let cloned = event.clone();
384
385 assert_eq!(event, cloned);
386 assert_eq!(cloned.source, 5);
387 assert_eq!(cloned.target, 10);
388 assert_eq!(cloned.position, DropPosition::After);
389 }
390
391 #[test]
392 fn test_drop_event_with_different_id_types() {
393 let event_u64 = DropEvent::new(1u64, 2u64, DropPosition::Inside);
394 assert_eq!(event_u64.source, 1u64);
395
396 let event_string = DropEvent::new("node1".to_string(), "node2".to_string(), DropPosition::Before);
397 assert_eq!(event_string.source, "node1".to_string());
398 assert_eq!(event_string.target, "node2".to_string());
399 }
400
401}