egui_arbor/
state.rs

1//! State management for the outliner widget.
2//!
3//! This module provides the [`OutlinerState`] struct which tracks the expansion
4//! and editing state of nodes in the outliner. The state integrates with egui's
5//! memory system to persist across frames.
6
7use crate::drag_drop::DragDropState;
8use std::collections::HashSet;
9use std::hash::Hash;
10
11/// State for box selection operations.
12///
13/// Tracks the start position and whether a box selection is currently active.
14#[derive(Clone, Debug, PartialEq)]
15pub struct BoxSelectionState {
16    /// The starting position of the box selection in screen coordinates.
17    pub start_pos: egui::Pos2,
18    /// Whether the box selection is currently active.
19    pub active: bool,
20}
21
22impl BoxSelectionState {
23    /// Creates a new box selection state.
24    pub fn new(start_pos: egui::Pos2) -> Self {
25        Self {
26            start_pos,
27            active: true,
28        }
29    }
30}
31
32/// State for an outliner widget instance.
33///
34/// This struct tracks which collection nodes are expanded and which node (if any)
35/// is currently being edited. The state is generic over the node ID type and
36/// integrates with egui's memory system for automatic persistence.
37///
38/// # Type Parameters
39///
40/// * `Id` - The type used to identify nodes. Must implement `Hash`, `Eq`, and `Clone`.
41///
42/// # Examples
43///
44/// ```
45/// use egui_arbor::OutlinerState;
46/// use std::collections::HashSet;
47///
48/// let mut state = OutlinerState::<String>::default();
49/// 
50/// // Toggle expansion state
51/// state.toggle_expanded(&"node1".to_string());
52/// assert!(state.is_expanded(&"node1".to_string()));
53///
54/// // Start editing a node
55/// state.start_editing("node2".to_string(), "Node 2".to_string());
56/// assert!(state.is_editing(&"node2".to_string()));
57/// ```
58#[derive(Clone, Debug)]
59#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
60pub struct OutlinerState<Id>
61where
62    Id: Hash + Eq + Clone + Send + Sync,
63{
64    /// Set of expanded collection node IDs.
65    ///
66    /// A node ID in this set indicates that the collection node is expanded
67    /// and its children should be visible.
68    expanded: HashSet<Id>,
69
70    /// The ID of the node currently being edited, if any.
71    ///
72    /// When `Some(id)`, the node with the given ID is in edit mode (e.g., for renaming).
73    /// Only one node can be edited at a time.
74    editing: Option<Id>,
75
76    /// The text being edited for the current node.
77    ///
78    /// This stores the text content while editing is in progress.
79    /// This field is not persisted across frames (it's transient state).
80    #[cfg_attr(feature = "serde", serde(skip))]
81    editing_text: String,
82
83    /// Drag-and-drop state for this outliner.
84    ///
85    /// Tracks the current drag operation, hover targets, and drop positions.
86    /// This field is not persisted across frames (it's transient state).
87    #[cfg_attr(feature = "serde", serde(skip))]
88    drag_drop: DragDropState<Id>,
89
90    /// The ID of the last selected node for shift-click range selection.
91    ///
92    /// This is used to determine the range when shift-clicking.
93    /// This field is not persisted across frames (it's transient state).
94    #[cfg_attr(feature = "serde", serde(skip))]
95    last_selected: Option<Id>,
96
97    /// State for box selection.
98    ///
99    /// Tracks the start position and current state of a box selection operation.
100    /// This field is not persisted across frames (it's transient state).
101    #[cfg_attr(feature = "serde", serde(skip))]
102    box_selection: Option<BoxSelectionState>,
103
104    /// IDs of all nodes being dragged in a multi-drag operation.
105    ///
106    /// This is set when a drag starts and includes all selected nodes.
107    /// This field is not persisted across frames (it's transient state).
108    #[cfg_attr(feature = "serde", serde(skip))]
109    dragging_nodes: Vec<Id>,
110}
111
112impl<Id> Default for OutlinerState<Id>
113where
114    Id: Hash + Eq + Clone + Send + Sync,
115{
116    /// Creates a new outliner state with no expanded nodes and no editing node.
117    fn default() -> Self {
118        Self {
119            expanded: HashSet::new(),
120            editing: None,
121            editing_text: String::new(),
122            drag_drop: DragDropState::new(),
123            last_selected: None,
124            box_selection: None,
125            dragging_nodes: Vec::new(),
126        }
127    }
128}
129
130impl<Id> OutlinerState<Id>
131where
132    Id: Hash + Eq + Clone + Send + Sync,
133{
134    /// Loads the outliner state from egui's memory system.
135    ///
136    /// If no state exists for the given ID, returns a default empty state.
137    ///
138    /// # Parameters
139    ///
140    /// * `ctx` - The egui context to load state from
141    /// * `id` - The unique identifier for this outliner instance
142    ///
143    /// # Examples
144    ///
145    /// ```no_run
146    /// # use egui_arbor::OutlinerState;
147    /// # fn example(ctx: &egui::Context) {
148    /// let state = OutlinerState::<String>::load(ctx, egui::Id::new("my_outliner"));
149    /// # }
150    /// ```
151    pub fn load(ctx: &egui::Context, id: egui::Id) -> Self
152    where
153        Id: 'static,
154    {
155        ctx.data_mut(|d| d.get_persisted(id).unwrap_or_default())
156    }
157
158    /// Stores the outliner state to egui's memory system.
159    ///
160    /// The state will be persisted across frames and can be retrieved using
161    /// [`load`](Self::load) with the same ID.
162    ///
163    /// # Parameters
164    ///
165    /// * `ctx` - The egui context to store state in
166    /// * `id` - The unique identifier for this outliner instance
167    ///
168    /// # Examples
169    ///
170    /// ```no_run
171    /// # use egui_arbor::OutlinerState;
172    /// # fn example(ctx: &egui::Context) {
173    /// let mut state = OutlinerState::<String>::default();
174    /// state.toggle_expanded(&"node1".to_string());
175    /// state.store(ctx, egui::Id::new("my_outliner"));
176    /// # }
177    /// ```
178    pub fn store(&self, ctx: &egui::Context, id: egui::Id)
179    where
180        Id: 'static,
181    {
182        ctx.data_mut(|d| d.insert_persisted(id, self.clone()));
183    }
184
185    /// Checks if a node is currently expanded.
186    ///
187    /// # Parameters
188    ///
189    /// * `id` - The ID of the node to check
190    ///
191    /// # Returns
192    ///
193    /// `true` if the node is expanded, `false` otherwise.
194    ///
195    /// # Examples
196    ///
197    /// ```
198    /// # use egui_arbor::OutlinerState;
199    /// let mut state = OutlinerState::<String>::default();
200    /// state.set_expanded(&"node1".to_string(), true);
201    /// assert!(state.is_expanded(&"node1".to_string()));
202    /// ```
203    pub fn is_expanded(&self, id: &Id) -> bool {
204        self.expanded.contains(id)
205    }
206
207    /// Toggles the expansion state of a node.
208    ///
209    /// If the node is currently expanded, it will be collapsed.
210    /// If the node is currently collapsed, it will be expanded.
211    ///
212    /// # Parameters
213    ///
214    /// * `id` - The ID of the node to toggle
215    ///
216    /// # Examples
217    ///
218    /// ```
219    /// # use egui_arbor::OutlinerState;
220    /// let mut state = OutlinerState::<String>::default();
221    /// state.toggle_expanded(&"node1".to_string());
222    /// assert!(state.is_expanded(&"node1".to_string()));
223    /// state.toggle_expanded(&"node1".to_string());
224    /// assert!(!state.is_expanded(&"node1".to_string()));
225    /// ```
226    pub fn toggle_expanded(&mut self, id: &Id) {
227        if self.expanded.contains(id) {
228            self.expanded.remove(id);
229        } else {
230            self.expanded.insert(id.clone());
231        }
232    }
233
234    /// Sets the expansion state of a node.
235    ///
236    /// # Parameters
237    ///
238    /// * `id` - The ID of the node to modify
239    /// * `expanded` - `true` to expand the node, `false` to collapse it
240    ///
241    /// # Examples
242    ///
243    /// ```
244    /// # use egui_arbor::OutlinerState;
245    /// let mut state = OutlinerState::<String>::default();
246    /// state.set_expanded(&"node1".to_string(), true);
247    /// assert!(state.is_expanded(&"node1".to_string()));
248    /// state.set_expanded(&"node1".to_string(), false);
249    /// assert!(!state.is_expanded(&"node1".to_string()));
250    /// ```
251    pub fn set_expanded(&mut self, id: &Id, expanded: bool) {
252        if expanded {
253            self.expanded.insert(id.clone());
254        } else {
255            self.expanded.remove(id);
256        }
257    }
258
259    /// Checks if a node is currently being edited.
260    ///
261    /// # Parameters
262    ///
263    /// * `id` - The ID of the node to check
264    ///
265    /// # Returns
266    ///
267    /// `true` if the node is being edited, `false` otherwise.
268    ///
269    /// # Examples
270    ///
271    /// ```
272    /// # use egui_arbor::OutlinerState;
273    /// let mut state = OutlinerState::<String>::default();
274    /// state.start_editing("node1".to_string(), "Node 1".to_string());
275    /// assert!(state.is_editing(&"node1".to_string()));
276    /// ```
277    pub fn is_editing(&self, id: &Id) -> bool {
278        self.editing.as_ref() == Some(id)
279    }
280
281    /// Starts editing a node.
282    ///
283    /// This will stop editing any previously edited node, as only one node
284    /// can be edited at a time.
285    ///
286    /// # Parameters
287    ///
288    /// * `id` - The ID of the node to start editing
289    /// * `initial_text` - The initial text to display in the edit field
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// # use egui_arbor::OutlinerState;
295    /// let mut state = OutlinerState::<String>::default();
296    /// state.start_editing("node1".to_string(), "Initial Name".to_string());
297    /// assert!(state.is_editing(&"node1".to_string()));
298    /// ```
299    pub fn start_editing(&mut self, id: Id, initial_text: String) {
300        self.editing = Some(id);
301        self.editing_text = initial_text;
302    }
303
304    /// Stops editing the currently edited node, if any.
305    ///
306    /// # Examples
307    ///
308    /// ```
309    /// # use egui_arbor::OutlinerState;
310    /// let mut state = OutlinerState::<String>::default();
311    /// state.start_editing("node1".to_string(), "Name".to_string());
312    /// state.stop_editing();
313    /// assert!(!state.is_editing(&"node1".to_string()));
314    /// ```
315    pub fn stop_editing(&mut self) {
316        self.editing = None;
317        self.editing_text.clear();
318    }
319
320    /// Returns a mutable reference to the editing text.
321    ///
322    /// This allows the text edit widget to modify the text directly.
323    pub fn editing_text_mut(&mut self) -> &mut String {
324        &mut self.editing_text
325    }
326
327    /// Returns a reference to the editing text.
328    pub fn editing_text(&self) -> &str {
329        &self.editing_text
330    }
331
332    /// Returns a reference to the drag-drop state.
333    ///
334    /// # Examples
335    ///
336    /// ```
337    /// # use egui_arbor::OutlinerState;
338    /// let state = OutlinerState::<String>::default();
339    /// assert!(!state.drag_drop().is_dragging());
340    /// ```
341    pub fn drag_drop(&self) -> &DragDropState<Id> {
342        &self.drag_drop
343    }
344
345    /// Returns a mutable reference to the drag-drop state.
346    ///
347    /// # Examples
348    ///
349    /// ```
350    /// # use egui_arbor::OutlinerState;
351    /// let mut state = OutlinerState::<String>::default();
352    /// state.drag_drop_mut().start_drag("node1".to_string());
353    /// assert!(state.drag_drop().is_dragging());
354    /// ```
355    pub fn drag_drop_mut(&mut self) -> &mut DragDropState<Id> {
356        &mut self.drag_drop
357    }
358
359    /// Sets the last selected node for shift-click range selection.
360    ///
361    /// # Parameters
362    ///
363    /// * `id` - The ID of the last selected node
364    pub fn set_last_selected(&mut self, id: Option<Id>) {
365        self.last_selected = id;
366    }
367
368    /// Returns the ID of the last selected node, if any.
369    pub fn last_selected(&self) -> Option<&Id> {
370        self.last_selected.as_ref()
371    }
372
373    /// Starts a box selection operation.
374    ///
375    /// # Parameters
376    ///
377    /// * `start_pos` - The starting position in screen coordinates
378    pub fn start_box_selection(&mut self, start_pos: egui::Pos2) {
379        self.box_selection = Some(BoxSelectionState {
380            start_pos,
381            active: true,
382        });
383    }
384
385    /// Returns the current box selection state, if any.
386    pub fn box_selection(&self) -> Option<&BoxSelectionState> {
387        self.box_selection.as_ref()
388    }
389
390    /// Ends the current box selection operation.
391    pub fn end_box_selection(&mut self) {
392        self.box_selection = None;
393    }
394
395    /// Sets the nodes being dragged in a multi-drag operation.
396    pub fn set_dragging_nodes(&mut self, nodes: Vec<Id>) {
397        self.dragging_nodes = nodes;
398    }
399
400    /// Returns the nodes being dragged in a multi-drag operation.
401    pub fn dragging_nodes(&self) -> &[Id] {
402        &self.dragging_nodes
403    }
404
405    /// Clears the dragging nodes list.
406    pub fn clear_dragging_nodes(&mut self) {
407        self.dragging_nodes.clear();
408    }
409}
410
411#[cfg(test)]
412mod tests {
413    use super::*;
414    use crate::traits::DropPosition;
415
416    #[test]
417    fn test_default_state() {
418        let state = OutlinerState::<String>::default();
419        assert!(!state.is_expanded(&"test".to_string()));
420        assert!(!state.is_editing(&"test".to_string()));
421        assert!(!state.drag_drop().is_dragging());
422        assert_eq!(state.last_selected(), None);
423        assert_eq!(state.box_selection(), None);
424        assert!(state.dragging_nodes().is_empty());
425    }
426
427    #[test]
428    fn test_expansion() {
429        let mut state = OutlinerState::<String>::default();
430        let id = "node1".to_string();
431
432        assert!(!state.is_expanded(&id));
433
434        state.set_expanded(&id, true);
435        assert!(state.is_expanded(&id));
436
437        state.set_expanded(&id, false);
438        assert!(!state.is_expanded(&id));
439    }
440
441    #[test]
442    fn test_toggle_expansion() {
443        let mut state = OutlinerState::<String>::default();
444        let id = "node1".to_string();
445
446        state.toggle_expanded(&id);
447        assert!(state.is_expanded(&id));
448
449        state.toggle_expanded(&id);
450        assert!(!state.is_expanded(&id));
451    }
452
453    #[test]
454    fn test_multiple_expansions() {
455        let mut state = OutlinerState::<String>::default();
456        let id1 = "node1".to_string();
457        let id2 = "node2".to_string();
458        let id3 = "node3".to_string();
459
460        state.set_expanded(&id1, true);
461        state.set_expanded(&id2, true);
462        state.set_expanded(&id3, true);
463
464        assert!(state.is_expanded(&id1));
465        assert!(state.is_expanded(&id2));
466        assert!(state.is_expanded(&id3));
467
468        state.set_expanded(&id2, false);
469        assert!(state.is_expanded(&id1));
470        assert!(!state.is_expanded(&id2));
471        assert!(state.is_expanded(&id3));
472    }
473
474    #[test]
475    fn test_editing() {
476        let mut state = OutlinerState::<String>::default();
477        let id1 = "node1".to_string();
478        let id2 = "node2".to_string();
479
480        assert!(!state.is_editing(&id1));
481
482        state.start_editing(id1.clone(), "Node 1".to_string());
483        assert!(state.is_editing(&id1));
484        assert!(!state.is_editing(&id2));
485        assert_eq!(state.editing_text(), "Node 1");
486
487        state.start_editing(id2.clone(), "Node 2".to_string());
488        assert!(!state.is_editing(&id1));
489        assert!(state.is_editing(&id2));
490        assert_eq!(state.editing_text(), "Node 2");
491
492        state.stop_editing();
493        assert!(!state.is_editing(&id1));
494        assert!(!state.is_editing(&id2));
495        assert_eq!(state.editing_text(), "");
496    }
497
498    #[test]
499    fn test_editing_same_node_twice() {
500        let mut state = OutlinerState::<String>::default();
501        let id = "node1".to_string();
502
503        state.start_editing(id.clone(), "First".to_string());
504        assert!(state.is_editing(&id));
505        assert_eq!(state.editing_text(), "First");
506
507        state.start_editing(id.clone(), "Second".to_string());
508        assert!(state.is_editing(&id));
509        assert_eq!(state.editing_text(), "Second");
510    }
511
512    #[test]
513    fn test_drag_drop_state_access() {
514        let mut state = OutlinerState::<u64>::default();
515        
516        assert!(!state.drag_drop().is_dragging());
517        
518        state.drag_drop_mut().start_drag(42);
519        assert!(state.drag_drop().is_dragging());
520        assert_eq!(state.drag_drop().dragging_id(), Some(&42));
521    }
522
523    #[test]
524    fn test_last_selected() {
525        let mut state = OutlinerState::<u64>::default();
526        
527        assert_eq!(state.last_selected(), None);
528        
529        state.set_last_selected(Some(1));
530        assert_eq!(state.last_selected(), Some(&1));
531        
532        state.set_last_selected(Some(2));
533        assert_eq!(state.last_selected(), Some(&2));
534        
535        state.set_last_selected(None);
536        assert_eq!(state.last_selected(), None);
537    }
538
539    #[test]
540    fn test_box_selection_lifecycle() {
541        let mut state = OutlinerState::<u64>::default();
542        
543        assert_eq!(state.box_selection(), None);
544        
545        let start_pos = egui::pos2(10.0, 20.0);
546        state.start_box_selection(start_pos);
547        
548        let box_sel = state.box_selection();
549        assert!(box_sel.is_some());
550        assert_eq!(box_sel.unwrap().start_pos, start_pos);
551        assert!(box_sel.unwrap().active);
552        
553        state.end_box_selection();
554        assert_eq!(state.box_selection(), None);
555    }
556
557    #[test]
558    fn test_box_selection_state_new() {
559        let pos = egui::pos2(5.0, 10.0);
560        let box_sel = BoxSelectionState::new(pos);
561        
562        assert_eq!(box_sel.start_pos, pos);
563        assert!(box_sel.active);
564    }
565
566    #[test]
567    fn test_dragging_nodes() {
568        let mut state = OutlinerState::<u64>::default();
569        
570        assert!(state.dragging_nodes().is_empty());
571        
572        let nodes = vec![1, 2, 3];
573        state.set_dragging_nodes(nodes.clone());
574        assert_eq!(state.dragging_nodes(), &[1, 2, 3]);
575        
576        state.clear_dragging_nodes();
577        assert!(state.dragging_nodes().is_empty());
578    }
579
580    #[test]
581    fn test_dragging_nodes_update() {
582        let mut state = OutlinerState::<u64>::default();
583        
584        state.set_dragging_nodes(vec![1, 2]);
585        assert_eq!(state.dragging_nodes().len(), 2);
586        
587        state.set_dragging_nodes(vec![3, 4, 5]);
588        assert_eq!(state.dragging_nodes().len(), 3);
589        assert_eq!(state.dragging_nodes(), &[3, 4, 5]);
590    }
591
592    #[test]
593    fn test_combined_state_operations() {
594        let mut state = OutlinerState::<u64>::default();
595        
596        // Expand some nodes
597        state.set_expanded(&1, true);
598        state.set_expanded(&2, true);
599        
600        // Start editing
601        state.start_editing(3, "Node 3".to_string());
602        
603        // Set last selected
604        state.set_last_selected(Some(4));
605        
606        // Start drag
607        state.drag_drop_mut().start_drag(5);
608        
609        // Verify all states are maintained
610        assert!(state.is_expanded(&1));
611        assert!(state.is_expanded(&2));
612        assert!(state.is_editing(&3));
613        assert_eq!(state.last_selected(), Some(&4));
614        assert!(state.drag_drop().is_dragging_node(&5));
615    }
616
617    #[test]
618    fn test_expansion_persistence() {
619        let mut state = OutlinerState::<u64>::default();
620        
621        // Expand multiple nodes
622        for i in 1..=10 {
623            state.set_expanded(&i, true);
624        }
625        
626        // Verify all are expanded
627        for i in 1..=10 {
628            assert!(state.is_expanded(&i));
629        }
630        
631        // Collapse some
632        state.set_expanded(&3, false);
633        state.set_expanded(&7, false);
634        
635        // Verify correct state
636        assert!(state.is_expanded(&1));
637        assert!(state.is_expanded(&2));
638        assert!(!state.is_expanded(&3));
639        assert!(state.is_expanded(&4));
640        assert!(!state.is_expanded(&7));
641        assert!(state.is_expanded(&10));
642    }
643
644    #[test]
645    fn test_drag_drop_integration() {
646        let mut state = OutlinerState::<u64>::default();
647        
648        // Start drag
649        state.drag_drop_mut().start_drag(1);
650        state.set_dragging_nodes(vec![1, 2, 3]);
651        
652        // Update hover
653        state.drag_drop_mut().update_hover(4, DropPosition::Inside);
654        
655        // Verify state
656        assert!(state.drag_drop().is_dragging());
657        assert_eq!(state.dragging_nodes(), &[1, 2, 3]);
658        assert!(state.drag_drop().is_hover_target(&4));
659        
660        // End drag
661        let result = state.drag_drop_mut().end_drag();
662        assert!(result.is_some());
663        
664        // Clear dragging nodes
665        state.clear_dragging_nodes();
666        assert!(state.dragging_nodes().is_empty());
667    }
668
669    #[test]
670    fn test_state_isolation() {
671        let mut state1 = OutlinerState::<u64>::default();
672        let mut state2 = OutlinerState::<u64>::default();
673        
674        state1.set_expanded(&1, true);
675        state2.set_expanded(&2, true);
676        
677        assert!(state1.is_expanded(&1));
678        assert!(!state1.is_expanded(&2));
679        assert!(!state2.is_expanded(&1));
680        assert!(state2.is_expanded(&2));
681    }
682}