Skip to main content

vizia_core/views/
virtual_tree_view.rs

1use std::{
2    collections::{HashMap, HashSet},
3    hash::Hash,
4    ops::Deref,
5    rc::Rc,
6};
7
8use crate::{
9    icons::{ICON_CHEVRON_DOWN, ICON_CHEVRON_RIGHT},
10    prelude::*,
11};
12
13use super::tree_table::{TreeNodeRow, TreeTableRow};
14
15fn flatten_hierarchy_rows<U, Id>(
16    tree: &U,
17    root_ids: &dyn Fn(&U) -> Vec<Id>,
18    child_ids: &dyn Fn(&U, &Id) -> Vec<Id>,
19    is_visible: &dyn Fn(&U, &Id) -> bool,
20) -> Vec<TreeNodeRow<Id>>
21where
22    Id: Clone + Eq + Hash + 'static,
23{
24    fn visit<U, Id>(
25        tree: &U,
26        node_id: Id,
27        parent_id: Option<Id>,
28        child_ids: &dyn Fn(&U, &Id) -> Vec<Id>,
29        is_visible: &dyn Fn(&U, &Id) -> bool,
30        out: &mut Vec<TreeNodeRow<Id>>,
31    ) where
32        Id: Clone + Eq + Hash + 'static,
33    {
34        if !is_visible(tree, &node_id) {
35            return;
36        }
37
38        out.push(TreeNodeRow { id: node_id.clone(), parent_id });
39
40        for child_id in child_ids(tree, &node_id) {
41            visit(tree, child_id, Some(node_id.clone()), child_ids, is_visible, out);
42        }
43    }
44
45    let mut rows = Vec::new();
46    for root_id in root_ids(tree) {
47        visit(tree, root_id, None, child_ids, is_visible, &mut rows);
48    }
49
50    rows
51}
52
53fn flatten_visible_rows<T, V, Id>(
54    rows: &V,
55    row_id: &dyn Fn(&T) -> Id,
56    parent_id: &dyn Fn(&T) -> Option<Id>,
57    expanded_row_ids: &[Id],
58) -> Vec<TreeTableRow<T, Id>>
59where
60    V: Deref<Target = [T]>,
61    T: PartialEq + Clone + 'static,
62    Id: Clone + Eq + Hash + 'static,
63{
64    let mut rows_by_parent: HashMap<Option<Id>, Vec<T>> = HashMap::new();
65    for row in rows.deref().iter().cloned() {
66        rows_by_parent.entry(parent_id(&row)).or_default().push(row);
67    }
68
69    let expanded_set: HashSet<Id> = expanded_row_ids.iter().cloned().collect();
70    let mut visible_rows = Vec::new();
71
72    fn visit<T, Id>(
73        rows: &[T],
74        depth: usize,
75        rows_by_parent: &HashMap<Option<Id>, Vec<T>>,
76        expanded_set: &HashSet<Id>,
77        row_id: &dyn Fn(&T) -> Id,
78        out: &mut Vec<TreeTableRow<T, Id>>,
79    ) where
80        T: PartialEq + Clone + 'static,
81        Id: Clone + Eq + Hash + 'static,
82    {
83        for row in rows {
84            let id = row_id(row);
85            let child_rows =
86                rows_by_parent.get(&Some(id.clone())).map(Vec::as_slice).unwrap_or(&[]);
87            let has_children = !child_rows.is_empty();
88            let expanded = has_children && expanded_set.contains(&id);
89
90            out.push(TreeTableRow {
91                row: row.clone(),
92                id: id.clone(),
93                parent_id: None,
94                depth,
95                has_children,
96                expanded,
97            });
98
99            if expanded {
100                visit(child_rows, depth + 1, rows_by_parent, expanded_set, row_id, out);
101            }
102        }
103    }
104
105    let roots = rows_by_parent.get(&None).map(Vec::as_slice).unwrap_or(&[]);
106    visit(roots, 0, &rows_by_parent, &expanded_set, row_id, &mut visible_rows);
107
108    for visible_row in &mut visible_rows {
109        visible_row.parent_id = parent_id(&visible_row.row);
110    }
111
112    visible_rows
113}
114
115fn focused_visible_index<T, Id>(
116    visible_rows: &[TreeTableRow<T, Id>],
117    focused_row_id: Option<&Id>,
118    selected_row_ids: &[Id],
119) -> Option<usize>
120where
121    T: PartialEq + Clone + 'static,
122    Id: Clone + Eq + Hash + 'static,
123{
124    if let Some(focused_row_id) = focused_row_id {
125        if let Some(index) = visible_rows.iter().position(|row| &row.id == focused_row_id) {
126            return Some(index);
127        }
128    }
129
130    visible_rows
131        .iter()
132        .position(|row| selected_row_ids.iter().any(|selected_id| selected_id == &row.id))
133}
134
135enum VirtualTreeViewEvent<Id> {
136    SelectRow(usize),
137    FocusRow(Id),
138    SelectFocused,
139    ToggleCheckedFocused,
140    ExpandFocused,
141    CollapseFocused,
142    ToggleRow(Id, bool),
143}
144
145type VirtualTreeViewItemContent<T, Id> = dyn Fn(&mut Context, Memo<TreeTableRow<T, Id>>);
146type VirtualTreeViewTypeAheadText<T> = dyn Fn(&T) -> Option<String>;
147type VirtualTreeViewCheckedRow<T> = dyn Fn(&T) -> bool;
148
149pub struct VirtualTreeView<T, V, Id>
150where
151    V: Deref<Target = [T]> + Clone + 'static,
152    T: PartialEq + Clone + 'static,
153    Id: Eq + Hash + Clone + Send + Sync + 'static,
154{
155    rows: Signal<V>,
156    row_id: Rc<dyn Fn(&T) -> Id>,
157    parent_id: Rc<dyn Fn(&T) -> Option<Id>>,
158    selectable: Signal<Selectable>,
159    selection_follows_focus: Signal<bool>,
160    selected_row_ids: Signal<Vec<Id>>,
161    expanded_row_ids: Signal<Vec<Id>>,
162    focused_row_id: Signal<Option<Id>>,
163    list_entity: Signal<Entity>,
164    checked_row: Signal<Option<Rc<VirtualTreeViewCheckedRow<T>>>>,
165    type_ahead_text: Signal<Option<Rc<VirtualTreeViewTypeAheadText<T>>>>,
166    on_row_select: Option<Box<dyn Fn(&mut EventContext, Id)>>,
167    on_row_focus: Option<Box<dyn Fn(&mut EventContext, Id)>>,
168    on_row_check_toggle: Option<Box<dyn Fn(&mut EventContext, Id)>>,
169    on_row_toggle: Option<Box<dyn Fn(&mut EventContext, Id, bool)>>,
170}
171
172impl<T, V, Id> VirtualTreeView<T, V, Id>
173where
174    V: Deref<Target = [T]> + Clone + 'static,
175    T: PartialEq + Clone + 'static,
176    Id: Eq + Hash + Clone + Send + Sync + 'static,
177{
178    pub fn new<S, U, F>(
179        cx: &mut Context,
180        tree: S,
181        item_height: f32,
182        flatten_rows: F,
183        row_id: impl Fn(&T) -> Id + 'static,
184        parent_id: impl Fn(&T) -> Option<Id> + 'static,
185        item_content: impl Fn(&mut Context, Memo<TreeTableRow<T, Id>>) + 'static,
186    ) -> Handle<Self>
187    where
188        S: Res<U> + 'static,
189        U: Clone + 'static,
190        F: Fn(&U) -> V + 'static,
191    {
192        let tree_signal = tree.to_signal(cx);
193        let flatten_rows: Rc<dyn Fn(&U) -> V> = Rc::new(flatten_rows);
194        let row_signal = Signal::new(tree_signal.with(|tree| flatten_rows(tree)));
195        let row_id: Rc<dyn Fn(&T) -> Id> = Rc::new(row_id);
196        let parent_id: Rc<dyn Fn(&T) -> Option<Id>> = Rc::new(parent_id);
197        let item_content: Rc<VirtualTreeViewItemContent<T, Id>> = Rc::new(item_content);
198        let item_content_signal = Signal::new(item_content.clone());
199
200        let selectable = Signal::new(Selectable::None);
201        let selection_follows_focus = Signal::new(false);
202        let selected_row_ids = Signal::new(Vec::new());
203        let expanded_row_ids = Signal::new(Vec::new());
204        let focused_row_id = Signal::new(None);
205        let list_entity = Signal::new(Entity::null());
206        let checked_row = Signal::new(None);
207        let type_ahead_text = Signal::new(None);
208
209        let visible_rows = Memo::new({
210            let row_id = row_id.clone();
211            let parent_id = parent_id.clone();
212            move |_| {
213                row_signal.with(|rows| {
214                    expanded_row_ids.with(|expanded| {
215                        flatten_visible_rows(rows, &*row_id, &*parent_id, expanded)
216                    })
217                })
218            }
219        });
220
221        let selected_indices =
222            Memo::new(move |_| {
223                visible_rows.with(|rows| {
224                    selected_row_ids.with(|selected_ids| {
225                        rows.iter()
226                            .enumerate()
227                            .filter_map(|(index, row)| {
228                                if selected_ids.contains(&row.id) { Some(index) } else { None }
229                            })
230                            .collect::<Vec<usize>>()
231                    })
232                })
233            });
234
235        let focused_index = Memo::new(move |_| {
236            visible_rows.with(|rows| {
237                let focused_row_id = focused_row_id.get();
238                selected_row_ids.with(|selected_ids| {
239                    focused_visible_index(rows, focused_row_id.as_ref(), selected_ids)
240                })
241            })
242        });
243
244        let handle = Self {
245            rows: row_signal,
246            row_id,
247            parent_id,
248            selectable,
249            selection_follows_focus,
250            selected_row_ids,
251            expanded_row_ids,
252            focused_row_id,
253            list_entity,
254            checked_row,
255            type_ahead_text,
256            on_row_select: None,
257            on_row_focus: None,
258            on_row_check_toggle: None,
259            on_row_toggle: None,
260        }
261        .build(cx, move |cx| {
262            Keymap::from(vec![
263                (
264                    KeyChord::new(Modifiers::empty(), Code::Enter),
265                    KeymapEntry::new("Select Focused", |cx| {
266                        cx.emit(VirtualTreeViewEvent::<Id>::SelectFocused)
267                    }),
268                ),
269                (
270                    KeyChord::new(Modifiers::empty(), Code::Space),
271                    KeymapEntry::new("Toggle Checked Focused", |cx| {
272                        cx.emit(VirtualTreeViewEvent::<Id>::ToggleCheckedFocused)
273                    }),
274                ),
275                (
276                    KeyChord::new(Modifiers::empty(), Code::ArrowRight),
277                    KeymapEntry::new("Expand Focused", |cx| {
278                        cx.emit(VirtualTreeViewEvent::<Id>::ExpandFocused)
279                    }),
280                ),
281                (
282                    KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
283                    KeymapEntry::new("Collapse Focused", |cx| {
284                        cx.emit(VirtualTreeViewEvent::<Id>::CollapseFocused)
285                    }),
286                ),
287            ])
288            .build(cx);
289
290            let list = VirtualList::new_custom_items_with_selection(
291                cx,
292                visible_rows,
293                item_height,
294                move |cx, row_index, row, is_selected| {
295                    let row: Memo<TreeTableRow<T, Id>> = Memo::new(move |_| row.get());
296                    let row_data = row.get();
297                    let has_children = row_data.has_children;
298                    let expanded = row_data.expanded;
299                    let row_id = row_data.id.clone();
300                    let checked_row_callback = checked_row.get();
301                    let indent = row_data.depth as f32 * 16.0;
302                    let item_content = item_content_signal.get();
303                    let row_for_size_of_set = row;
304                    let size_of_set = Memo::new(move |_| {
305                        let row_data = row_for_size_of_set.get();
306                        visible_rows.with(|rows| {
307                            rows.iter()
308                                .filter(|candidate| candidate.parent_id == row_data.parent_id)
309                                .count()
310                        })
311                    });
312                    let row_for_position = row;
313                    let position_in_set = Memo::new(move |_| {
314                        let row_data = row_for_position.get();
315                        visible_rows.with(|rows| {
316                            rows.iter()
317                                .filter(|candidate| candidate.parent_id == row_data.parent_id)
318                                .position(|candidate| candidate.id == row_data.id)
319                                .map(|position| position + 1)
320                                .unwrap_or(1)
321                        })
322                    });
323
324                    let is_checked = if let Some(checked_row_callback) = checked_row_callback {
325                        row.map(move |value| checked_row_callback(&value.row))
326                    } else {
327                        is_selected
328                    };
329
330                    let row_handle = HStack::new(cx, move |cx| {
331                        Element::new(cx)
332                            .class("tree-view-indent")
333                            .width(Pixels(indent))
334                            .height(Stretch(1.0))
335                            .pointer_events(PointerEvents::None);
336
337                        if has_children {
338                            let icon =
339                                if expanded { ICON_CHEVRON_DOWN } else { ICON_CHEVRON_RIGHT };
340                            Button::new(cx, move |cx| Svg::new(cx, icon).text_wrap(false))
341                                .variant(ButtonVariant::Text)
342                                .class("tree-view-disclosure")
343                                .navigable(false)
344                                .on_press({
345                                    let row_id = row_id.clone();
346                                    move |cx| {
347                                        cx.emit(VirtualTreeViewEvent::ToggleRow(
348                                            row_id.clone(),
349                                            !expanded,
350                                        ));
351                                    }
352                                });
353                        } else {
354                            Element::new(cx)
355                                .class("tree-view-disclosure-placeholder")
356                                .pointer_events(PointerEvents::None);
357                        }
358
359                        HStack::new(cx, move |cx| {
360                            item_content(cx, row);
361                        })
362                        .class("tree-view-row-content")
363                        .width(Stretch(1.0))
364                        .min_width(Auto)
365                        .height(Auto)
366                        .pointer_events(PointerEvents::None);
367                    })
368                    .class("tree-view-row")
369                    .toggle_class("odd", row_index % 2 == 1)
370                    .toggle_class("even", row_index % 2 == 0)
371                    .toggle_class("selected", is_selected)
372                    .toggle_class("expanded", row.map(|value| value.expanded))
373                    .toggle_class("collapsible", row.map(|value| value.has_children))
374                    .alignment(Alignment::Left)
375                    .height(Auto)
376                    .width(Stretch(1.0))
377                    .min_width(Auto)
378                    .role(Role::TreeItem)
379                    .accessibility_selected(is_selected)
380                    .checked(is_checked)
381                    .level(row.map(|value| value.depth + 1))
382                    .size_of_set(size_of_set)
383                    .position_in_set(position_in_set)
384                    .on_mouse_down(move |cx, button| {
385                        if button == MouseButton::Left && cx.hovered() == cx.current() {
386                            cx.emit(ListEvent::Select(row_index));
387                        }
388                    });
389
390                    if has_children {
391                        row_handle.expanded(row.map(|value| value.expanded))
392                    } else {
393                        row_handle
394                    }
395                },
396            )
397            .width(Stretch(1.0))
398            .height(Stretch(1.0))
399            .class("tree-view-body")
400            .selection(selected_indices)
401            .focused_index(focused_index)
402            .on_focus(move |cx, index| {
403                visible_rows.with(|rows| {
404                    if let Some(row) = rows.get(index) {
405                        focused_row_id.set(Some(row.id.clone()));
406                        cx.emit(VirtualTreeViewEvent::FocusRow(row.id.clone()));
407                    }
408                });
409            })
410            .selectable(selectable)
411            .selection_follows_focus(selection_follows_focus)
412            .space_selects_focused(false)
413            .type_ahead_text(move |_cx, index| {
414                let text_fn = type_ahead_text.get()?;
415                visible_rows.with(|rows| rows.get(index).and_then(|row| text_fn(&row.row)))
416            })
417            .on_select(move |cx, index| cx.emit(VirtualTreeViewEvent::<Id>::SelectRow(index)))
418            .role(Role::Tree);
419
420            list_entity.set(list.entity());
421        })
422        .navigable(false);
423
424        let flatten_rows_for_bind = flatten_rows.clone();
425        handle.bind(tree_signal, move |handle| {
426            let rows = tree_signal.with(|tree| flatten_rows_for_bind(tree));
427            handle.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| tree_view.rows.set(rows));
428        })
429    }
430
431    pub fn from_rows<S>(
432        cx: &mut Context,
433        rows: S,
434        item_height: f32,
435        row_id: impl Fn(&T) -> Id + 'static,
436        parent_id: impl Fn(&T) -> Option<Id> + 'static,
437        item_content: impl Fn(&mut Context, Memo<TreeTableRow<T, Id>>) + 'static,
438    ) -> Handle<Self>
439    where
440        S: Res<V> + 'static,
441    {
442        Self::new(cx, rows, item_height, |rows: &V| rows.clone(), row_id, parent_id, item_content)
443    }
444
445    fn emit_toggle(&self, cx: &mut EventContext, row_id: Id, next_expanded: bool) {
446        if let Some(callback) = &self.on_row_toggle {
447            (callback)(cx, row_id, next_expanded);
448        }
449    }
450
451    fn emit_select(&self, cx: &mut EventContext, row_id: Id) {
452        if let Some(callback) = &self.on_row_select {
453            (callback)(cx, row_id);
454        }
455    }
456
457    fn emit_focus(&self, cx: &mut EventContext, row_id: Id) {
458        if let Some(callback) = &self.on_row_focus {
459            (callback)(cx, row_id);
460        }
461    }
462
463    fn emit_check_toggle(&self, cx: &mut EventContext, row_id: Id) {
464        if let Some(callback) = &self.on_row_check_toggle {
465            (callback)(cx, row_id);
466        }
467    }
468
469    fn visible_rows(&self) -> Vec<TreeTableRow<T, Id>> {
470        flatten_visible_rows(
471            &self.rows.get(),
472            &*self.row_id,
473            &*self.parent_id,
474            &self.expanded_row_ids.get(),
475        )
476    }
477
478    fn focused_or_selected_visible_row(&self) -> Option<TreeTableRow<T, Id>> {
479        let visible_rows = self.visible_rows();
480
481        if let Some(focused_id) = self.focused_row_id.get() {
482            if let Some(row) = visible_rows.iter().find(|row| row.id == focused_id) {
483                return Some(row.clone());
484            }
485        }
486
487        let selected_id = self.selected_row_ids.get().first().cloned()?;
488        visible_rows.into_iter().find(|row| row.id == selected_id)
489    }
490
491    fn focus_row_id(&self, cx: &mut EventContext, row_id: Id) {
492        let visible_rows = self.visible_rows();
493        if let Some(index) = visible_rows.iter().position(|row| row.id == row_id) {
494            let list_entity = self.list_entity.get();
495            if list_entity != Entity::null() {
496                cx.emit_to(list_entity, ListEvent::Focus(index));
497                self.focused_row_id.set(Some(row_id));
498            }
499        }
500    }
501}
502
503impl<Id> VirtualTreeView<TreeNodeRow<Id>, Vec<TreeNodeRow<Id>>, Id>
504where
505    Id: Eq + Hash + Clone + Send + Sync + 'static,
506{
507    pub fn from_hierarchy<S, U>(
508        cx: &mut Context,
509        tree: S,
510        item_height: f32,
511        root_ids: impl Fn(&U) -> Vec<Id> + 'static,
512        child_ids: impl Fn(&U, &Id) -> Vec<Id> + 'static,
513        is_visible: impl Fn(&U, &Id) -> bool + 'static,
514        item_content: impl Fn(&mut Context, Memo<TreeTableRow<TreeNodeRow<Id>, Id>>) + 'static,
515    ) -> Handle<Self>
516    where
517        S: Res<U> + 'static,
518        U: Clone + 'static,
519    {
520        let root_ids: Rc<dyn Fn(&U) -> Vec<Id>> = Rc::new(root_ids);
521        let child_ids: Rc<dyn Fn(&U, &Id) -> Vec<Id>> = Rc::new(child_ids);
522        let is_visible: Rc<dyn Fn(&U, &Id) -> bool> = Rc::new(is_visible);
523
524        Self::new(
525            cx,
526            tree,
527            item_height,
528            move |tree: &U| flatten_hierarchy_rows(tree, &*root_ids, &*child_ids, &*is_visible),
529            |row: &TreeNodeRow<Id>| row.id.clone(),
530            |row: &TreeNodeRow<Id>| row.parent_id.clone(),
531            item_content,
532        )
533    }
534}
535
536impl<T, V, Id> View for VirtualTreeView<T, V, Id>
537where
538    V: Deref<Target = [T]> + Clone + 'static,
539    T: PartialEq + Clone + 'static,
540    Id: Eq + Hash + Clone + Send + Sync + 'static,
541{
542    fn element(&self) -> Option<&'static str> {
543        Some("virtual-tree-view")
544    }
545
546    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
547        event.map(|tree_event: &VirtualTreeViewEvent<Id>, _| match tree_event {
548            VirtualTreeViewEvent::SelectRow(index) => {
549                let visible_rows = self.visible_rows();
550
551                if let Some(row) = visible_rows.get(*index) {
552                    self.emit_select(cx, row.id.clone());
553                }
554            }
555
556            VirtualTreeViewEvent::SelectFocused => {
557                if let Some(row) = self.focused_or_selected_visible_row() {
558                    self.emit_select(cx, row.id);
559                }
560            }
561
562            VirtualTreeViewEvent::FocusRow(row_id) => {
563                self.emit_focus(cx, row_id.clone());
564            }
565
566            VirtualTreeViewEvent::ToggleCheckedFocused => {
567                if let Some(row) = self.focused_or_selected_visible_row() {
568                    self.emit_check_toggle(cx, row.id);
569                }
570            }
571
572            VirtualTreeViewEvent::ExpandFocused => {
573                if let Some(row) = self.focused_or_selected_visible_row() {
574                    if row.has_children && !row.expanded {
575                        self.emit_toggle(cx, row.id, true);
576                    } else if row.has_children {
577                        let child_id = self
578                            .visible_rows()
579                            .into_iter()
580                            .find(|candidate| candidate.parent_id.as_ref() == Some(&row.id))
581                            .map(|candidate| candidate.id);
582
583                        if let Some(child_id) = child_id {
584                            self.focus_row_id(cx, child_id);
585                        }
586                    }
587                }
588            }
589
590            VirtualTreeViewEvent::CollapseFocused => {
591                if let Some(row) = self.focused_or_selected_visible_row() {
592                    if row.has_children && row.expanded {
593                        self.emit_toggle(cx, row.id, false);
594                    } else if let Some(parent_id) = row.parent_id {
595                        self.focus_row_id(cx, parent_id);
596                    }
597                }
598            }
599
600            VirtualTreeViewEvent::ToggleRow(row_id, next) => {
601                self.emit_toggle(cx, row_id.clone(), *next);
602            }
603        });
604    }
605}
606
607pub trait VirtualTreeViewModifiers<Id>: Sized
608where
609    Id: Eq + Hash + Clone + Send + Sync + 'static,
610{
611    fn selectable<U: Into<Selectable> + Clone + 'static>(
612        self,
613        selectable: impl Res<U> + 'static,
614    ) -> Self;
615
616    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
617        self,
618        flag: impl Res<U> + 'static,
619    ) -> Self;
620
621    fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
622    where
623        R: Deref<Target = [Id]> + Clone + 'static;
624
625    fn expanded_row_ids<R>(self, expanded_row_ids: impl Res<R> + 'static) -> Self
626    where
627        R: Deref<Target = [Id]> + Clone + 'static;
628
629    fn on_row_select<F>(self, callback: F) -> Self
630    where
631        F: 'static + Fn(&mut EventContext, Id);
632
633    fn on_row_focus<F>(self, callback: F) -> Self
634    where
635        F: 'static + Fn(&mut EventContext, Id);
636
637    fn on_row_check_toggle<F>(self, callback: F) -> Self
638    where
639        F: 'static + Fn(&mut EventContext, Id);
640
641    fn on_row_toggle<F>(self, callback: F) -> Self
642    where
643        F: 'static + Fn(&mut EventContext, Id, bool);
644}
645
646impl<T, V, Id> VirtualTreeViewModifiers<Id> for Handle<'_, VirtualTreeView<T, V, Id>>
647where
648    V: Deref<Target = [T]> + Clone + 'static,
649    T: PartialEq + Clone + 'static,
650    Id: Eq + Hash + Clone + Send + Sync + 'static,
651{
652    fn selectable<U: Into<Selectable> + Clone + 'static>(
653        self,
654        selectable: impl Res<U> + 'static,
655    ) -> Self {
656        let selectable = selectable.to_signal(self.cx);
657        self.bind(selectable, move |handle| {
658            let selectable = selectable.get().into();
659            handle.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
660                tree_view.selectable.set(selectable)
661            });
662        })
663    }
664
665    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
666        self,
667        flag: impl Res<U> + 'static,
668    ) -> Self {
669        let flag = flag.to_signal(self.cx);
670        self.bind(flag, move |handle| {
671            let flag = flag.get().into();
672            handle.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
673                tree_view.selection_follows_focus.set(flag)
674            });
675        })
676    }
677
678    fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
679    where
680        R: Deref<Target = [Id]> + Clone + 'static,
681    {
682        let selected_row_ids = selected_row_ids.to_signal(self.cx);
683        self.bind(selected_row_ids, move |handle| {
684            let ids = selected_row_ids.with(|ids| ids.deref().to_vec());
685            handle.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
686                tree_view.selected_row_ids.set(ids)
687            });
688        })
689    }
690
691    fn expanded_row_ids<R>(self, expanded_row_ids: impl Res<R> + 'static) -> Self
692    where
693        R: Deref<Target = [Id]> + Clone + 'static,
694    {
695        let expanded_row_ids = expanded_row_ids.to_signal(self.cx);
696        self.bind(expanded_row_ids, move |handle| {
697            let ids = expanded_row_ids.with(|ids| ids.deref().to_vec());
698            handle.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
699                tree_view.expanded_row_ids.set(ids)
700            });
701        })
702    }
703
704    fn on_row_select<F>(self, callback: F) -> Self
705    where
706        F: 'static + Fn(&mut EventContext, Id),
707    {
708        self.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
709            tree_view.on_row_select = Some(Box::new(callback))
710        })
711    }
712
713    fn on_row_focus<F>(self, callback: F) -> Self
714    where
715        F: 'static + Fn(&mut EventContext, Id),
716    {
717        self.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
718            tree_view.on_row_focus = Some(Box::new(callback))
719        })
720    }
721
722    fn on_row_check_toggle<F>(self, callback: F) -> Self
723    where
724        F: 'static + Fn(&mut EventContext, Id),
725    {
726        self.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
727            tree_view.on_row_check_toggle = Some(Box::new(callback))
728        })
729    }
730
731    fn on_row_toggle<F>(self, callback: F) -> Self
732    where
733        F: 'static + Fn(&mut EventContext, Id, bool),
734    {
735        self.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
736            tree_view.on_row_toggle = Some(Box::new(callback))
737        })
738    }
739}
740
741impl<T, V, Id> Handle<'_, VirtualTreeView<T, V, Id>>
742where
743    V: Deref<Target = [T]> + Clone + 'static,
744    T: PartialEq + Clone + 'static,
745    Id: Eq + Hash + Clone + Send + Sync + 'static,
746{
747    pub fn type_ahead_text<F>(self, callback: F) -> Self
748    where
749        F: 'static + Fn(&T) -> Option<String>,
750    {
751        let callback: Rc<VirtualTreeViewTypeAheadText<T>> = Rc::new(callback);
752        self.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
753            tree_view.type_ahead_text.set(Some(callback.clone()))
754        })
755    }
756
757    pub fn checked_row<F>(self, callback: F) -> Self
758    where
759        F: 'static + Fn(&T) -> bool,
760    {
761        let callback: Rc<VirtualTreeViewCheckedRow<T>> = Rc::new(callback);
762        self.modify(|tree_view: &mut VirtualTreeView<T, V, Id>| {
763            tree_view.checked_row.set(Some(callback.clone()))
764        })
765    }
766}
767
768#[cfg(test)]
769mod tests {
770    use super::*;
771
772    fn make_row(id: i32) -> TreeTableRow<i32, i32> {
773        TreeTableRow {
774            row: id,
775            id,
776            parent_id: None,
777            depth: 0,
778            has_children: false,
779            expanded: false,
780        }
781    }
782
783    #[test]
784    fn focused_visible_index_prefers_focused_row() {
785        let visible_rows = vec![make_row(1), make_row(2), make_row(3)];
786
787        assert_eq!(focused_visible_index(&visible_rows, Some(&2), &[1, 3]), Some(1));
788    }
789
790    #[test]
791    fn focused_visible_index_falls_back_to_first_visible_selected_row() {
792        let visible_rows = vec![make_row(4), make_row(5), make_row(6)];
793
794        assert_eq!(focused_visible_index(&visible_rows, Some(&9), &[6, 4]), Some(0));
795    }
796
797    #[test]
798    fn focused_visible_index_returns_none_when_nothing_is_visible() {
799        let visible_rows = vec![make_row(7), make_row(8)];
800
801        assert_eq!(focused_visible_index(&visible_rows, None, &[9]), None);
802    }
803}