Skip to main content

sequoia_view/
tree.rs

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}