Skip to main content

vizia_core/views/
virtual_table.rs

1use std::{marker::PhantomData, ops::Deref, rc::Rc, sync::Arc};
2
3use crate::prelude::*;
4
5use super::{
6    TableColumn, TableSortCycle, TableSortDirection, TableSortState, table::next_sort_direction,
7    table::sort_direction_for_column,
8};
9
10/// A virtualized table view backed by [`VirtualList`] for large datasets.
11///
12/// Rows use a fixed `item_height` for virtualization performance.
13pub struct VirtualTable<T, V, Id, H, K = String>
14where
15    V: Deref<Target = [T]> + Clone + 'static,
16    T: PartialEq + Clone + 'static,
17    Id: PartialEq + Clone + 'static,
18    H: View,
19    K: Clone + PartialEq + Send + Sync + 'static,
20{
21    rows: Signal<V>,
22    _header: PhantomData<H>,
23    row_id: Rc<dyn Fn(&T) -> Id>,
24    sort_state: Signal<Option<TableSortState<K>>>,
25    sort_cycle: Signal<TableSortCycle>,
26    resizable_columns: Signal<bool>,
27    selectable: Signal<Selectable>,
28    selection_follows_focus: Signal<bool>,
29    selected_row_ids: Signal<Vec<Id>>,
30    on_sort: Option<Arc<dyn Fn(&mut EventContext, K, TableSortDirection) + Send + Sync>>,
31    on_row_select: Option<Box<dyn Fn(&mut EventContext, Id)>>,
32}
33
34enum VirtualTableEvent<K> {
35    RequestSort(K, TableSortDirection),
36    SelectRow(usize),
37}
38
39impl<T, V, Id, H, K> VirtualTable<T, V, Id, H, K>
40where
41    V: Deref<Target = [T]> + Clone + 'static,
42    T: PartialEq + Clone + 'static,
43    Id: PartialEq + Clone + 'static,
44    H: Clone + View,
45    K: Clone + PartialEq + Send + Sync + 'static,
46{
47    /// Creates a new virtualized table view.
48    pub fn new<S, C, R>(
49        cx: &mut Context,
50        rows: S,
51        columns: C,
52        item_height: f32,
53        row_id: impl Fn(&T) -> Id + 'static,
54    ) -> Handle<Self>
55    where
56        S: Res<V> + 'static,
57        C: Res<R> + 'static,
58        R: Deref<Target = [TableColumn<T, H, K>]> + Clone + 'static,
59    {
60        let row_signal = rows.to_signal(cx);
61        let column_signal = columns.to_signal(cx);
62        let row_id: Rc<dyn Fn(&T) -> Id> = Rc::new(row_id);
63
64        let sort_state = Signal::new(None);
65        let sort_cycle = Signal::new(TableSortCycle::BiState);
66        let resizable_columns = Signal::new(false);
67        let selectable = Signal::new(Selectable::None);
68        let selection_follows_focus = Signal::new(false);
69        let selected_row_ids = Signal::new(Vec::new());
70
71        let selected_indices = Memo::new({
72            let row_id = row_id.clone();
73            move |_| {
74                row_signal.with(|rows| {
75                    selected_row_ids.with(|selected_ids| {
76                        rows.deref()
77                            .iter()
78                            .enumerate()
79                            .filter_map(|(index, row)| {
80                                let id = (row_id)(row);
81                                if selected_ids.contains(&id) { Some(index) } else { None }
82                            })
83                            .collect::<Vec<usize>>()
84                    })
85                })
86            }
87        });
88
89        let column_layout = Memo::new(move |_| {
90            column_signal.with(|columns| {
91                columns
92                    .deref()
93                    .iter()
94                    .map(|column| (column.key.clone(), column.hidden.get()))
95                    .collect::<Vec<_>>()
96            })
97        });
98
99        Self {
100            rows: row_signal,
101            _header: PhantomData,
102            row_id,
103            sort_state,
104            sort_cycle,
105            resizable_columns,
106            selectable,
107            selection_follows_focus,
108            selected_row_ids,
109            on_sort: None,
110            on_row_select: None,
111        }
112        .build(cx, move |cx| {
113            Binding::new(cx, column_layout, move |cx| {
114                let visible_columns = column_signal.with(|columns| {
115                    columns
116                        .deref()
117                        .iter()
118                        .filter(|column| !column.hidden.get())
119                        .cloned()
120                        .collect::<Vec<_>>()
121                });
122                let last_header_index = visible_columns.len().saturating_sub(1);
123                let header_columns = Rc::new(visible_columns);
124
125                HStack::new(cx, move |cx| {
126                    for (column_index, column) in header_columns.iter().cloned().enumerate() {
127                        let width_signal = column.width;
128                        let sort_state = sort_state;
129                        let sort_cycle = sort_cycle;
130                        let resizable_columns = resizable_columns;
131                        let min_width = column.min_width;
132                        let sortable = column.sortable;
133                        let resizable = column.resizable;
134                        let is_last_column = column_index == last_header_index;
135                        let header_content = column.header_content.clone();
136                        let column_key = column.key.clone();
137                        let sort_direction = sort_state.map({
138                            let column_key = column_key.clone();
139                            move |state| sort_direction_for_column(state.as_ref(), &column_key)
140                        });
141
142                        if is_last_column {
143                            HStack::new(cx, move |cx| {
144                                let header = header_content(cx, sort_direction);
145
146                                let column_key = column_key.clone();
147                                header.on_press(move |cx| {
148                                    if sortable.get() {
149                                        let current_direction = sort_direction_for_column(
150                                            sort_state.get().as_ref(),
151                                            &column_key,
152                                        );
153                                        let next_direction = next_sort_direction(
154                                            sort_cycle.get(),
155                                            current_direction,
156                                        );
157                                        cx.emit(VirtualTableEvent::RequestSort(
158                                            column_key.clone(),
159                                            next_direction,
160                                        ));
161                                    }
162                                });
163                            })
164                            .class("table-header-cell")
165                            .toggle_class("sortable", sortable)
166                            .toggle_class("not-sortable", sortable.map(|value| !*value))
167                            .toggle_class("resizable", false)
168                            .width(Stretch(1.0))
169                            .min_width(Auto);
170                        } else {
171                            Resizable::new(
172                                cx,
173                                width_signal.map(|value| Pixels(*value)),
174                                ResizeStackDirection::Right,
175                                move |_cx, new_size| {
176                                    if resizable_columns.get() && resizable.get() {
177                                        width_signal.set(new_size.max(min_width.get()));
178                                    }
179                                },
180                                move |cx| {
181                                    let header = header_content(cx, sort_direction);
182
183                                    let column_key = column_key.clone();
184                                    header.on_press(move |cx| {
185                                        if sortable.get() {
186                                            let current_direction = sort_direction_for_column(
187                                                sort_state.get().as_ref(),
188                                                &column_key,
189                                            );
190                                            let next_direction = next_sort_direction(
191                                                sort_cycle.get(),
192                                                current_direction,
193                                            );
194                                            cx.emit(VirtualTableEvent::RequestSort(
195                                                column_key.clone(),
196                                                next_direction,
197                                            ));
198                                        }
199                                    });
200                                },
201                            )
202                            .class("table-header-cell")
203                            .toggle_class("sortable", sortable)
204                            .toggle_class("not-sortable", sortable.map(|value| !*value))
205                            .toggle_class(
206                                "resizable",
207                                resizable_columns.map(move |enabled| *enabled && resizable.get()),
208                            )
209                            .toggle_class(
210                                "not-resizable",
211                                resizable_columns.map(move |enabled| !*enabled || !resizable.get()),
212                            )
213                            .min_width(min_width.map(|value| Pixels(*value)));
214                        }
215                    }
216                })
217                .class("table-header-row")
218                .height(Auto)
219                .width(Stretch(1.0))
220                .min_width(Auto);
221
222                VirtualList::new(cx, row_signal, item_height, move |cx, row_index, row| {
223                    HStack::new(cx, |cx| {
224                        column_signal.with(|columns| {
225                            let visible_columns = columns
226                                .deref()
227                                .iter()
228                                .filter(|column| !column.hidden.get())
229                                .collect::<Vec<_>>();
230
231                            for (column_index, column) in visible_columns.iter().enumerate() {
232                                let width_signal = column.width;
233                                let min_width = column.min_width;
234                                let cell_content = column.cell_content.clone();
235                                let is_last_column = column_index + 1 == visible_columns.len();
236
237                                if is_last_column {
238                                    VStack::new(cx, move |cx| {
239                                        cell_content(cx, row.map(|value| value.clone()));
240                                    })
241                                    .class("table-cell")
242                                    .width(Stretch(1.0))
243                                    .min_width(Auto)
244                                    .height(Percentage(100.0));
245                                } else {
246                                    VStack::new(cx, move |cx| {
247                                        cell_content(cx, row.map(|value| value.clone()));
248                                    })
249                                    .class("table-cell")
250                                    .width(width_signal.map(|value| Pixels(*value)))
251                                    .min_width(min_width.map(|value| Pixels(*value)))
252                                    .height(Percentage(100.0));
253                                }
254                            }
255                        });
256                    })
257                    .class("table-row")
258                    .toggle_class("odd", row_index % 2 == 1)
259                    .toggle_class("even", row_index % 2 == 0)
260                    .alignment(Alignment::Left)
261                    .height(Percentage(100.0))
262                    .width(Stretch(1.0))
263                    .min_width(Auto)
264                })
265                .width(Stretch(1.0))
266                .min_width(Auto)
267                .height(Stretch(1.0))
268                .min_height(Auto)
269                .class("table-body")
270                .selection(selected_indices)
271                .selectable(selectable)
272                .selection_follows_focus(selection_follows_focus)
273                .on_select(move |cx, index| cx.emit(VirtualTableEvent::<K>::SelectRow(index)));
274            });
275        })
276        .class("table")
277        .role(Role::List)
278    }
279}
280
281impl<T, V, Id, H, K> View for VirtualTable<T, V, Id, H, K>
282where
283    V: Deref<Target = [T]> + Clone + 'static,
284    T: PartialEq + Clone + 'static,
285    Id: PartialEq + Clone + 'static,
286    H: Clone + View,
287    K: Clone + PartialEq + Send + Sync + 'static,
288{
289    fn element(&self) -> Option<&'static str> {
290        Some("virtual-table")
291    }
292
293    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
294        event.map(|table_event: &VirtualTableEvent<K>, _| match table_event {
295            VirtualTableEvent::RequestSort(column, direction) => {
296                if let Some(callback) = &self.on_sort {
297                    (callback)(cx, column.clone(), *direction);
298                }
299            }
300
301            VirtualTableEvent::SelectRow(index) => {
302                let rows = self.rows.get();
303                if let Some(row) = rows.deref().get(*index) {
304                    if let Some(callback) = &self.on_row_select {
305                        (callback)(cx, (self.row_id)(row));
306                    }
307                }
308            }
309        });
310    }
311}
312
313/// Modifiers for configuring controlled virtual table state and callbacks.
314pub trait VirtualTableModifiers<Id, K = String>: Sized
315where
316    K: Clone + PartialEq + Send + Sync + 'static,
317{
318    fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self;
319
320    fn resizable_columns<U: Into<bool> + Clone + 'static>(
321        self,
322        flag: impl Res<U> + 'static,
323    ) -> Self;
324
325    fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
326        self,
327        cycle: impl Res<U> + 'static,
328    ) -> Self;
329
330    fn selectable<U: Into<Selectable> + Clone + 'static>(
331        self,
332        selectable: impl Res<U> + 'static,
333    ) -> Self;
334
335    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
336        self,
337        flag: impl Res<U> + 'static,
338    ) -> Self;
339
340    fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
341    where
342        R: Deref<Target = [Id]> + Clone + 'static;
343
344    fn on_sort<F>(self, callback: F) -> Self
345    where
346        F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync;
347
348    fn on_row_select<F>(self, callback: F) -> Self
349    where
350        F: 'static + Fn(&mut EventContext, Id);
351}
352
353impl<T, V, Id, H, K> VirtualTableModifiers<Id, K> for Handle<'_, VirtualTable<T, V, Id, H, K>>
354where
355    V: Deref<Target = [T]> + Clone + 'static,
356    T: PartialEq + Clone + 'static,
357    Id: PartialEq + Clone + 'static,
358    H: Clone + View,
359    K: Clone + PartialEq + Send + Sync + 'static,
360{
361    fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self {
362        let sort_state = sort_state.to_signal(self.cx);
363        self.bind(sort_state, move |handle| {
364            let sort_state = sort_state.get();
365            handle.modify(|table: &mut VirtualTable<T, V, Id, H, K>| {
366                table.sort_state.set(sort_state)
367            });
368        })
369    }
370
371    fn resizable_columns<U: Into<bool> + Clone + 'static>(
372        self,
373        flag: impl Res<U> + 'static,
374    ) -> Self {
375        let flag = flag.to_signal(self.cx);
376        self.bind(flag, move |handle| {
377            let flag = flag.get().into();
378            handle.modify(|table: &mut VirtualTable<T, V, Id, H, K>| {
379                table.resizable_columns.set(flag)
380            });
381        })
382    }
383
384    fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
385        self,
386        cycle: impl Res<U> + 'static,
387    ) -> Self {
388        let cycle = cycle.to_signal(self.cx);
389        self.bind(cycle, move |handle| {
390            let cycle = cycle.get().into();
391            handle.modify(|table: &mut VirtualTable<T, V, Id, H, K>| table.sort_cycle.set(cycle));
392        })
393    }
394
395    fn selectable<U: Into<Selectable> + Clone + 'static>(
396        self,
397        selectable: impl Res<U> + 'static,
398    ) -> Self {
399        let selectable = selectable.to_signal(self.cx);
400        self.bind(selectable, move |handle| {
401            let selectable = selectable.get().into();
402            handle.modify(|table: &mut VirtualTable<T, V, Id, H, K>| {
403                table.selectable.set(selectable)
404            });
405        })
406    }
407
408    fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
409        self,
410        flag: impl Res<U> + 'static,
411    ) -> Self {
412        let flag = flag.to_signal(self.cx);
413        self.bind(flag, move |handle| {
414            let flag = flag.get().into();
415            handle.modify(|table: &mut VirtualTable<T, V, Id, H, K>| {
416                table.selection_follows_focus.set(flag)
417            });
418        })
419    }
420
421    fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
422    where
423        R: Deref<Target = [Id]> + Clone + 'static,
424    {
425        let selected_row_ids = selected_row_ids.to_signal(self.cx);
426        self.bind(selected_row_ids, move |handle| {
427            let ids = selected_row_ids.with(|ids| ids.deref().to_vec());
428            handle
429                .modify(|table: &mut VirtualTable<T, V, Id, H, K>| table.selected_row_ids.set(ids));
430        })
431    }
432
433    fn on_sort<F>(self, callback: F) -> Self
434    where
435        F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync,
436    {
437        self.modify(|table: &mut VirtualTable<T, V, Id, H, K>| {
438            table.on_sort = Some(Arc::new(callback))
439        })
440    }
441
442    fn on_row_select<F>(self, callback: F) -> Self
443    where
444        F: 'static + Fn(&mut EventContext, Id),
445    {
446        self.modify(|table: &mut VirtualTable<T, V, Id, H, K>| {
447            table.on_row_select = Some(Box::new(callback))
448        })
449    }
450}