Skip to main content

egui_charts/ui/widget_bar/panels/object_tree/
state.rs

1//! State management for the Object Tree panel
2//!
3//! Tracks selection, expansion, filtering, and other UI state.
4
5use std::collections::{BTreeSet, HashSet};
6
7use super::types::SourceType;
8
9/// State for the Object Tree panel
10#[derive(Clone, Debug, Default)]
11pub struct ObjectTreeState {
12    /// Known folder paths, including empty folders. Hosts may save and restore this set
13    /// per chart; nonempty folders are also recovered from source rows on each frame.
14    pub folders: BTreeSet<Vec<String>>,
15    /// Collapsed folder paths. Search temporarily expands matching descendants.
16    pub collapsed_folders: BTreeSet<Vec<String>>,
17    /// Parent of the folder being created by the name prompt.
18    pub creating_folder: Option<Vec<String>>,
19
20    // === Selection ===
21    /// Drawing and source ids in the rendered order, used for range selection.
22    pub visible_ids: Vec<usize>,
23    /// Currently selected item IDs
24    pub selected_ids: HashSet<usize>,
25    /// Last selected ID (for shift-click range selection)
26    pub last_selected_id: Option<usize>,
27    /// Anchor ID for range selection
28    pub selection_anchor: Option<usize>,
29
30    // === Expansion ===
31    /// Expanded group headers
32    pub expanded_groups: HashSet<String>,
33    /// Expanded item IDs (showing properties)
34    pub expanded_items: HashSet<usize>,
35    /// Data Window section expanded
36    pub data_window_expanded: bool,
37
38    // === Filtering ===
39    /// Filter text (search query)
40    pub filter_text: String,
41    /// Filter by source type
42    pub filter_type: Option<SourceType>,
43
44    // === Drag & Drop ===
45    /// Currently dragging item ID
46    pub dragging_id: Option<usize>,
47    /// Drag start position
48    pub drag_start_pos: Option<egui::Pos2>,
49    /// Drop target position (index)
50    pub drop_target: Option<usize>,
51
52    // === Rename ===
53    /// Currently renaming item ID
54    pub renaming_id: Option<usize>,
55    /// Rename text buffer
56    pub rename_text: String,
57
58    // === Hover ===
59    /// Currently hovered item ID
60    pub hovered_id: Option<usize>,
61}
62
63impl ObjectTreeState {
64    /// Create a new state with default expansions
65    pub fn new() -> Self {
66        let mut state = Self::default();
67        // Expand all groups by default
68        state.expanded_groups.insert("Indicators".to_string());
69        state.expanded_groups.insert("Drawings".to_string());
70        state.expanded_groups.insert("Templates".to_string());
71        state.data_window_expanded = true;
72        state
73    }
74
75    // === Selection Methods ===
76
77    /// Select a single item (clears other selections)
78    pub fn select(&mut self, id: usize) {
79        self.selected_ids.clear();
80        self.selected_ids.insert(id);
81        self.last_selected_id = Some(id);
82        self.selection_anchor = Some(id);
83    }
84
85    /// Add item to selection (toggle if already selected)
86    pub fn toggle_selection(&mut self, id: usize) {
87        if self.selected_ids.contains(&id) {
88            self.selected_ids.remove(&id);
89        } else {
90            self.selected_ids.insert(id);
91        }
92        self.last_selected_id = Some(id);
93    }
94
95    /// Range select from anchor to id (given a list of ordered IDs)
96    pub fn range_select(&mut self, id: usize, ordered_ids: &[usize]) {
97        if let Some(anchor) = self.selection_anchor {
98            let anchor_idx = ordered_ids.iter().position(|&x| x == anchor);
99            let target_idx = ordered_ids.iter().position(|&x| x == id);
100
101            if let (Some(start), Some(end)) = (anchor_idx, target_idx) {
102                let (min, max) = if start < end {
103                    (start, end)
104                } else {
105                    (end, start)
106                };
107
108                self.selected_ids.clear();
109                for &item_id in &ordered_ids[min..=max] {
110                    self.selected_ids.insert(item_id);
111                }
112            }
113        } else {
114            self.select(id);
115        }
116        self.last_selected_id = Some(id);
117    }
118
119    /// Select all items
120    pub fn select_all(&mut self, all_ids: &[usize]) {
121        self.selected_ids = all_ids.iter().copied().collect();
122    }
123
124    /// Clear all selections
125    pub fn clear_selection(&mut self) {
126        self.selected_ids.clear();
127        self.last_selected_id = None;
128    }
129
130    /// Check if item is selected
131    pub fn is_selected(&self, id: usize) -> bool {
132        self.selected_ids.contains(&id)
133    }
134
135    /// Get number of selected items
136    pub fn selection_count(&self) -> usize {
137        self.selected_ids.len()
138    }
139
140    // === Expansion Methods ===
141
142    /// Toggle group expansion
143    pub fn toggle_group(&mut self, group: &str) {
144        if self.expanded_groups.contains(group) {
145            self.expanded_groups.remove(group);
146        } else {
147            self.expanded_groups.insert(group.to_string());
148        }
149    }
150
151    /// Check if group is expanded
152    pub fn is_group_expanded(&self, group: &str) -> bool {
153        self.expanded_groups.contains(group)
154    }
155
156    /// Toggle item expansion (properties)
157    pub fn toggle_item(&mut self, id: usize) {
158        if self.expanded_items.contains(&id) {
159            self.expanded_items.remove(&id);
160        } else {
161            self.expanded_items.insert(id);
162        }
163    }
164
165    /// Check if item is expanded
166    pub fn is_item_expanded(&self, id: usize) -> bool {
167        self.expanded_items.contains(&id)
168    }
169
170    /// Expand all groups
171    pub fn expand_all_groups(&mut self) {
172        self.collapsed_folders.clear();
173        self.expanded_groups.insert("Indicators".to_string());
174        self.expanded_groups.insert("Drawings".to_string());
175        self.expanded_groups.insert("Templates".to_string());
176    }
177
178    /// Collapse all groups
179    pub fn collapse_all_groups(&mut self) {
180        self.expanded_groups.clear();
181        self.collapsed_folders = self.folders.clone();
182    }
183
184    // === Filter Methods ===
185
186    /// Set filter text
187    pub fn set_filter(&mut self, text: impl Into<String>) {
188        self.filter_text = text.into();
189    }
190
191    /// Clear filter
192    pub fn clear_filter(&mut self) {
193        self.filter_text.clear();
194        self.filter_type = None;
195    }
196
197    /// Check if filter is active
198    pub fn has_filter(&self) -> bool {
199        !self.filter_text.is_empty() || self.filter_type.is_some()
200    }
201
202    // === Rename Methods ===
203
204    /// Start renaming an item
205    pub fn start_rename(&mut self, id: usize, current_name: &str) {
206        self.renaming_id = Some(id);
207        self.rename_text = current_name.to_string();
208    }
209
210    /// Cancel rename
211    pub fn cancel_rename(&mut self) {
212        self.renaming_id = None;
213        self.rename_text.clear();
214    }
215
216    /// Finish rename and return new name if changed
217    pub fn finish_rename(&mut self) -> Option<(usize, String)> {
218        if let Some(id) = self.renaming_id.take() {
219            let name = std::mem::take(&mut self.rename_text);
220            if !name.is_empty() {
221                return Some((id, name));
222            }
223        }
224        None
225    }
226
227    // === Drag & Drop Methods ===
228
229    /// Start dragging an item
230    pub fn start_drag(&mut self, id: usize, pos: egui::Pos2) {
231        self.dragging_id = Some(id);
232        self.drag_start_pos = Some(pos);
233    }
234
235    /// Update drop target
236    pub fn set_drop_target(&mut self, target: Option<usize>) {
237        self.drop_target = target;
238    }
239
240    /// End drag operation
241    pub fn end_drag(&mut self) -> Option<(usize, usize)> {
242        let result = match (self.dragging_id, self.drop_target) {
243            (Some(src), Some(dst)) if src != dst => Some((src, dst)),
244            _ => None,
245        };
246        self.dragging_id = None;
247        self.drag_start_pos = None;
248        self.drop_target = None;
249        result
250    }
251
252    /// Check if currently dragging
253    pub fn is_dragging(&self) -> bool {
254        self.dragging_id.is_some()
255    }
256
257    /// Sync selected drawing from an external selection.
258    ///
259    /// If `selected_drawing` is `Some`, ensures it is the sole selection.
260    pub fn sync_selected_drawing(&mut self, selected_drawing: Option<usize>) {
261        if let Some(drawing_id) = selected_drawing
262            && !self.selected_ids.contains(&drawing_id)
263        {
264            self.selected_ids.clear();
265            self.selected_ids.insert(drawing_id);
266            self.last_selected_id = Some(drawing_id);
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    #[test]
276    fn test_selection() {
277        let mut state = ObjectTreeState::new();
278
279        state.select(1);
280        assert!(state.is_selected(1));
281        assert_eq!(state.selection_count(), 1);
282
283        state.toggle_selection(2);
284        assert!(state.is_selected(1));
285        assert!(state.is_selected(2));
286        assert_eq!(state.selection_count(), 2);
287
288        state.toggle_selection(1);
289        assert!(!state.is_selected(1));
290        assert!(state.is_selected(2));
291    }
292
293    #[test]
294    fn test_range_select() {
295        let mut state = ObjectTreeState::new();
296        let ids = vec![1, 2, 3, 4, 5];
297
298        state.select(2);
299        state.range_select(4, &ids);
300
301        assert!(state.is_selected(2));
302        assert!(state.is_selected(3));
303        assert!(state.is_selected(4));
304        assert!(!state.is_selected(1));
305        assert!(!state.is_selected(5));
306    }
307}