1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use sequoia_core::Length;
6use sequoia_layout::LayoutStyle;
7use sequoia_reactive::Signal;
8use sequoia_style::TreeStyle;
9
10use crate::any::AnyView;
11use crate::scroll_view::ScrollbarVisibility;
12use crate::view::{EventCtx, View};
13use crate::views::{Row, Spacer, Text, Unit};
14use crate::virtual_list::VirtualList;
15
16use sequoia_input::{Event, PointerButton, PointerEvent};
17
18#[derive(Clone)]
19pub struct TreeNode<T> {
20 pub id: String,
21 pub value: T,
22 pub children: Vec<TreeNode<T>>,
23 pub expanded: Signal<bool>,
24}
25
26impl<T: Clone> TreeNode<T> {
27 pub fn leaf(id: impl Into<String>, value: T) -> Self {
28 Self {
29 id: id.into(),
30 value,
31 children: Vec::new(),
32 expanded: Signal::new(false),
33 }
34 }
35
36 pub fn branch(id: impl Into<String>, value: T, children: Vec<TreeNode<T>>) -> Self {
37 Self {
38 id: id.into(),
39 value,
40 children,
41 expanded: Signal::new(true),
42 }
43 }
44
45 pub fn expanded(self, expanded: bool) -> Self {
46 self.expanded.set(expanded);
47 self
48 }
49}
50
51#[derive(Clone)]
52pub struct FlatRow<T> {
53 pub id: String,
54 pub depth: usize,
55 pub has_children: bool,
56 pub expanded: Signal<bool>,
57 pub value: T,
58}
59
60pub struct TreeView<T> {
61 roots: Rc<dyn Fn() -> Vec<TreeNode<T>>>,
62 row: Rc<dyn Fn(&T, usize) -> AnyView>,
63 row_height: f32,
64 indent: f32,
65 overscan: usize,
66 scrollbar: ScrollbarVisibility,
67 style: Option<TreeStyle>,
68 label: String,
69 expanded: Rc<RefCell<HashMap<String, Signal<bool>>>>,
70}
71
72impl<T: Clone + 'static> Clone for TreeView<T> {
73 fn clone(&self) -> Self {
74 Self {
75 roots: self.roots.clone(),
76 row: self.row.clone(),
77 row_height: self.row_height,
78 indent: self.indent,
79 overscan: self.overscan,
80 scrollbar: self.scrollbar,
81 style: self.style,
82 label: self.label.clone(),
83 expanded: Rc::clone(&self.expanded),
84 }
85 }
86}
87
88impl<T: Clone + 'static> TreeView<T> {
89 pub fn new(roots: impl Fn() -> Vec<TreeNode<T>> + 'static) -> Self {
90 Self {
91 roots: Rc::new(roots),
92 row: Rc::new(|_, _| AnyView::new(Unit)),
93 row_height: 28.0,
94 indent: 16.0,
95 overscan: 4,
96 scrollbar: ScrollbarVisibility::Auto,
97 style: None,
98 label: String::new(),
99 expanded: Rc::new(RefCell::new(HashMap::new())),
100 }
101 }
102
103 pub fn label(mut self, label: impl Into<String>) -> Self {
104 self.label = label.into();
105 self
106 }
107
108 pub fn style(mut self, style: TreeStyle) -> Self {
109 self.style = Some(style);
110 self
111 }
112
113 fn font_size(&self) -> f32 {
114 self.style.and_then(|s| s.font_size).unwrap_or(12.0)
115 }
116
117 fn arrow_color(&self) -> Option<sequoia_core::Color> {
118 self.style.and_then(|s| s.arrow_color)
119 }
120
121 pub fn row<V: View + 'static>(mut self, f: impl Fn(&T, usize) -> V + 'static) -> Self {
122 self.row = Rc::new(move |v, d| AnyView::new(f(v, d)));
123 self
124 }
125
126 pub fn row_height(mut self, h: f32) -> Self {
127 self.row_height = h.max(1.0);
128 self
129 }
130
131 pub fn indent(mut self, px: f32) -> Self {
132 self.indent = px.max(0.0);
133 self
134 }
135
136 pub fn overscan(mut self, n: usize) -> Self {
137 self.overscan = n;
138 self
139 }
140
141 pub fn flatten(&self) -> Vec<FlatRow<T>> {
142 fn walk<T: Clone>(
143 nodes: &[TreeNode<T>],
144 depth: usize,
145 expanded: &mut HashMap<String, Signal<bool>>,
146 out: &mut Vec<FlatRow<T>>,
147 ) {
148 for n in nodes {
149 let signal = expanded
150 .entry(n.id.clone())
151 .or_insert_with(|| n.expanded.clone())
152 .clone();
153 let is_open = signal.get_untracked();
154 out.push(FlatRow {
155 id: n.id.clone(),
156 depth,
157 has_children: !n.children.is_empty(),
158 expanded: signal,
159 value: n.value.clone(),
160 });
161 if !n.children.is_empty() && is_open {
162 walk(&n.children, depth + 1, expanded, out);
163 }
164 }
165 }
166
167 let roots = (self.roots)();
168 let mut expanded = self.expanded.borrow_mut();
169 let mut out = Vec::new();
170 walk(&roots, 0, &mut expanded, &mut out);
171 for r in &out {
172 let _ = r.expanded.get();
173 }
174 out
175 }
176}
177
178#[derive(Clone)]
179struct ArrowBtn {
180 expanded: Signal<bool>,
181 has_children: bool,
182 size: f32,
183 color: Option<sequoia_core::Color>,
184}
185
186impl View for ArrowBtn {
187 fn body(&self) -> impl View + 'static {
188 Unit
189 }
190
191 fn children_override(&self) -> Option<Vec<AnyView>> {
192 let glyph = if !self.has_children {
193 " "
194 } else if self.expanded.get() {
195 "▾ "
196 } else {
197 "▸ "
198 };
199 let mut txt = Text::new(glyph).size(self.size);
200 if let Some(color) = self.color {
201 txt = txt.color(color);
202 }
203 Some(vec![AnyView::new(txt)])
204 }
205
206 fn layout_style(&self) -> LayoutStyle {
207 LayoutStyle::default()
208 }
209
210 fn on_event(&self, event: &Event, ctx: &mut EventCtx) {
211 match event {
212 Event::Pointer(PointerEvent::Up {
213 button: PointerButton::Left,
214 pos,
215 ..
216 }) if ctx.rect.contains(*pos) && self.has_children => {
217 self.expanded.update(|e| *e = !*e);
218 ctx.handled();
219 }
220 _ => {}
221 }
222 }
223}
224
225impl<T: Clone + 'static> View for TreeView<T> {
226 fn body(&self) -> impl View + 'static {
227 Unit
228 }
229
230 fn accessibility(&self) -> Option<crate::a11y::Accessible> {
231 Some(crate::a11y::Accessible::new(
232 crate::a11y::AccessibleRole::Tree,
233 self.label.clone(),
234 ))
235 }
236
237 fn children_override(&self) -> Option<Vec<AnyView>> {
238 let me = self.clone();
239 let indent = self.indent;
240 let row_fn2 = self.row.clone();
241 let font_size = self.font_size();
242 let arrow_color = self.arrow_color();
243 let list = VirtualList::new(move || me.flatten())
244 .key(|r: &FlatRow<T>| {
245 let mut h = 0u64;
246 for b in r.id.bytes() {
247 h = h.wrapping_mul(31).wrapping_add(b as u64);
248 }
249 h
250 })
251 .item(move |r: &FlatRow<T>| {
252 let pad = (r.depth as f32) * indent;
253 Row::new()
254 .gap(2.0)
255 .child(Spacer::fixed_width(pad))
256 .child(ArrowBtn {
257 expanded: r.expanded.clone(),
258 has_children: r.has_children,
259 size: font_size,
260 color: arrow_color,
261 })
262 .child_flex(row_fn2(&r.value, r.depth), 1.0)
263 })
264 .item_height(self.row_height)
265 .overscan(self.overscan)
266 .scrollbar_visibility(self.scrollbar);
267 Some(vec![AnyView::new(list)])
268 }
269
270 fn layout_style(&self) -> LayoutStyle {
271 LayoutStyle {
272 width: Length::Fill(1.0),
273 height: Length::Fill(1.0),
274 ..LayoutStyle::default()
275 }
276 }
277}