Skip to main content

vizia_core/views/
virtual_tree_table.rs

1use accesskit::SortDirection as AccessSortDirection;
2use std::{
3    collections::{HashMap, HashSet},
4    hash::Hash,
5    marker::PhantomData,
6    ops::Deref,
7    rc::Rc,
8    sync::Arc,
9};
10
11use crate::prelude::*;
12
13use super::{
14    Cell, TableSelectionMode, TableSortCycle, TableSortDirection, TableSortState, TreeNodeRow,
15    TreeTableColumn, TreeTableEvent, TreeTableFirstCellEvent, TreeTableRow,
16    table::next_sort_direction, table::sort_direction_for_column,
17};
18
19#[derive(Clone, PartialEq)]
20enum TableFocus<Id, K>
21where
22    Id: Eq + Hash + Clone + Send + Sync + 'static,
23    K: Clone + PartialEq + Send + Sync + 'static,
24{
25    Row(Id),
26    Cell(Id, K),
27}
28
29fn flatten_hierarchy_rows<U, Id>(
30    tree: &U,
31    root_ids: &dyn Fn(&U) -> Vec<Id>,
32    child_ids: &dyn Fn(&U, &Id) -> Vec<Id>,
33    is_visible: &dyn Fn(&U, &Id) -> bool,
34) -> Vec<TreeNodeRow<Id>>
35where
36    Id: Clone + Eq + Hash + 'static,
37{
38    fn visit<U, Id>(
39        tree: &U,
40        node_id: Id,
41        parent_id: Option<Id>,
42        child_ids: &dyn Fn(&U, &Id) -> Vec<Id>,
43        is_visible: &dyn Fn(&U, &Id) -> bool,
44        out: &mut Vec<TreeNodeRow<Id>>,
45    ) where
46        Id: Clone + Eq + Hash + 'static,
47    {
48        if !is_visible(tree, &node_id) {
49            return;
50        }
51
52        out.push(TreeNodeRow { id: node_id.clone(), parent_id });
53
54        for child_id in child_ids(tree, &node_id) {
55            visit(tree, child_id, Some(node_id.clone()), child_ids, is_visible, out);
56        }
57    }
58
59    let mut rows = Vec::new();
60    for root_id in root_ids(tree) {
61        visit(tree, root_id, None, child_ids, is_visible, &mut rows);
62    }
63
64    rows
65}
66
67fn flatten_visible_rows<T, V, Id>(
68    rows: &V,
69    row_id: &dyn Fn(&T) -> Id,
70    parent_id: &dyn Fn(&T) -> Option<Id>,
71    expanded_row_ids: &[Id],
72) -> Vec<TreeTableRow<T, Id>>
73where
74    V: Deref<Target = [T]>,
75    T: PartialEq + Clone + 'static,
76    Id: Clone + Eq + Hash + 'static,
77{
78    let mut rows_by_parent: HashMap<Option<Id>, Vec<T>> = HashMap::new();
79    for row in rows.deref().iter().cloned() {
80        rows_by_parent.entry(parent_id(&row)).or_default().push(row);
81    }
82
83    let expanded_set: HashSet<Id> = expanded_row_ids.iter().cloned().collect();
84    let mut visible_rows = Vec::new();
85
86    fn visit<T, Id>(
87        rows: &[T],
88        depth: usize,
89        rows_by_parent: &HashMap<Option<Id>, Vec<T>>,
90        expanded_set: &HashSet<Id>,
91        row_id: &dyn Fn(&T) -> Id,
92        out: &mut Vec<TreeTableRow<T, Id>>,
93    ) where
94        T: PartialEq + Clone + 'static,
95        Id: Clone + Eq + Hash + 'static,
96    {
97        for row in rows {
98            let id = row_id(row);
99            let child_rows =
100                rows_by_parent.get(&Some(id.clone())).map(Vec::as_slice).unwrap_or(&[]);
101            let has_children = !child_rows.is_empty();
102            let expanded = has_children && expanded_set.contains(&id);
103
104            out.push(TreeTableRow {
105                row: row.clone(),
106                id: id.clone(),
107                parent_id: None,
108                depth,
109                has_children,
110                expanded,
111            });
112
113            if expanded {
114                visit(child_rows, depth + 1, rows_by_parent, expanded_set, row_id, out);
115            }
116        }
117    }
118
119    let roots = rows_by_parent.get(&None).map(Vec::as_slice).unwrap_or(&[]);
120    visit(roots, 0, &rows_by_parent, &expanded_set, row_id, &mut visible_rows);
121
122    for visible_row in &mut visible_rows {
123        visible_row.parent_id = parent_id(&visible_row.row);
124    }
125
126    visible_rows
127}
128
129pub struct VirtualTreeTable<T, V, Id, H, K = String>
130where
131    V: Deref<Target = [T]> + Clone + 'static,
132    T: PartialEq + Clone + 'static,
133    Id: Eq + Hash + Clone + Send + Sync + 'static,
134    H: View,
135    K: Eq + Hash + Clone + PartialEq + Send + Sync + 'static,
136{
137    rows: Signal<V>,
138    visible_rows: Memo<Vec<TreeTableRow<T, Id>>>,
139    _header: PhantomData<H>,
140    row_id: Rc<dyn Fn(&T) -> Id>,
141    parent_id: Rc<dyn Fn(&T) -> Option<Id>>,
142    sort_state: Signal<Option<TableSortState<K>>>,
143    sort_cycle: Signal<TableSortCycle>,
144    resizable_columns: Signal<bool>,
145    selectable: Signal<Selectable>,
146    selection_follows_focus: Signal<bool>,
147    selection: Signal<HashSet<Cell<Id, K>>>,
148    selection_mode: Signal<TableSelectionMode>,
149    expanded_row_ids: Signal<Vec<Id>>,
150    focused: Signal<Option<TableFocus<Id, K>>>,
151    treegrid_label: Signal<Option<String>>,
152    on_sort: Option<Arc<dyn Fn(&mut EventContext, K, TableSortDirection) + Send + Sync>>,
153    on_select: Option<Box<dyn Fn(&mut EventContext, HashSet<Cell<Id, K>>) + Send + Sync>>,
154    on_row_toggle: Option<Box<dyn Fn(&mut EventContext, Id, bool)>>,
155    columns: Memo<Vec<K>>,
156}
157
158impl<T, V, Id, H, K> VirtualTreeTable<T, V, Id, H, K>
159where
160    V: Deref<Target = [T]> + Clone + 'static,
161    T: PartialEq + Clone + 'static,
162    Id: Eq + Hash + Clone + Send + Sync + 'static,
163    H: Clone + View,
164    K: Eq + Hash + Clone + PartialEq + Send + Sync + 'static,
165{
166    pub fn new<S, U, C, R, F>(
167        cx: &mut Context,
168        tree: S,
169        columns: C,
170        item_height: f32,
171        flatten_rows: F,
172        row_id: impl Fn(&T) -> Id + 'static,
173        parent_id: impl Fn(&T) -> Option<Id> + 'static,
174    ) -> Handle<Self>
175    where
176        S: Res<U> + 'static,
177        U: Clone + 'static,
178        C: Res<R> + 'static,
179        R: Deref<Target = [TreeTableColumn<T, Id, H, K>]> + Clone + 'static,
180        F: Fn(&U) -> V + 'static,
181    {
182        let tree_signal = tree.to_signal(cx);
183        let flatten_rows: Rc<dyn Fn(&U) -> V> = Rc::new(flatten_rows);
184        let row_signal = Signal::new(tree_signal.with(|tree| flatten_rows(tree)));
185        let column_signal = columns.to_signal(cx);
186        let row_id: Rc<dyn Fn(&T) -> Id> = Rc::new(row_id);
187        let parent_id: Rc<dyn Fn(&T) -> Option<Id>> = Rc::new(parent_id);
188
189        let sort_state = Signal::new(None);
190        let sort_cycle = Signal::new(TableSortCycle::BiState);
191        let resizable_columns = Signal::new(false);
192        let selectable = Signal::new(Selectable::None);
193        let selection_follows_focus = Signal::new(false);
194        let selection = Signal::new(HashSet::new());
195        let selection_mode = Signal::new(TableSelectionMode::Cell);
196        let expanded_row_ids = Signal::new(Vec::new());
197        let focused = Signal::new(None);
198        let treegrid_label = Signal::new(None::<String>);
199
200        let visible_rows = Memo::new({
201            let row_id = row_id.clone();
202            let parent_id = parent_id.clone();
203            move |_| {
204                row_signal.with(|rows| {
205                    expanded_row_ids.with(|expanded| {
206                        flatten_visible_rows(rows, &*row_id, &*parent_id, expanded)
207                    })
208                })
209            }
210        });
211
212        let focused_index = Memo::new(move |_| {
213            visible_rows.with(|rows| {
214                let focused_row_id =
215                    focused.with(|focused: &Option<TableFocus<Id, K>>| match focused {
216                        Some(TableFocus::Row(id)) => Some(id.clone()),
217                        Some(TableFocus::Cell(_id, _)) => None,
218                        None => None,
219                    });
220
221                rows.iter().position(|row| Some(&row.id) == focused_row_id.as_ref())
222            })
223        });
224
225        let columns = Memo::new(move |_| {
226            column_signal.with(|columns| {
227                columns
228                    .deref()
229                    .iter()
230                    .filter(|column| !column.hidden.get())
231                    .map(|column| column.key.clone())
232                    .collect::<Vec<_>>()
233            })
234        });
235
236        let column_layout = Memo::new(move |_| {
237            column_signal.with(|columns| {
238                columns
239                    .deref()
240                    .iter()
241                    .map(|column| (column.key.clone(), column.hidden.get()))
242                    .collect::<Vec<_>>()
243            })
244        });
245
246        let handle = Self {
247            rows: row_signal,
248            visible_rows,
249            _header: PhantomData,
250            row_id,
251            parent_id,
252            sort_state,
253            sort_cycle,
254            resizable_columns,
255            selectable,
256            selection_follows_focus,
257            selection,
258            selection_mode,
259            expanded_row_ids,
260            focused,
261            treegrid_label,
262            on_sort: None,
263            on_select: None,
264            on_row_toggle: None,
265            columns,
266        }
267        .build(cx, move |cx| {
268            Keymap::from(vec![
269                (
270                    KeyChord::new(Modifiers::empty(), Code::ArrowRight),
271                    KeymapEntry::new("Focus Right", |cx| {
272                        cx.emit(TreeTableEvent::<K, Id>::FocusRight)
273                    }),
274                ),
275                (
276                    KeyChord::new(Modifiers::empty(), Code::ArrowLeft),
277                    KeymapEntry::new("Focus Left", |cx| {
278                        cx.emit(TreeTableEvent::<K, Id>::FocusLeft)
279                    }),
280                ),
281                (
282                    KeyChord::new(Modifiers::empty(), Code::ArrowUp),
283                    KeymapEntry::new("Focus Up", |cx| cx.emit(TreeTableEvent::<K, Id>::FocusUp)),
284                ),
285                (
286                    KeyChord::new(Modifiers::empty(), Code::ArrowDown),
287                    KeymapEntry::new("Focus Down", |cx| {
288                        cx.emit(TreeTableEvent::<K, Id>::FocusDown)
289                    }),
290                ),
291                (
292                    KeyChord::new(Modifiers::empty(), Code::Home),
293                    KeymapEntry::new("Focus Home", |cx| {
294                        cx.emit(TreeTableEvent::<K, Id>::FocusHome)
295                    }),
296                ),
297                (
298                    KeyChord::new(Modifiers::empty(), Code::End),
299                    KeymapEntry::new("Focus End", |cx| cx.emit(TreeTableEvent::<K, Id>::FocusEnd)),
300                ),
301                (
302                    KeyChord::new(Modifiers::empty(), Code::PageUp),
303                    KeymapEntry::new("Focus Page Up", |cx| {
304                        cx.emit(TreeTableEvent::<K, Id>::PageUp)
305                    }),
306                ),
307                (
308                    KeyChord::new(Modifiers::empty(), Code::PageDown),
309                    KeymapEntry::new("Focus Page Down", |cx| {
310                        cx.emit(TreeTableEvent::<K, Id>::PageDown)
311                    }),
312                ),
313                (
314                    KeyChord::new(Modifiers::CTRL, Code::Home),
315                    KeymapEntry::new("Focus Control Home", |cx| {
316                        cx.emit(TreeTableEvent::<K, Id>::CtrlHome)
317                    }),
318                ),
319                (
320                    KeyChord::new(Modifiers::CTRL, Code::End),
321                    KeymapEntry::new("Focus Control End", |cx| {
322                        cx.emit(TreeTableEvent::<K, Id>::CtrlEnd)
323                    }),
324                ),
325                (
326                    KeyChord::new(Modifiers::empty(), Code::Enter),
327                    KeymapEntry::new("Select Focused", |cx| {
328                        cx.emit(TreeTableEvent::<K, Id>::SelectFocused)
329                    }),
330                ),
331            ])
332            .build(cx);
333
334            Binding::new(cx, column_layout, move |cx| {
335                let visible_columns = column_signal.with(|columns| {
336                    columns
337                        .deref()
338                        .iter()
339                        .filter(|column| !column.hidden.get())
340                        .cloned()
341                        .collect::<Vec<_>>()
342                });
343
344                let last_header_index = visible_columns.len().saturating_sub(1);
345                let header_columns = Rc::new(visible_columns);
346
347                HStack::new(cx, move |cx| {
348                    for (column_index, column) in header_columns.iter().cloned().enumerate() {
349                        let width_signal = column.width;
350                        let sort_state = sort_state;
351                        let resizable_columns = resizable_columns;
352                        let min_width = column.min_width;
353                        let sortable = column.sortable;
354                        let resizable = column.resizable;
355                        let is_last_column = column_index == last_header_index;
356                        let header_content = column.header_content.clone();
357                        let column_key = column.key.clone();
358                        let sort_direction = sort_state.map({
359                            let column_key = column_key.clone();
360                            move |state| sort_direction_for_column(state.as_ref(), &column_key)
361                        });
362
363                        if is_last_column {
364                            let header = header_content(cx, sort_direction);
365                            let on_press_column_key = column_key.clone();
366                            header
367                                .class("table-header-cell")
368                                .role(Role::ColumnHeader)
369                                .sort_direction(sort_direction.map(|direction| match direction {
370                                    TableSortDirection::Ascending => {
371                                        Some(AccessSortDirection::Ascending)
372                                    }
373                                    TableSortDirection::Descending => {
374                                        Some(AccessSortDirection::Descending)
375                                    }
376                                    TableSortDirection::None => None,
377                                }))
378                                .toggle_class("sortable", sortable)
379                                .toggle_class("not-sortable", sortable.map(|value| !*value))
380                                .toggle_class("resizable", false)
381                                .toggle_class("not-resizable", true)
382                                .width(Stretch(1.0))
383                                .min_width(Auto)
384                                .on_press(move |cx| {
385                                    cx.emit(TreeTableEvent::<K, Id>::SelectColumn(
386                                        on_press_column_key.clone(),
387                                    ));
388                                });
389                        } else {
390                            Resizable::new(
391                                cx,
392                                width_signal,
393                                ResizeStackDirection::Right,
394                                move |_cx, new_size| {
395                                    if resizable_columns.get() && resizable.get() {
396                                        width_signal.set(Pixels(new_size.max(min_width.get())));
397                                    }
398                                },
399                                move |cx| {
400                                    let header = header_content(cx, sort_direction);
401                                    let on_press_column_key = column_key.clone();
402                                    header.on_press(move |cx| {
403                                        cx.emit(TreeTableEvent::<K, Id>::SelectColumn(
404                                            on_press_column_key.clone(),
405                                        ));
406                                    });
407                                },
408                            )
409                            .class("table-header-cell")
410                            .role(Role::ColumnHeader)
411                            .sort_direction(sort_direction.map(|direction| match direction {
412                                TableSortDirection::Ascending => {
413                                    Some(AccessSortDirection::Ascending)
414                                }
415                                TableSortDirection::Descending => {
416                                    Some(AccessSortDirection::Descending)
417                                }
418                                TableSortDirection::None => None,
419                            }))
420                            .toggle_class("sortable", sortable)
421                            .toggle_class("not-sortable", sortable.map(|value| !*value))
422                            .toggle_class(
423                                "resizable",
424                                resizable_columns.map(move |enabled| *enabled && resizable.get()),
425                            )
426                            .toggle_class(
427                                "not-resizable",
428                                resizable_columns.map(move |enabled| !*enabled || !resizable.get()),
429                            )
430                            .width(width_signal)
431                            .min_width(min_width.map(|value| Pixels(*value)));
432                        }
433                    }
434                })
435                .class("table-header-row")
436                .height(Auto)
437                .width(Stretch(1.0))
438                .min_width(Auto);
439
440                VirtualList::new_custom_items_with_selection(
441                    cx,
442                    visible_rows,
443                    item_height,
444                    move |cx, row_index, row, _selected| {
445                        let row: Memo<TreeTableRow<T, Id>> = Memo::new(move |_| row.get());
446                        let mut row_handle =
447                            HStack::new(cx, |cx| {
448                                column_signal.with(|columns| {
449                                    let visible_columns = columns
450                                        .deref()
451                                        .iter()
452                                        .filter(|column| !column.hidden.get())
453                                        .collect::<Vec<_>>();
454
455                                    for (column_index, column) in visible_columns.iter().enumerate()
456                                    {
457                                        let width_signal = column.width;
458                                        let min_width = column.min_width;
459                                        let cell_content = column.cell_content.clone();
460                                        let is_last_column =
461                                            column_index + 1 == visible_columns.len();
462
463                                        let col_key = column.key.clone();
464                                        let cell_row_id = row.map(|value| value.id.clone());
465                                        let cell_col_key = col_key.clone();
466
467                                        let col_key_for_focus = col_key.clone();
468                                        let row_id_for_focus = row.map(|value| value.id.clone());
469                                        let cell_is_focused =
470                                            focused.map(move |focused| match focused {
471                                                Some(TableFocus::Cell(id, col)) => {
472                                                    *col == col_key_for_focus
473                                                        && *id == row_id_for_focus.get()
474                                                }
475                                                _ => false,
476                                            });
477
478                                        let col_key_for_select = col_key.clone();
479                                        let row_id_for_select = row.map(|value| value.id.clone());
480                                        let cell_is_selected = selection.map(move |selection| {
481                                            selection.contains(&Cell(
482                                                row_id_for_select.get(),
483                                                col_key_for_select.clone(),
484                                            ))
485                                        });
486
487                                        let cell_col_key_1 = cell_col_key.clone();
488                                        let cell_col_key_2 = cell_col_key.clone();
489
490                                        if is_last_column {
491                                            VStack::new(cx, move |cx| {
492                                                cell_content(cx, row);
493                                            })
494                                            .class("table-cell")
495                                            .role(Role::GridCell)
496                                            .toggle_class("selected", cell_is_selected)
497                                            .selected(cell_is_selected)
498                                            .focusable(true)
499                                            .navigable(false)
500                                            .focused_with_visibility(cell_is_focused, true)
501                                            .width(Stretch(1.0))
502                                            .min_width(Auto)
503                                            .height(Stretch(1.0))
504                                            .min_height(Auto)
505                                            .on_press(move |cx| {
506                                                cx.emit(TreeTableEvent::<K, Id>::SelectCell(
507                                                    cell_row_id.get(),
508                                                    cell_col_key_1.clone(),
509                                                ));
510                                            });
511                                        } else {
512                                            VStack::new(cx, move |cx| {
513                                                cell_content(cx, row);
514                                            })
515                                            .class("table-cell")
516                                            .role(Role::GridCell)
517                                            .toggle_class("selected", cell_is_selected)
518                                            .selected(cell_is_selected)
519                                            .focusable(true)
520                                            .navigable(false)
521                                            .focused_with_visibility(cell_is_focused, true)
522                                            .width(width_signal)
523                                            .min_width(min_width.map(|value| Pixels(*value)))
524                                            .height(Stretch(1.0))
525                                            .min_height(Auto)
526                                            .on_press(move |cx| {
527                                                cx.emit(TreeTableEvent::<K, Id>::SelectCell(
528                                                    cell_row_id.get(),
529                                                    cell_col_key_2.clone(),
530                                                ));
531                                            });
532                                        }
533                                    }
534                                });
535                            })
536                            .class("table-row")
537                            .toggle_class("odd", row_index % 2 == 1)
538                            .toggle_class("even", row_index % 2 == 0)
539                            .toggle_class("expanded", row.map(|value| value.expanded))
540                            .toggle_class("collapsible", row.map(|value| value.has_children))
541                            .alignment(Alignment::Left)
542                            .height(Auto)
543                            .width(Stretch(1.0))
544                            .min_width(Auto)
545                            .role(Role::Row)
546                            .level(row.map(|value| value.depth + 1))
547                            .on_press(move |cx| cx.emit(ListEvent::Select(row_index)));
548
549                        if row.get().has_children {
550                            row_handle = row_handle.expanded(row.map(|value| value.expanded));
551                        }
552
553                        row_handle
554                    },
555                )
556                .width(Stretch(1.0))
557                .min_width(Auto)
558                .height(Stretch(1.0))
559                .min_height(Auto)
560                .class("table-body")
561                .class("tree-table-body")
562                .focused_index(focused_index)
563                .on_focus(move |_cx, index| {
564                    visible_rows.with(|rows| {
565                        if let Some(row) = rows.get(index) {
566                            focused.set(Some(TableFocus::Row(row.id.clone())));
567                        }
568                    });
569                })
570                .on_build(|cx| {
571                    cx.emit_to(
572                        cx.current(),
573                        KeymapEvent::RemoveAction(
574                            KeyChord::new(Modifiers::empty(), Code::ArrowDown),
575                            "Focus Next",
576                        ),
577                    );
578
579                    cx.emit_to(
580                        cx.current(),
581                        KeymapEvent::RemoveAction(
582                            KeyChord::new(Modifiers::empty(), Code::ArrowUp),
583                            "Focus Previous",
584                        ),
585                    );
586                });
587            });
588        })
589        .class("table")
590        .class("virtual-table")
591        .class("tree-table")
592        .class("virtual-tree-table")
593        .navigable(true)
594        .name(treegrid_label.map(|label| label.clone().unwrap_or_else(|| "Tree table".to_string())))
595        .multiselectable(selectable.map(|mode| *mode == Selectable::Multi))
596        .role(Role::TreeGrid);
597
598        let flatten_rows_for_bind = flatten_rows.clone();
599        handle.bind(tree_signal, move |handle| {
600            let rows = tree_signal.with(|tree| flatten_rows_for_bind(tree));
601            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| table.rows.set(rows));
602        })
603    }
604
605    pub fn from_rows<S, C, R>(
606        cx: &mut Context,
607        rows: S,
608        columns: C,
609        item_height: f32,
610        row_id: impl Fn(&T) -> Id + 'static,
611        parent_id: impl Fn(&T) -> Option<Id> + 'static,
612    ) -> Handle<Self>
613    where
614        S: Res<V> + 'static,
615        C: Res<R> + 'static,
616        R: Deref<Target = [TreeTableColumn<T, Id, H, K>]> + Clone + 'static,
617    {
618        Self::new(cx, rows, columns, item_height, |rows: &V| rows.clone(), row_id, parent_id)
619    }
620
621    fn emit_toggle(&self, cx: &mut EventContext, row_id: Id, next_expanded: bool) {
622        if let Some(callback) = &self.on_row_toggle {
623            (callback)(cx, row_id, next_expanded);
624        }
625    }
626
627    fn focused_visible_row(&self) -> Option<TreeTableRow<T, Id>> {
628        let focused_id = self.focused.get().map(|focused| match focused {
629            TableFocus::Row(id) => id.clone(),
630            TableFocus::Cell(id, _) => id.clone(),
631        })?;
632
633        self.visible_rows.with(|rows| rows.clone()).into_iter().find(|row| row.id == focused_id)
634    }
635
636    fn focus_row_id(&self, row_id: Id) {
637        let next_focus = match self.focused.get() {
638            Some(TableFocus::Cell(_, column_key)) => TableFocus::Cell(row_id, column_key),
639            _ => TableFocus::Row(row_id),
640        };
641
642        self.focused.set(Some(next_focus));
643    }
644}
645
646impl<Id, H, K> VirtualTreeTable<TreeNodeRow<Id>, Vec<TreeNodeRow<Id>>, Id, H, K>
647where
648    Id: Eq + Hash + Clone + Send + Sync + 'static,
649    H: Clone + View,
650    K: Eq + Hash + Clone + PartialEq + Send + Sync + 'static,
651{
652    /// Creates a [`VirtualTreeTable`] directly from hierarchical data.
653    ///
654    /// Provide closures to enumerate root node IDs and child node IDs for a given parent.
655    /// The order returned by those closures is preserved; filtering only removes hidden
656    /// nodes and their descendants.
657    /// The table manages expand/collapse state and internally projects IDs into
658    /// [`TreeNodeRow`] values.
659    pub fn from_hierarchy<S, U, C, R>(
660        cx: &mut Context,
661        tree: S,
662        columns: C,
663        item_height: f32,
664        root_ids: impl Fn(&U) -> Vec<Id> + 'static,
665        child_ids: impl Fn(&U, &Id) -> Vec<Id> + 'static,
666        is_visible: impl Fn(&U, &Id) -> bool + 'static,
667    ) -> Handle<Self>
668    where
669        S: Res<U> + 'static,
670        U: Clone + 'static,
671        C: Res<R> + 'static,
672        R: Deref<Target = [TreeTableColumn<TreeNodeRow<Id>, Id, H, K>]> + Clone + 'static,
673    {
674        let root_ids: Rc<dyn Fn(&U) -> Vec<Id>> = Rc::new(root_ids);
675        let child_ids: Rc<dyn Fn(&U, &Id) -> Vec<Id>> = Rc::new(child_ids);
676        let is_visible: Rc<dyn Fn(&U, &Id) -> bool> = Rc::new(is_visible);
677
678        Self::new(
679            cx,
680            tree,
681            columns,
682            item_height,
683            move |tree: &U| flatten_hierarchy_rows(tree, &*root_ids, &*child_ids, &*is_visible),
684            |row: &TreeNodeRow<Id>| row.id.clone(),
685            |row: &TreeNodeRow<Id>| row.parent_id.clone(),
686        )
687    }
688}
689
690impl<T, V, Id, H, K> View for VirtualTreeTable<T, V, Id, H, K>
691where
692    V: Deref<Target = [T]> + Clone + 'static,
693    T: PartialEq + Clone + 'static,
694    Id: Eq + Hash + Clone + Send + Sync + 'static,
695    H: Clone + View,
696    K: Eq + Hash + Clone + PartialEq + Send + Sync + 'static,
697{
698    fn element(&self) -> Option<&'static str> {
699        Some("virtual-tree-table")
700    }
701
702    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
703        event.map(|table_event: &TreeTableEvent<K, Id>, _| match table_event {
704            TreeTableEvent::RequestSort(key, direction) => {
705                if let Some(callback) = &self.on_sort {
706                    (callback)(cx, key.clone(), *direction);
707                }
708            }
709
710            TreeTableEvent::SelectColumn(column_key) => {
711                let visible_rows = flatten_visible_rows(
712                    &self.rows.get(),
713                    &*self.row_id,
714                    &*self.parent_id,
715                    &self.expanded_row_ids.get(),
716                );
717
718                let column_cells = HashSet::from_iter(
719                    visible_rows.iter().map(|row| Cell(row.id.clone(), column_key.clone())),
720                );
721
722                let current_selection = self.selection.get();
723                let next_selection =
724                    if current_selection == column_cells { HashSet::new() } else { column_cells };
725
726                self.selection.set(next_selection.clone());
727
728                if let Some(callback) = &self.on_select {
729                    (callback)(cx, next_selection);
730                }
731            }
732
733            TreeTableEvent::SelectRow(index) => {
734                if self.selectable.get() == Selectable::None {
735                    return;
736                }
737
738                let visible_rows = flatten_visible_rows(
739                    &self.rows.get(),
740                    &*self.row_id,
741                    &*self.parent_id,
742                    &self.expanded_row_ids.get(),
743                );
744
745                if let Some(row) = visible_rows.get(*index) {
746                    let cols = self.columns.get();
747                    self.selection.set(HashSet::from_iter(
748                        cols.iter().map(|col| Cell(row.id.clone(), col.clone())),
749                    ));
750
751                    if let Some(callback) = &self.on_select {
752                        (callback)(cx, self.selection.get());
753                    }
754                }
755            }
756
757            TreeTableEvent::SelectCell(row_id, column_key) => {
758                let selectable = self.selectable.get();
759                if selectable == Selectable::None {
760                    return;
761                }
762
763                let selection_mode = self.selection_mode.get();
764
765                let next_selection = match selection_mode {
766                    TableSelectionMode::Cell => {
767                        let cell = Cell(row_id.clone(), column_key.clone());
768                        if selectable == Selectable::Single {
769                            let current = self.selection.get();
770                            if current.contains(&cell) {
771                                HashSet::new()
772                            } else {
773                                HashSet::from([cell])
774                            }
775                        } else {
776                            let current = self.selection.get();
777                            let mut next = current;
778                            if next.contains(&cell) {
779                                next.remove(&cell);
780                            } else {
781                                next.insert(cell);
782                            }
783                            next
784                        }
785                    }
786                    TableSelectionMode::Row => {
787                        let cols = self.columns.get();
788                        let row_cells: HashSet<Cell<Id, K>> = HashSet::from_iter(
789                            cols.iter().map(|col| Cell(row_id.clone(), col.clone())),
790                        );
791
792                        if selectable == Selectable::Single {
793                            let current = self.selection.get();
794                            if current == row_cells { HashSet::new() } else { row_cells }
795                        } else {
796                            let mut next = self.selection.get();
797                            let row_selected = row_cells.iter().all(|c| next.contains(c));
798                            if row_selected {
799                                for c in &row_cells {
800                                    next.remove(c);
801                                }
802                            } else {
803                                next.extend(row_cells);
804                            }
805                            next
806                        }
807                    }
808                };
809
810                self.selection.set(next_selection.clone());
811
812                if let Some(callback) = &self.on_select {
813                    (callback)(cx, next_selection);
814                }
815            }
816
817            TreeTableEvent::SelectFocused => self.focused.with(|focused| match focused {
818                Some(TableFocus::Row(id)) => {
819                    if self.selectable.get() == Selectable::None {
820                        return;
821                    }
822
823                    let cols = self.columns.get();
824                    self.selection.set(HashSet::from_iter(
825                        cols.iter().map(|col| Cell(id.clone(), col.clone())),
826                    ));
827
828                    if let Some(callback) = &self.on_select {
829                        (callback)(cx, self.selection.get());
830                    }
831                }
832                Some(TableFocus::Cell(id, col)) => {
833                    self.selection.set(HashSet::from([Cell(id.clone(), col.clone())]));
834
835                    if let Some(callback) = &self.on_select {
836                        (callback)(cx, self.selection.get());
837                    }
838                }
839                None => {}
840            }),
841
842            TreeTableEvent::ExpandSelected => {
843                if let Some(row) = self.focused_visible_row() {
844                    if row.has_children && !row.expanded {
845                        self.emit_toggle(cx, row.id, true);
846                    } else if row.has_children {
847                        let child_id = self.visible_rows.with(|rows| {
848                            rows.iter()
849                                .find(|candidate| candidate.parent_id.as_ref() == Some(&row.id))
850                                .map(|candidate| candidate.id.clone())
851                        });
852
853                        if let Some(child_id) = child_id {
854                            self.focus_row_id(child_id);
855                        }
856                    }
857                }
858            }
859
860            TreeTableEvent::FocusRight => {
861                let next_focus = match self.focused.get() {
862                    Some(TableFocus::Row(id)) => {
863                        if let Some(row) = self.focused_visible_row() {
864                            if row.has_children && !row.expanded {
865                                cx.emit(TreeTableEvent::<K, Id>::ExpandSelected);
866                                return;
867                            }
868                        }
869
870                        let first_column_key =
871                            self.columns.with(|columns| columns.first().cloned()).unwrap();
872                        Some(TableFocus::Cell(id.clone(), first_column_key))
873                    }
874                    Some(TableFocus::Cell(id, col)) => {
875                        let next_col = self.columns.with(|columns| {
876                            columns
877                                .iter()
878                                .position(|column| column == &col)
879                                .and_then(|index| index.checked_add(1))
880                                .and_then(|index| columns.get(index))
881                                .cloned()
882                        });
883
884                        next_col.map(|next_col| TableFocus::Cell(id.clone(), next_col))
885                    }
886                    None => None,
887                };
888
889                if next_focus.is_some() {
890                    self.focused.set(next_focus);
891                }
892            }
893
894            TreeTableEvent::FocusLeft => {
895                let next_focus = match self.focused.get() {
896                    Some(TableFocus::Row(_)) => {
897                        if let Some(row) = self.focused_visible_row() {
898                            if row.has_children && row.expanded {
899                                self.emit_toggle(cx, row.id, false);
900                            }
901                        }
902
903                        None
904                    }
905                    Some(TableFocus::Cell(id, col)) => {
906                        let prev_col = self.columns.with(|columns| {
907                            columns
908                                .iter()
909                                .position(|column| column == &col)
910                                .and_then(|index| index.checked_sub(1))
911                                .and_then(|index| columns.get(index))
912                                .cloned()
913                        });
914
915                        prev_col
916                            .map(|prev_col| TableFocus::Cell(id.clone(), prev_col))
917                            .or_else(|| Some(TableFocus::Row(id.clone())))
918                    }
919                    None => None,
920                };
921
922                if next_focus.is_some() {
923                    self.focused.set(next_focus);
924                }
925            }
926
927            TreeTableEvent::FocusHome => {
928                let next_focus = self.focused.with(|focused| match focused {
929                    Some(TableFocus::Row(_)) => self
930                        .visible_rows
931                        .with(|rows| rows.first().map(|row| TableFocus::Row(row.id.clone()))),
932                    Some(TableFocus::Cell(_, col)) => self.visible_rows.with(|rows| {
933                        rows.first().map(|row| TableFocus::Cell(row.id.clone(), col.clone()))
934                    }),
935                    None => None,
936                });
937
938                if next_focus.is_some() {
939                    self.focused.set(next_focus);
940                }
941            }
942
943            TreeTableEvent::FocusEnd => {
944                let next_focus = self.focused.with(|focused| match focused {
945                    Some(TableFocus::Row(_)) => self
946                        .visible_rows
947                        .with(|rows| rows.last().map(|row| TableFocus::Row(row.id.clone()))),
948                    Some(TableFocus::Cell(_, col)) => self.visible_rows.with(|rows| {
949                        rows.last().map(|row| TableFocus::Cell(row.id.clone(), col.clone()))
950                    }),
951                    None => None,
952                });
953
954                if next_focus.is_some() {
955                    self.focused.set(next_focus);
956                }
957            }
958
959            TreeTableEvent::PageUp => {
960                let page_size = self.visible_rows.with(|rows| rows.len().saturating_div(2).max(1));
961                let next_focus = self.focused.with(|focused| {
962                    self.visible_rows.with(|rows| {
963                        let current_index = focused.as_ref().and_then(|focused| match focused {
964                            TableFocus::Row(id) | TableFocus::Cell(id, _) => {
965                                rows.iter().position(|row| row.id == id.clone())
966                            }
967                        });
968
969                        current_index.and_then(|index| {
970                            let next_index = index.saturating_sub(page_size);
971                            rows.get(next_index).map(|row| match focused.as_ref() {
972                                Some(TableFocus::Cell(_, col)) => {
973                                    TableFocus::Cell(row.id.clone(), col.clone())
974                                }
975                                _ => TableFocus::Row(row.id.clone()),
976                            })
977                        })
978                    })
979                });
980
981                if next_focus.is_some() {
982                    self.focused.set(next_focus);
983                }
984            }
985
986            TreeTableEvent::PageDown => {
987                let page_size = self.visible_rows.with(|rows| rows.len().saturating_div(2).max(1));
988                let next_focus = self.focused.with(|focused| {
989                    self.visible_rows.with(|rows| {
990                        let current_index = focused.as_ref().and_then(|focused| match focused {
991                            TableFocus::Row(id) | TableFocus::Cell(id, _) => {
992                                rows.iter().position(|row| row.id == id.clone())
993                            }
994                        });
995
996                        current_index.and_then(|index| {
997                            let next_index = (index + page_size).min(rows.len().saturating_sub(1));
998                            rows.get(next_index).map(|row| match focused.as_ref() {
999                                Some(TableFocus::Cell(_, col)) => {
1000                                    TableFocus::Cell(row.id.clone(), col.clone())
1001                                }
1002                                _ => TableFocus::Row(row.id.clone()),
1003                            })
1004                        })
1005                    })
1006                });
1007
1008                if next_focus.is_some() {
1009                    self.focused.set(next_focus);
1010                }
1011            }
1012
1013            TreeTableEvent::CtrlHome => {
1014                let next_focus = self.focused.with(|focused| {
1015                    self.visible_rows.with(|rows| {
1016                        rows.first().map(|row| match focused {
1017                            Some(TableFocus::Cell(_, col)) => {
1018                                TableFocus::Cell(row.id.clone(), col.clone())
1019                            }
1020                            _ => TableFocus::Row(row.id.clone()),
1021                        })
1022                    })
1023                });
1024
1025                if next_focus.is_some() {
1026                    self.focused.set(next_focus);
1027                }
1028            }
1029
1030            TreeTableEvent::CtrlEnd => {
1031                let next_focus = self.focused.with(|focused| {
1032                    self.visible_rows.with(|rows| {
1033                        rows.last().map(|row| match focused {
1034                            Some(TableFocus::Cell(_, col)) => {
1035                                TableFocus::Cell(row.id.clone(), col.clone())
1036                            }
1037                            _ => TableFocus::Row(row.id.clone()),
1038                        })
1039                    })
1040                });
1041
1042                if next_focus.is_some() {
1043                    self.focused.set(next_focus);
1044                }
1045            }
1046
1047            TreeTableEvent::FocusUp => {
1048                let next_focus = self.focused.with(|focused| match focused {
1049                    Some(TableFocus::Row(id)) => self.visible_rows.with(|rows| {
1050                        rows.iter()
1051                            .take_while(|row| row.id != *id)
1052                            .last()
1053                            .map(|row| TableFocus::Row(row.id.clone()))
1054                    }),
1055                    Some(TableFocus::Cell(id, col)) => self.visible_rows.with(|rows| {
1056                        rows.iter()
1057                            .take_while(|row| row.id != *id)
1058                            .last()
1059                            .map(|row| TableFocus::Cell(row.id.clone(), col.clone()))
1060                    }),
1061                    None => None,
1062                });
1063
1064                if next_focus.is_some() {
1065                    self.focused.set(next_focus);
1066                }
1067            }
1068
1069            TreeTableEvent::FocusDown => {
1070                let next_focus = self.focused.with(|focused| match focused {
1071                    Some(TableFocus::Row(id)) => self.visible_rows.with(|rows| {
1072                        rows.iter()
1073                            .skip_while(|row| row.id != *id)
1074                            .nth(1)
1075                            .map(|row| TableFocus::Row(row.id.clone()))
1076                    }),
1077                    Some(TableFocus::Cell(id, col)) => self.visible_rows.with(|rows| {
1078                        rows.iter()
1079                            .skip_while(|row| row.id != *id)
1080                            .nth(1)
1081                            .map(|row| TableFocus::Cell(row.id.clone(), col.clone()))
1082                    }),
1083                    None => None,
1084                });
1085
1086                if next_focus.is_some() {
1087                    self.focused.set(next_focus);
1088                }
1089            }
1090        });
1091
1092        event.map(|cell_event: &TreeTableFirstCellEvent<Id>, _| {
1093            let TreeTableFirstCellEvent::Toggle(id, next) = cell_event;
1094            self.emit_toggle(cx, id.clone(), *next);
1095        });
1096
1097        event.map(|table_event: &TableEvent<K>, _| match table_event {
1098            TableEvent::ToggleSort(col) => {
1099                let visible_rows = flatten_visible_rows(
1100                    &self.rows.get(),
1101                    &*self.row_id,
1102                    &*self.parent_id,
1103                    &self.expanded_row_ids.get(),
1104                );
1105
1106                let column_cells = HashSet::from_iter(
1107                    visible_rows.iter().map(|row| Cell(row.id.clone(), col.clone())),
1108                );
1109
1110                let current_selection = self.selection.get();
1111                let next_selection =
1112                    if current_selection == column_cells { HashSet::new() } else { column_cells };
1113
1114                self.selection.set(next_selection.clone());
1115
1116                if let Some(callback) = &self.on_select {
1117                    (callback)(cx, next_selection);
1118                }
1119
1120                if let Some(callback) = &self.on_sort {
1121                    let current_direction =
1122                        sort_direction_for_column(self.sort_state.get().as_ref(), col);
1123                    let next_direction =
1124                        next_sort_direction(self.sort_cycle.get(), current_direction);
1125                    (callback)(cx, col.clone(), next_direction);
1126                }
1127            }
1128
1129            _ => {}
1130        });
1131    }
1132}
1133
1134pub trait VirtualTreeTableModifiers<Id, K = String>: Sized
1135where
1136    Id: Eq + Hash + Clone + Send + Sync + 'static,
1137    K: Eq + Hash + Clone + PartialEq + Send + Sync + 'static,
1138{
1139    fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self;
1140
1141    fn resizable_columns<U: Into<bool> + Clone + 'static>(
1142        self,
1143        flag: impl Res<U> + 'static,
1144    ) -> Self;
1145
1146    fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
1147        self,
1148        cycle: impl Res<U> + 'static,
1149    ) -> Self;
1150
1151    fn selectable<U: Into<Selectable> + Clone + 'static>(
1152        self,
1153        selectable: impl Res<U> + 'static,
1154    ) -> Self;
1155
1156    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
1157        self,
1158        flag: impl Res<U> + 'static,
1159    ) -> Self;
1160
1161    fn selection_mode<U: Into<TableSelectionMode> + Clone + 'static>(
1162        self,
1163        mode: impl Res<U> + 'static,
1164    ) -> Self;
1165
1166    fn treegrid_label<U: Into<Option<String>> + Clone + 'static>(
1167        self,
1168        label: impl Res<U> + 'static,
1169    ) -> Self;
1170
1171    fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
1172    where
1173        R: Deref<Target = [Cell<Id, K>]> + Clone + 'static;
1174
1175    fn expanded_row_ids<R>(self, expanded_row_ids: impl Res<R> + 'static) -> Self
1176    where
1177        R: Deref<Target = [Id]> + Clone + 'static;
1178
1179    fn on_sort<F>(self, callback: F) -> Self
1180    where
1181        F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync;
1182
1183    fn on_select<F>(self, callback: F) -> Self
1184    where
1185        F: 'static + Fn(&mut EventContext, HashSet<Cell<Id, K>>) + Send + Sync;
1186
1187    fn on_row_toggle<F>(self, callback: F) -> Self
1188    where
1189        F: 'static + Fn(&mut EventContext, Id, bool);
1190}
1191
1192impl<T, V, Id, H, K> VirtualTreeTableModifiers<Id, K>
1193    for Handle<'_, VirtualTreeTable<T, V, Id, H, K>>
1194where
1195    V: Deref<Target = [T]> + Clone + 'static,
1196    T: PartialEq + Clone + 'static,
1197    Id: Eq + Hash + Clone + Send + Sync + 'static,
1198    H: Clone + View,
1199    K: Eq + Hash + Clone + PartialEq + Send + Sync + 'static,
1200{
1201    fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self {
1202        let sort_state = sort_state.to_signal(self.cx);
1203        self.bind(sort_state, move |handle| {
1204            let sort_state = sort_state.get();
1205            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1206                table.sort_state.set(sort_state)
1207            });
1208        })
1209    }
1210
1211    fn resizable_columns<U: Into<bool> + Clone + 'static>(
1212        self,
1213        flag: impl Res<U> + 'static,
1214    ) -> Self {
1215        let flag = flag.to_signal(self.cx);
1216        self.bind(flag, move |handle| {
1217            let flag = flag.get().into();
1218            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1219                table.resizable_columns.set(flag)
1220            });
1221        })
1222    }
1223
1224    fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
1225        self,
1226        cycle: impl Res<U> + 'static,
1227    ) -> Self {
1228        let cycle = cycle.to_signal(self.cx);
1229        self.bind(cycle, move |handle| {
1230            let cycle = cycle.get().into();
1231            handle
1232                .modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| table.sort_cycle.set(cycle));
1233        })
1234    }
1235
1236    fn selectable<U: Into<Selectable> + Clone + 'static>(
1237        self,
1238        selectable: impl Res<U> + 'static,
1239    ) -> Self {
1240        let selectable = selectable.to_signal(self.cx);
1241        self.bind(selectable, move |handle| {
1242            let selectable = selectable.get().into();
1243            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1244                table.selectable.set(selectable)
1245            });
1246        })
1247    }
1248
1249    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
1250        self,
1251        flag: impl Res<U> + 'static,
1252    ) -> Self {
1253        let flag = flag.to_signal(self.cx);
1254        self.bind(flag, move |handle| {
1255            let flag = flag.get().into();
1256            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1257                table.selection_follows_focus.set(flag)
1258            });
1259        })
1260    }
1261
1262    fn selection_mode<U: Into<TableSelectionMode> + Clone + 'static>(
1263        self,
1264        mode: impl Res<U> + 'static,
1265    ) -> Self {
1266        let mode = mode.to_signal(self.cx);
1267        self.bind(mode, move |handle| {
1268            let mode = mode.get().into();
1269            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1270                table.selection_mode.set(mode)
1271            });
1272        })
1273    }
1274
1275    fn treegrid_label<U: Into<Option<String>> + Clone + 'static>(
1276        self,
1277        label: impl Res<U> + 'static,
1278    ) -> Self {
1279        let label = label.to_signal(self.cx);
1280        self.bind(label, move |handle| {
1281            let label = label.get().into();
1282            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1283                table.treegrid_label.set(label)
1284            });
1285        })
1286    }
1287
1288    fn selection<R>(self, selection: impl Res<R> + 'static) -> Self
1289    where
1290        R: Deref<Target = [Cell<Id, K>]> + Clone + 'static,
1291    {
1292        let selection = selection.to_signal(self.cx);
1293        self.bind(selection, move |handle| {
1294            let ids = selection.with(|ids| ids.deref().to_vec());
1295            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1296                table.selection.set(HashSet::from_iter(ids))
1297            });
1298        })
1299    }
1300
1301    fn expanded_row_ids<R>(self, expanded_row_ids: impl Res<R> + 'static) -> Self
1302    where
1303        R: Deref<Target = [Id]> + Clone + 'static,
1304    {
1305        let expanded_row_ids = expanded_row_ids.to_signal(self.cx);
1306        self.bind(expanded_row_ids, move |handle| {
1307            let ids = expanded_row_ids.with(|ids| ids.deref().to_vec());
1308            handle.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1309                table.expanded_row_ids.set(ids)
1310            });
1311        })
1312    }
1313
1314    fn on_sort<F>(self, callback: F) -> Self
1315    where
1316        F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync,
1317    {
1318        self.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1319            table.on_sort = Some(Arc::new(callback))
1320        })
1321    }
1322
1323    fn on_select<F>(self, callback: F) -> Self
1324    where
1325        F: 'static + Fn(&mut EventContext, HashSet<Cell<Id, K>>) + Send + Sync,
1326    {
1327        self.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1328            table.on_select = Some(Box::new(callback))
1329        })
1330    }
1331
1332    fn on_row_toggle<F>(self, callback: F) -> Self
1333    where
1334        F: 'static + Fn(&mut EventContext, Id, bool),
1335    {
1336        self.modify(|table: &mut VirtualTreeTable<T, V, Id, H, K>| {
1337            table.on_row_toggle = Some(Box::new(callback))
1338        })
1339    }
1340}