Skip to main content

sequoia_view/
table.rs

1use std::cmp::Ordering;
2use std::rc::Rc;
3
4use sequoia_core::Length;
5use sequoia_layout::LayoutStyle;
6use sequoia_reactive::Signal;
7use sequoia_style::TableStyle;
8
9use crate::any::AnyView;
10use crate::scroll_view::ScrollbarVisibility;
11use crate::view::View;
12use crate::views::{Column, Row, Text, Unit};
13use crate::virtual_list::VirtualList;
14
15#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16pub enum SortDir {
17    #[default]
18    None,
19    Asc,
20    Desc,
21}
22
23impl SortDir {
24    pub fn next(self) -> Self {
25        match self {
26            SortDir::None => SortDir::Asc,
27            SortDir::Asc => SortDir::Desc,
28            SortDir::Desc => SortDir::None,
29        }
30    }
31
32    pub fn arrow(self) -> &'static str {
33        match self {
34            SortDir::None => "",
35            SortDir::Asc => " ▲",
36            SortDir::Desc => " ▼",
37        }
38    }
39}
40
41type SortKeyFn<R> = Rc<dyn Fn(&R, &R) -> Ordering>;
42type MatchesFn<R> = Rc<dyn Fn(&R, &str) -> bool>;
43type CellFn<R> = Rc<dyn Fn(&R) -> AnyView>;
44
45pub struct TableColumn<R> {
46    pub header: String,
47    pub width: Length,
48    pub sortable: bool,
49    pub sort_key: Option<SortKeyFn<R>>,
50    pub filter: Signal<String>,
51    pub filter_enabled: bool,
52    pub matches: MatchesFn<R>,
53    pub cell: CellFn<R>,
54}
55
56impl<R: 'static> Clone for TableColumn<R> {
57    fn clone(&self) -> Self {
58        Self {
59            header: self.header.clone(),
60            width: self.width,
61            sortable: self.sortable,
62            sort_key: self.sort_key.clone(),
63            filter: self.filter.clone(),
64            filter_enabled: self.filter_enabled,
65            matches: self.matches.clone(),
66            cell: self.cell.clone(),
67        }
68    }
69}
70
71impl<R: 'static> TableColumn<R> {
72    pub fn new(header: impl Into<String>) -> Self {
73        Self {
74            header: header.into(),
75            width: Length::Fill(1.0),
76            sortable: false,
77            sort_key: None,
78            filter: Signal::new(String::new()),
79            filter_enabled: false,
80            matches: Rc::new(|_, _| true),
81            cell: Rc::new(|_| AnyView::new(Unit)),
82        }
83    }
84
85    pub fn width(mut self, width: Length) -> Self {
86        self.width = width;
87        self
88    }
89
90    pub fn sortable(mut self) -> Self {
91        self.sortable = true;
92        self
93    }
94
95    pub fn sort_key(mut self, f: impl Fn(&R, &R) -> Ordering + 'static) -> Self {
96        self.sort_key = Some(Rc::new(f));
97        self.sortable = true;
98        self
99    }
100
101    pub fn filterable(mut self) -> Self {
102        self.filter_enabled = true;
103        self
104    }
105
106    pub fn matches(mut self, f: impl Fn(&R, &str) -> bool + 'static) -> Self {
107        self.matches = Rc::new(f);
108        self.filter_enabled = true;
109        self
110    }
111
112    pub fn cell<V: View + 'static>(mut self, f: impl Fn(&R) -> V + 'static) -> Self {
113        self.cell = Rc::new(move |r| AnyView::new(f(r)));
114        self
115    }
116
117    pub fn text_cell(mut self, f: impl Fn(&R) -> String + 'static) -> Self
118    where
119        R: Clone,
120    {
121        self.cell = Rc::new(move |r: &R| AnyView::new(Text::new(f(r))));
122        self
123    }
124}
125
126pub struct Table<R> {
127    rows: Rc<dyn Fn() -> Vec<R>>,
128    key: Rc<dyn Fn(&R) -> u64>,
129    columns: Vec<TableColumn<R>>,
130    sort: Signal<Option<(usize, SortDir)>>,
131    row_height: f32,
132    overscan: usize,
133    scrollbar: ScrollbarVisibility,
134    style: Option<TableStyle>,
135    label: String,
136}
137
138impl<R: Clone + 'static> Clone for Table<R> {
139    fn clone(&self) -> Self {
140        Self {
141            rows: self.rows.clone(),
142            key: self.key.clone(),
143            columns: self.columns.clone(),
144            sort: self.sort.clone(),
145            row_height: self.row_height,
146            overscan: self.overscan,
147            scrollbar: self.scrollbar,
148            style: self.style,
149            label: self.label.clone(),
150        }
151    }
152}
153
154impl<R: Clone + 'static> Table<R> {
155    pub fn new(rows: impl Fn() -> Vec<R> + 'static) -> Self {
156        Self {
157            rows: Rc::new(rows),
158            key: Rc::new(|_| 0),
159            columns: Vec::new(),
160            sort: Signal::new(None),
161            row_height: 28.0,
162            overscan: 4,
163            scrollbar: ScrollbarVisibility::Auto,
164            style: None,
165            label: String::new(),
166        }
167    }
168
169    pub fn label(mut self, label: impl Into<String>) -> Self {
170        self.label = label.into();
171        self
172    }
173
174    pub fn style(mut self, style: TableStyle) -> Self {
175        self.style = Some(style);
176        self
177    }
178
179    fn header_size(&self) -> f32 {
180        self.style.and_then(|s| s.header_size).unwrap_or(12.0)
181    }
182
183    fn header_color(&self) -> Option<sequoia_core::Color> {
184        self.style.and_then(|s| s.header_color)
185    }
186
187    fn stripe(&self, index: usize) -> Option<sequoia_core::Color> {
188        if index % 2 == 1 {
189            self.style.and_then(|s| s.row_stripe)
190        } else {
191            None
192        }
193    }
194
195    pub fn key(mut self, key: impl Fn(&R) -> u64 + 'static) -> Self {
196        self.key = Rc::new(key);
197        self
198    }
199
200    pub fn column(mut self, col: TableColumn<R>) -> Self {
201        self.columns.push(col);
202        self
203    }
204
205    pub fn row_height(mut self, h: f32) -> Self {
206        self.row_height = h.max(1.0);
207        self
208    }
209
210    pub fn overscan(mut self, n: usize) -> Self {
211        self.overscan = n;
212        self
213    }
214
215    pub fn scrollbar_visibility(mut self, v: ScrollbarVisibility) -> Self {
216        self.scrollbar = v;
217        self
218    }
219
220    pub fn sort_signal(&self) -> Signal<Option<(usize, SortDir)>> {
221        self.sort.clone()
222    }
223
224    pub fn column_filter(&self, index: usize) -> Option<Signal<String>> {
225        self.columns.get(index).map(|c| c.filter.clone())
226    }
227
228    pub fn columns_for_test(&self) -> Vec<TableColumn<R>> {
229        self.columns.clone()
230    }
231
232    pub fn filtered_sorted(&self) -> Vec<R> {
233        let mut out = (self.rows)();
234        out.retain(|r| {
235            self.columns.iter().all(|c| {
236                if !c.filter_enabled {
237                    return true;
238                }
239                let q = c.filter.get_untracked();
240                if q.is_empty() {
241                    return true;
242                }
243                (c.matches)(r, &q)
244            })
245        });
246        if let Some((idx, dir)) = self.sort.get_untracked()
247            && dir != SortDir::None
248            && let Some(col) = self.columns.get(idx)
249            && let Some(key) = &col.sort_key
250        {
251            match dir {
252                SortDir::Asc => out.sort_by(|a, b| key(a, b)),
253                SortDir::Desc => out.sort_by(|a, b| key(b, a)),
254                SortDir::None => {}
255            }
256        }
257        out
258    }
259
260    fn header(&self) -> AnyView {
261        let mut row = Row::new().gap(4.0);
262        for (i, col) in self.columns.iter().enumerate() {
263            let header = col.header.clone();
264            let sortable = col.sortable && col.sort_key.is_some();
265            let sort = self.sort.clone();
266            let dir = self
267                .sort
268                .with_untracked(|s| s.filter(|(j, _)| *j == i).map(|(_, d)| d))
269                .unwrap_or(SortDir::None);
270            let label = format!("{header}{}", dir.arrow());
271            let cell: AnyView = if sortable {
272                AnyView::new(HeaderCell {
273                    label,
274                    size: self.header_size(),
275                    color: self.header_color(),
276                    sort,
277                    index: i,
278                })
279            } else {
280                let mut txt = Text::new(label).size(self.header_size()).bold();
281                if let Some(color) = self.header_color() {
282                    txt = txt.color(color);
283                }
284                AnyView::new(txt)
285            };
286            row = match col.width {
287                Length::Fill(w) => row.child_flex(cell, w.max(0.0)),
288                _ => row.child(cell),
289            };
290        }
291        AnyView::new(row)
292    }
293
294    fn list(&self) -> AnyView {
295        let me = self.clone();
296        let list = VirtualList::new(move || me.filtered_sorted())
297            .key({
298                let key = self.key.clone();
299                move |r: &R| key(r)
300            })
301            .item_indexed({
302                let columns = self.columns.clone();
303                let stripe_of = self.clone();
304                move |index: usize, r: &R| {
305                    let mut row = Row::new().gap(4.0);
306                    for col in &columns {
307                        let cell = (col.cell)(r);
308                        row = match col.width {
309                            Length::Fill(w) => row.child_flex(cell, w.max(0.0)),
310                            _ => row.child(cell),
311                        };
312                    }
313                    match stripe_of.stripe(index) {
314                        Some(bg) => row.background(bg),
315                        None => row,
316                    }
317                }
318            })
319            .item_height(self.row_height)
320            .overscan(self.overscan)
321            .scrollbar_visibility(self.scrollbar);
322        AnyView::new(list)
323    }
324}
325
326impl<R: Clone + 'static> View for Table<R> {
327    fn body(&self) -> impl View + 'static {
328        Unit
329    }
330
331    fn accessibility(&self) -> Option<crate::a11y::Accessible> {
332        Some(crate::a11y::Accessible::new(
333            crate::a11y::AccessibleRole::Table,
334            self.label.clone(),
335        ))
336    }
337
338    fn children_override(&self) -> Option<Vec<AnyView>> {
339        let _ = (self.rows)();
340        let _ = self.sort.get();
341        for col in &self.columns {
342            if col.filter_enabled {
343                let _ = col.filter.get();
344            }
345        }
346        Some(vec![self.header(), self.list()])
347    }
348
349    fn layout_style(&self) -> LayoutStyle {
350        LayoutStyle {
351            width: Length::Fill(1.0),
352            height: Length::Fill(1.0),
353            ..LayoutStyle::default()
354        }
355    }
356}
357
358#[derive(Clone)]
359struct HeaderCell {
360    label: String,
361    size: f32,
362    color: Option<sequoia_core::Color>,
363    sort: Signal<Option<(usize, SortDir)>>,
364    index: usize,
365}
366
367impl View for HeaderCell {
368    fn body(&self) -> impl View + 'static {
369        Unit
370    }
371
372    fn children_override(&self) -> Option<Vec<AnyView>> {
373        let mut txt = Text::new(self.label.clone()).size(self.size).bold();
374        if let Some(color) = self.color {
375            txt = txt.color(color);
376        }
377        Some(vec![AnyView::new(txt)])
378    }
379
380    fn layout_style(&self) -> LayoutStyle {
381        LayoutStyle::default()
382    }
383
384    fn has_visual_state(&self) -> bool {
385        true
386    }
387
388    fn cursor_icon(&self, _state: sequoia_style::VisualState) -> sequoia_input::CursorIcon {
389        sequoia_input::CursorIcon::Pointer
390    }
391
392    fn on_event(&self, event: &sequoia_input::Event, ctx: &mut crate::view::EventCtx) {
393        use sequoia_input::{Event, PointerButton, PointerEvent};
394        match event {
395            Event::Pointer(PointerEvent::Up {
396                button: PointerButton::Left,
397                pos,
398                ..
399            }) if ctx.rect.contains(*pos) => {
400                let i = self.index;
401                self.sort.update(|s| {
402                    let next = match s {
403                        Some((j, d)) if *j == i => d.next(),
404                        _ => SortDir::Asc,
405                    };
406                    *s = if next == SortDir::None {
407                        None
408                    } else {
409                        Some((i, next))
410                    };
411                });
412                ctx.handled();
413            }
414            _ => {}
415        }
416    }
417}
418
419pub fn table_filters<R: Clone + 'static>(table: &Table<R>) -> AnyView {
420    use crate::views::Unit as U;
421    let mut row = Row::new().gap(4.0);
422    let mut any = false;
423    for col in &table.columns {
424        if col.filter_enabled {
425            any = true;
426            let filter = col.filter.clone();
427
428            let txt = Text::with(move || {
429                let q = filter.get();
430                if q.is_empty() {
431                    String::new()
432                } else {
433                    format!("⊃ {q}")
434                }
435            })
436            .size(11.0);
437            row = match col.width {
438                Length::Fill(w) => row.child_flex(txt, w.max(0.0)),
439                _ => row.child(txt),
440            };
441        }
442    }
443    if any {
444        AnyView::new(row)
445    } else {
446        AnyView::new(U)
447    }
448}
449
450#[allow(dead_code)]
451fn _uses_column() {
452    let _ = Column::new();
453}