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}