egui_material3/
treeview.rs1use 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#[derive(Clone, Debug)]
10pub struct TreeViewItem {
11 pub id: String,
13 pub label: String,
15 pub icon: Option<String>,
17 pub children: Vec<TreeViewItem>,
19 pub selectable: bool,
21 pub toggleable: bool,
23}
24
25impl TreeViewItem {
26 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 pub fn icon(mut self, icon: impl Into<String>) -> Self {
40 self.icon = Some(icon.into());
41 self
42 }
43
44 pub fn child(mut self, child: TreeViewItem) -> Self {
46 self.children.push(child);
47 self
48 }
49
50 pub fn children(mut self, children: Vec<TreeViewItem>) -> Self {
52 self.children = children;
53 self
54 }
55
56 pub fn selectable(mut self, selectable: bool) -> Self {
58 self.selectable = selectable;
59 self
60 }
61
62 pub fn toggleable(mut self, toggleable: bool) -> Self {
64 self.toggleable = toggleable;
65 self
66 }
67
68 pub fn has_children(&self) -> bool {
70 !self.children.is_empty()
71 }
72}
73
74#[derive(Clone, Debug, Default)]
76pub struct TreeViewState {
77 pub expanded: HashMap<String, bool>,
79 pub selected: HashMap<String, bool>,
81}
82
83impl TreeViewState {
84 pub fn new() -> Self {
86 Self::default()
87 }
88
89 pub fn is_expanded(&self, id: &str) -> bool {
91 self.expanded.get(id).copied().unwrap_or(false)
92 }
93
94 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 pub fn set_expanded(&mut self, id: &str, expanded: bool) {
102 self.expanded.insert(id.to_string(), expanded);
103 }
104
105 pub fn is_selected(&self, id: &str) -> bool {
107 self.selected.get(id).copied().unwrap_or(false)
108 }
109
110 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 pub fn set_selected(&mut self, id: &str, selected: bool) {
118 self.selected.insert(id.to_string(), selected);
119 }
120
121 pub fn clear_selections(&mut self) {
123 self.selected.clear();
124 }
125
126 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 pub fn collapse_all(&mut self) {
141 self.expanded.clear();
142 }
143}
144
145#[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 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 pub fn indent_width(mut self, width: f32) -> Self {
187 self.indent_width = width;
188 self
189 }
190
191 pub fn item_height(mut self, height: f32) -> Self {
193 self.item_height = height;
194 self
195 }
196
197 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 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 let _available_width = ui.available_width();
216
217 ui.horizontal(|ui| {
218 ui.add_space(indent);
219
220 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 ui.add_space(24.0);
238 }
239
240 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 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 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
283pub fn tree_view<'a>(items: &'a [TreeViewItem], state: &'a mut TreeViewState) -> MaterialTreeView<'a> {
285 MaterialTreeView::new(items, state)
286}