Skip to main content

egui_material3/
treeview.rs

1use crate::theme::get_global_color;
2use crate::material_symbol::material_symbol_text;
3use egui::{
4    Response, Sense, Ui, Vec2, Widget,
5};
6use std::collections::HashMap;
7
8/// A tree view item that can contain child items
9#[derive(Clone, Debug)]
10pub struct TreeViewItem {
11    /// Unique identifier for this item
12    pub id: String,
13    /// Display label for the item
14    pub label: String,
15    /// Optional icon (Material Symbol name)
16    pub icon: Option<String>,
17    /// Child items
18    pub children: Vec<TreeViewItem>,
19    /// Whether this item is selectable
20    pub selectable: bool,
21    /// Whether this item can toggle (show/hide children)
22    pub toggleable: bool,
23}
24
25impl TreeViewItem {
26    /// Create a new tree view item
27    pub fn new(id: impl Into<String>, label: impl Into<String>) -> Self {
28        Self {
29            id: id.into(),
30            label: label.into(),
31            icon: None,
32            children: Vec::new(),
33            selectable: true,
34            toggleable: true,
35        }
36    }
37
38    /// Set the icon for this item
39    pub fn icon(mut self, icon: impl Into<String>) -> Self {
40        self.icon = Some(icon.into());
41        self
42    }
43
44    /// Add a child item
45    pub fn child(mut self, child: TreeViewItem) -> Self {
46        self.children.push(child);
47        self
48    }
49
50    /// Add multiple children
51    pub fn children(mut self, children: Vec<TreeViewItem>) -> Self {
52        self.children = children;
53        self
54    }
55
56    /// Set whether this item is selectable
57    pub fn selectable(mut self, selectable: bool) -> Self {
58        self.selectable = selectable;
59        self
60    }
61
62    /// Set whether this item is toggleable
63    pub fn toggleable(mut self, toggleable: bool) -> Self {
64        self.toggleable = toggleable;
65        self
66    }
67
68    /// Check if this item has children
69    pub fn has_children(&self) -> bool {
70        !self.children.is_empty()
71    }
72}
73
74/// State management for tree view
75#[derive(Clone, Debug, Default)]
76pub struct TreeViewState {
77    /// Map of item ID to whether it's expanded
78    pub expanded: HashMap<String, bool>,
79    /// Map of item ID to whether it's selected
80    pub selected: HashMap<String, bool>,
81}
82
83impl TreeViewState {
84    /// Create a new tree view state
85    pub fn new() -> Self {
86        Self::default()
87    }
88
89    /// Check if an item is expanded
90    pub fn is_expanded(&self, id: &str) -> bool {
91        self.expanded.get(id).copied().unwrap_or(false)
92    }
93
94    /// Toggle the expanded state of an item
95    pub fn toggle_expanded(&mut self, id: &str) {
96        let current = self.is_expanded(id);
97        self.expanded.insert(id.to_string(), !current);
98    }
99
100    /// Set the expanded state of an item
101    pub fn set_expanded(&mut self, id: &str, expanded: bool) {
102        self.expanded.insert(id.to_string(), expanded);
103    }
104
105    /// Check if an item is selected
106    pub fn is_selected(&self, id: &str) -> bool {
107        self.selected.get(id).copied().unwrap_or(false)
108    }
109
110    /// Toggle the selected state of an item
111    pub fn toggle_selected(&mut self, id: &str) {
112        let current = self.is_selected(id);
113        self.selected.insert(id.to_string(), !current);
114    }
115
116    /// Set the selected state of an item
117    pub fn set_selected(&mut self, id: &str, selected: bool) {
118        self.selected.insert(id.to_string(), selected);
119    }
120
121    /// Clear all selections
122    pub fn clear_selections(&mut self) {
123        self.selected.clear();
124    }
125
126    /// Expand all items
127    pub fn expand_all(&mut self, items: &[TreeViewItem]) {
128        fn expand_recursive(state: &mut TreeViewState, items: &[TreeViewItem]) {
129            for item in items {
130                if item.has_children() {
131                    state.set_expanded(&item.id, true);
132                    expand_recursive(state, &item.children);
133                }
134            }
135        }
136        expand_recursive(self, items);
137    }
138
139    /// Collapse all items
140    pub fn collapse_all(&mut self) {
141        self.expanded.clear();
142    }
143}
144
145/// Material Design tree view component
146///
147/// A hierarchical tree view component that supports expand/collapse,
148/// selection, and icons following Material Design guidelines.
149///
150/// # Example
151/// ```rust
152/// # egui::__run_test_ui(|ui| {
153/// use egui_material3::{TreeViewItem, TreeViewState, MaterialTreeView};
154///
155/// let mut state = TreeViewState::new();
156/// let items = vec![
157///     TreeViewItem::new("1", "Root Item")
158///         .icon("folder")
159///         .child(TreeViewItem::new("1.1", "Child 1"))
160///         .child(TreeViewItem::new("1.2", "Child 2")),
161/// ];
162///
163/// ui.add(MaterialTreeView::new(&items, &mut state));
164/// # });
165/// ```
166#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
167pub struct MaterialTreeView<'a> {
168    items: &'a [TreeViewItem],
169    state: &'a mut TreeViewState,
170    indent_width: f32,
171    item_height: f32,
172}
173
174impl<'a> MaterialTreeView<'a> {
175    /// Create a new tree view
176    pub fn new(items: &'a [TreeViewItem], state: &'a mut TreeViewState) -> Self {
177        Self {
178            items,
179            state,
180            indent_width: 24.0,
181            item_height: 40.0,
182        }
183    }
184
185    /// Set the indent width for child items
186    pub fn indent_width(mut self, width: f32) -> Self {
187        self.indent_width = width;
188        self
189    }
190
191    /// Set the height of each item
192    pub fn item_height(mut self, height: f32) -> Self {
193        self.item_height = height;
194        self
195    }
196
197    /// Render a single tree item and its children
198    fn render_item(
199        &mut self,
200        ui: &mut Ui,
201        item: &TreeViewItem,
202        depth: usize,
203    ) -> Response {
204        let indent = depth as f32 * self.indent_width;
205        let is_expanded = self.state.is_expanded(&item.id);
206        let is_selected = self.state.is_selected(&item.id);
207
208        // Get Material Design colors
209        let on_surface = get_global_color("onSurface");
210        let on_surface_variant = get_global_color("onSurfaceVariant");
211        let _surface_variant = get_global_color("surfaceVariant");
212        let primary = get_global_color("primary");
213
214        // Calculate item width
215        let _available_width = ui.available_width();
216
217        ui.horizontal(|ui| {
218            ui.add_space(indent);
219
220            // Toggle button (chevron icon) if item has children
221            if item.has_children() && item.toggleable {
222                let chevron_icon = if is_expanded {
223                    material_symbol_text("expand_more")
224                } else {
225                    material_symbol_text("chevron_right")
226                };
227
228                let chevron_button = egui::Button::new(chevron_icon)
229                    .frame(false)
230                    .min_size(Vec2::new(24.0, 24.0));
231
232                if ui.add(chevron_button).clicked() {
233                    self.state.toggle_expanded(&item.id);
234                }
235            } else {
236                // Empty space for alignment
237                ui.add_space(24.0);
238            }
239
240            // Icon if present
241            if let Some(icon_name) = &item.icon {
242                let icon_text = material_symbol_text(icon_name);
243                ui.label(egui::RichText::new(icon_text).size(20.0).color(on_surface_variant));
244                ui.add_space(8.0);
245            }
246
247            // Label
248            let label_color = if is_selected { primary } else { on_surface };
249            let label_response = ui.selectable_label(is_selected,
250                egui::RichText::new(&item.label).color(label_color));
251
252            if label_response.clicked() && item.selectable {
253                self.state.toggle_selected(&item.id);
254            }
255        });
256
257        // Render children if expanded
258        let mut child_response = ui.allocate_response(Vec2::ZERO, Sense::hover());
259        if is_expanded && item.has_children() {
260            for child in &item.children {
261                let response = self.render_item(ui, child, depth + 1);
262                child_response = child_response.union(response);
263            }
264        }
265
266        child_response
267    }
268}
269
270impl<'a> Widget for MaterialTreeView<'a> {
271    fn ui(mut self, ui: &mut Ui) -> Response {
272        let mut response = ui.allocate_response(Vec2::ZERO, Sense::hover());
273
274        for item in self.items {
275            let item_response = self.render_item(ui, item, 0);
276            response = response.union(item_response);
277        }
278
279        response
280    }
281}
282
283/// Convenience function to create a tree view
284pub fn tree_view<'a>(items: &'a [TreeViewItem], state: &'a mut TreeViewState) -> MaterialTreeView<'a> {
285    MaterialTreeView::new(items, state)
286}