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