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}