1use std::{ops::Deref, rc::Rc, sync::Arc};
2
3use crate::{
4 icons::{ICON_ARROWS_SORT, ICON_SORT_ASCENDING, ICON_SORT_DESCENDING},
5 prelude::*,
6};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum TableSortDirection {
11 None,
13 Ascending,
15 Descending,
17}
18
19impl_res_simple!(TableSortDirection);
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum TableSortCycle {
24 BiState,
26 TriState,
28}
29
30impl_res_simple!(TableSortCycle);
31
32pub(super) fn sort_direction_for_column<K: PartialEq>(
33 sort_state: Option<&TableSortState<K>>,
34 column_key: &K,
35) -> TableSortDirection {
36 match sort_state {
37 Some(state) if &state.key == column_key => state.direction,
38 _ => TableSortDirection::None,
39 }
40}
41
42pub(super) fn next_sort_direction(
43 sort_cycle: TableSortCycle,
44 current_direction: TableSortDirection,
45) -> TableSortDirection {
46 match (sort_cycle, current_direction) {
47 (TableSortCycle::BiState, TableSortDirection::Ascending) => TableSortDirection::Descending,
48 (TableSortCycle::BiState, _) => TableSortDirection::Ascending,
49 (TableSortCycle::TriState, TableSortDirection::None) => TableSortDirection::Ascending,
50 (TableSortCycle::TriState, TableSortDirection::Ascending) => TableSortDirection::Descending,
51 (TableSortCycle::TriState, TableSortDirection::Descending) => TableSortDirection::None,
52 }
53}
54
55type TableHeaderContent<S> = dyn Fn(&mut Context, Memo<TableSortDirection>) -> Handle<S>;
56type TableCellContent<T> = dyn Fn(&mut Context, Memo<T>);
57
58impl<T: PartialEq + 'static, S: View, K: Clone + PartialEq + Send + Sync + 'static>
59 Res<Vec<TableColumn<T, S, K>>> for Vec<TableColumn<T, S, K>>
60{
61 fn get_value(&self, _: &impl DataContext) -> Vec<TableColumn<T, S, K>> {
62 self.clone()
63 }
64}
65
66#[derive(Clone)]
68pub struct TableHeader<K> {
69 #[allow(dead_code)]
70 key: K,
71}
72
73impl<K> TableHeader<K>
74where
75 K: Eq + std::hash::Hash + Clone + PartialEq + Send + Sync + 'static,
76{
77 pub fn new(
78 cx: &mut Context,
79 key: impl Into<K>,
80 title: impl Into<String>,
81 sort_direction: Memo<TableSortDirection>,
82 ) -> Handle<'_, TableHeader<K>> {
83 let key = key.into();
84 Self { key: key.clone() }
85 .build(cx, move |cx| {
86 let key = key.clone();
87 let title = title.into();
88 Label::new(cx, title)
89 .class("table-header-title")
90 .pointer_events(PointerEvents::None)
91 .width(Stretch(1.0))
92 .min_width(Auto);
93 let sort_indicator = Memo::new(move |_| match sort_direction.get() {
94 TableSortDirection::Ascending => ICON_SORT_ASCENDING,
95 TableSortDirection::Descending => ICON_SORT_DESCENDING,
96 TableSortDirection::None => ICON_ARROWS_SORT,
97 });
98 Button::new(cx, move |cx| {
99 Svg::new(cx, sort_indicator).class("table-sort-indicator")
100 })
101 .variant(ButtonVariant::Text)
102 .on_press(move |cx| cx.emit(TableEvent::<K>::ToggleSort(key.clone())));
103 })
104 .navigable(true)
105 .layout_type(LayoutType::Row)
106 .alignment(Alignment::Left)
107 .padding_left(Pixels(8.0))
108 .padding_right(Pixels(8.0))
109 .width(Stretch(1.0))
110 .min_width(Auto)
111 }
112}
113
114impl<K> View for TableHeader<K>
115where
116 K: Eq + std::hash::Hash + Clone + PartialEq + Send + Sync + 'static,
117{
118 fn element(&self) -> Option<&'static str> {
119 Some("table-header")
120 }
121}
122
123#[derive(Debug, Clone, PartialEq, Eq)]
125pub struct TableSortState<K = String> {
126 pub key: K,
128 pub direction: TableSortDirection,
130}
131
132pub struct TableColumn<T: PartialEq + 'static, S: View, K = String>
134where
135 K: Clone + PartialEq + Send + Sync + 'static,
136{
137 pub key: K,
139 pub width: Signal<f32>,
141 pub min_width: Signal<f32>,
143 pub sortable: Signal<bool>,
145 pub resizable: Signal<bool>,
147 pub hidden: Signal<bool>,
149 pub cell_content: Rc<TableCellContent<T>>,
151 pub header_content: Rc<TableHeaderContent<S>>,
153}
154
155impl<T: PartialEq + 'static, S: View, K: Clone + PartialEq + Send + Sync + 'static> Clone
156 for TableColumn<T, S, K>
157{
158 fn clone(&self) -> Self {
159 Self {
160 key: self.key.clone(),
161 width: self.width,
162 min_width: self.min_width,
163 sortable: self.sortable,
164 resizable: self.resizable,
165 hidden: self.hidden,
166 cell_content: self.cell_content.clone(),
167 header_content: self.header_content.clone(),
168 }
169 }
170}
171
172impl<T: PartialEq + 'static, S: View, K: Clone + PartialEq + Send + Sync + 'static>
173 TableColumn<T, S, K>
174{
175 pub fn new(
192 key: impl Into<K>,
193 header_content: impl Fn(&mut Context, Memo<TableSortDirection>) -> Handle<S> + 'static,
194 cell_content: impl Fn(&mut Context, Memo<T>) + 'static,
195 ) -> Self {
196 Self {
197 key: key.into(),
198 width: Signal::new(180.0),
199 min_width: Signal::new(80.0),
200 sortable: Signal::new(true),
201 resizable: Signal::new(false),
202 hidden: Signal::new(false),
203 cell_content: Rc::new(cell_content),
204 header_content: Rc::new(header_content),
205 }
206 }
207
208 pub fn width(self, width: f32) -> Self {
210 self.width.set(width.max(self.min_width.get_untracked()));
211 self
212 }
213
214 pub fn min_width(self, min_width: f32) -> Self {
216 self.min_width.set(min_width);
217 self.width.set(self.width.get_untracked().max(min_width));
218 self
219 }
220
221 pub fn sortable(self, sortable: bool) -> Self {
223 self.sortable.set(sortable);
224 self
225 }
226
227 pub fn resizable(self, resizable: bool) -> Self {
229 self.resizable.set(resizable);
230 self
231 }
232
233 pub fn hidden(self, hidden: bool) -> Self {
235 self.hidden.set(hidden);
236 self
237 }
238
239 pub fn hidden_res<U: Into<bool> + Clone + 'static>(
241 self,
242 cx: &mut Context,
243 hidden: impl Res<U> + 'static,
244 ) -> Self {
245 let hidden_signal = self.hidden;
246 hidden.set_or_bind(cx, move |cx, res| {
247 hidden_signal.set(res.get_value(cx).into());
248 });
249 self
250 }
251}
252
253pub struct Table<T, V, Id, K = String>
258where
259 V: Deref<Target = [T]> + Clone + 'static,
260 T: PartialEq + Clone + 'static,
261 Id: PartialEq + Clone + 'static,
262 K: Clone + PartialEq + Send + Sync + 'static,
263{
264 rows: Signal<V>,
265 row_id: Rc<dyn Fn(&T) -> Id>,
266 sort_state: Signal<Option<TableSortState<K>>>,
267 sort_cycle: Signal<TableSortCycle>,
268 resizable_columns: Signal<bool>,
269 selectable: Signal<Selectable>,
270 selection_follows_focus: Signal<bool>,
271 selected_row_ids: Signal<Vec<Id>>,
272 on_sort: Option<Arc<dyn Fn(&mut EventContext, K, TableSortDirection) + Send + Sync>>,
273 on_row_select: Option<Box<dyn Fn(&mut EventContext, Id)>>,
274}
275
276pub enum TableEvent<K> {
277 RequestSort(K, TableSortDirection),
278 ToggleSort(K),
279 SelectRow(usize),
280}
281
282impl<T, V, Id, K> Table<T, V, Id, K>
283where
284 V: Deref<Target = [T]> + Clone + 'static,
285 T: PartialEq + Clone + 'static,
286 Id: PartialEq + Clone + 'static,
287 K: Clone + PartialEq + Send + Sync + 'static,
288{
289 pub fn new<S, C, R, H>(
308 cx: &mut Context,
309 rows: S,
310 columns: C,
311 row_id: impl Fn(&T) -> Id + 'static,
312 ) -> Handle<Self>
313 where
314 S: Res<V> + 'static,
315 C: Res<R> + 'static,
316 R: Deref<Target = [TableColumn<T, H, K>]> + Clone + 'static,
317 H: Clone + View,
318 {
319 let row_signal = rows.to_signal(cx);
320 let column_signal = columns.to_signal(cx);
321 let row_id: Rc<dyn Fn(&T) -> Id> = Rc::new(row_id);
322 let sort_state = Signal::new(None);
323 let sort_cycle = Signal::new(TableSortCycle::BiState);
324 let resizable_columns = Signal::new(false);
325 let selectable = Signal::new(Selectable::None);
326 let selection_follows_focus = Signal::new(false);
327 let selected_row_ids = Signal::new(Vec::new());
328 let selected_indices = Memo::new({
329 let row_id = row_id.clone();
330 move |_| {
331 row_signal.with(|rows| {
332 selected_row_ids.with(|selected_ids| {
333 rows.deref()
334 .iter()
335 .enumerate()
336 .filter_map(|(index, row)| {
337 let id = (row_id)(row);
338 if selected_ids.contains(&id) { Some(index) } else { None }
339 })
340 .collect::<Vec<usize>>()
341 })
342 })
343 }
344 });
345
346 let column_layout = Memo::new(move |_| {
347 column_signal.with(|columns| {
348 columns
349 .deref()
350 .iter()
351 .map(|column| (column.key.clone(), column.hidden.get()))
352 .collect::<Vec<_>>()
353 })
354 });
355
356 Self {
357 rows: row_signal,
358 row_id,
359 sort_state,
360 sort_cycle,
361 resizable_columns,
362 selectable,
363 selection_follows_focus,
364 selected_row_ids,
365 on_sort: None,
366 on_row_select: None,
367 }
368 .build(cx, move |cx| {
369 Binding::new(cx, column_layout, move |cx| {
370 let visible_columns = column_signal.with(|columns| {
371 columns
372 .deref()
373 .iter()
374 .filter(|column| !column.hidden.get())
375 .cloned()
376 .collect::<Vec<_>>()
377 });
378 let last_header_index = visible_columns.len().saturating_sub(1);
379
380 let header_columns = Rc::new(visible_columns);
381 let body_columns = header_columns.clone();
382
383 HStack::new(cx, move |cx| {
384 for (column_index, column) in header_columns.iter().cloned().enumerate() {
385 let width_signal = column.width;
386 let sort_state = sort_state;
387 let sort_cycle = sort_cycle;
388 let resizable_columns = resizable_columns;
389 let min_width = column.min_width;
390 let sortable = column.sortable;
391 let resizable = column.resizable;
392 let is_last_column = column_index == last_header_index;
393 let header_content = column.header_content.clone();
394 let column_key = column.key.clone();
395 let sort_direction = sort_state.map({
396 let column_key = column_key.clone();
397 move |state| sort_direction_for_column(state.as_ref(), &column_key)
398 });
399
400 if is_last_column {
401 HStack::new(cx, move |cx| {
402 let header = header_content(cx, sort_direction);
403
404 let column_key = column_key.clone();
405 header.on_press(move |cx| {
406 if sortable.get() {
407 let current_direction = sort_direction_for_column(
408 sort_state.get().as_ref(),
409 &column_key,
410 );
411 let next_direction = next_sort_direction(
412 sort_cycle.get(),
413 current_direction,
414 );
415 cx.emit(TableEvent::RequestSort(
416 column_key.clone(),
417 next_direction,
418 ));
419 }
420 });
421 })
422 .class("table-header-cell")
423 .toggle_class("sortable", sortable)
424 .toggle_class("resizable", false)
425 .width(Stretch(1.0))
426 .min_width(Auto);
427 } else {
428 Resizable::new(
429 cx,
430 width_signal.map(|value| Pixels(*value)),
431 ResizeStackDirection::Right,
432 move |_cx, new_size| {
433 if resizable_columns.get() && resizable.get() {
434 width_signal.set(new_size.max(min_width.get()));
435 }
436 },
437 move |cx| {
438 let header = header_content(cx, sort_direction);
439
440 let column_key = column_key.clone();
441 header.on_press(move |cx| {
442 if sortable.get() {
443 let current_direction = sort_direction_for_column(
444 sort_state.get().as_ref(),
445 &column_key,
446 );
447 let next_direction = next_sort_direction(
448 sort_cycle.get(),
449 current_direction,
450 );
451 cx.emit(TableEvent::RequestSort(
452 column_key.clone(),
453 next_direction,
454 ));
455 }
456 });
457 },
458 )
459 .class("table-header-cell")
460 .toggle_class("sortable", sortable)
461 .toggle_class(
462 "resizable",
463 resizable_columns.map(move |enabled| *enabled && resizable.get()),
464 )
465 .min_width(min_width.map(|value| Pixels(*value)));
466 }
467 }
468 })
469 .class("table-header-row")
470 .height(Auto)
471 .width(Stretch(1.0))
472 .min_width(Auto);
473
474 List::new(cx, row_signal, move |cx, row_index, row| {
475 HStack::new(cx, |cx| {
476 for (column_index, column) in body_columns.iter().enumerate() {
477 let width_signal = column.width;
478 let min_width = column.min_width;
479 let cell_content = column.cell_content.clone();
480 let is_last_column = column_index + 1 == body_columns.len();
481
482 if is_last_column {
483 VStack::new(cx, move |cx| {
484 cell_content(cx, row.map(|value| value.clone()));
485 })
486 .class("table-cell")
487 .width(Stretch(1.0))
488 .min_width(Auto)
489 .height(Auto);
490 } else {
491 VStack::new(cx, move |cx| {
492 cell_content(cx, row.map(|value| value.clone()));
493 })
494 .class("table-cell")
495 .width(width_signal.map(|value| Pixels(*value)))
496 .min_width(min_width.map(|value| Pixels(*value)))
497 .height(Auto);
498 }
499 }
500 })
501 .class("table-row")
502 .toggle_class("odd", row_index % 2 == 1)
503 .toggle_class("even", row_index % 2 == 0)
504 .alignment(Alignment::Left)
505 .height(Auto)
506 .width(Stretch(1.0))
507 .min_width(Auto);
508 })
509 .width(Stretch(1.0))
510 .min_width(Auto)
511 .height(Stretch(1.0))
512 .min_height(Auto)
513 .class("table-body")
514 .selection(selected_indices)
515 .selectable(selectable)
516 .selection_follows_focus(selection_follows_focus)
517 .on_select(move |cx, index| cx.emit(TableEvent::<K>::SelectRow(index)));
518 });
519 })
520 }
521}
522
523impl<T, V, Id, K> View for Table<T, V, Id, K>
524where
525 V: Deref<Target = [T]> + Clone + 'static,
526 T: PartialEq + Clone + 'static,
527 Id: PartialEq + Clone + 'static,
528 K: Clone + PartialEq + Send + Sync + 'static,
529{
530 fn element(&self) -> Option<&'static str> {
531 Some("table")
532 }
533
534 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
535 event.map(|table_event: &TableEvent<K>, _| match table_event {
536 TableEvent::RequestSort(key, direction) => {
537 if let Some(callback) = &self.on_sort {
538 (callback)(cx, key.clone(), *direction);
539 }
540 }
541
542 TableEvent::SelectRow(index) => {
543 let rows = self.rows.get();
544 if let Some(row) = rows.deref().get(*index) {
545 if let Some(callback) = &self.on_row_select {
546 (callback)(cx, (self.row_id)(row));
547 }
548 }
549 }
550
551 TableEvent::ToggleSort(key) => {
552 if let Some(callback) = &self.on_sort {
553 let current_direction =
554 sort_direction_for_column(self.sort_state.get().as_ref(), key);
555 let next_direction =
556 next_sort_direction(self.sort_cycle.get(), current_direction);
557 (callback)(cx, key.clone(), next_direction);
558 }
559 }
560 });
561 }
562}
563
564pub trait TableModifiers<Id, K = String>: Sized
566where
567 K: Clone + PartialEq + Send + Sync + 'static,
568{
569 fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self;
571
572 fn resizable_columns<U: Into<bool> + Clone + 'static>(
574 self,
575 flag: impl Res<U> + 'static,
576 ) -> Self;
577
578 fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
580 self,
581 cycle: impl Res<U> + 'static,
582 ) -> Self;
583
584 fn selectable<U: Into<Selectable> + Clone + 'static>(
586 self,
587 selectable: impl Res<U> + 'static,
588 ) -> Self;
589
590 fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
592 self,
593 flag: impl Res<U> + 'static,
594 ) -> Self;
595
596 fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
598 where
599 R: Deref<Target = [Id]> + Clone + 'static;
600
601 fn on_sort<F>(self, callback: F) -> Self
603 where
604 F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync;
605
606 fn on_row_select<F>(self, callback: F) -> Self
608 where
609 F: 'static + Fn(&mut EventContext, Id);
610}
611
612impl<T, V, Id, K> TableModifiers<Id, K> for Handle<'_, Table<T, V, Id, K>>
613where
614 V: Deref<Target = [T]> + Clone + 'static,
615 T: PartialEq + Clone + 'static,
616 Id: PartialEq + Clone + 'static,
617 K: Clone + PartialEq + Send + Sync + 'static,
618{
619 fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self {
620 let sort_state = sort_state.to_signal(self.cx);
621 self.bind(sort_state, move |handle| {
622 let sort_state = sort_state.get();
623 handle.modify(|table: &mut Table<T, V, Id, K>| table.sort_state.set(sort_state));
624 })
625 }
626
627 fn resizable_columns<U: Into<bool> + Clone + 'static>(
628 self,
629 flag: impl Res<U> + 'static,
630 ) -> Self {
631 let flag = flag.to_signal(self.cx);
632 self.bind(flag, move |handle| {
633 let flag = flag.get().into();
634 handle.modify(|table: &mut Table<T, V, Id, K>| table.resizable_columns.set(flag));
635 })
636 }
637
638 fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
639 self,
640 cycle: impl Res<U> + 'static,
641 ) -> Self {
642 let cycle = cycle.to_signal(self.cx);
643 self.bind(cycle, move |handle| {
644 let cycle = cycle.get().into();
645 handle.modify(|table: &mut Table<T, V, Id, K>| table.sort_cycle.set(cycle));
646 })
647 }
648
649 fn selectable<U: Into<Selectable> + Clone + 'static>(
650 self,
651 selectable: impl Res<U> + 'static,
652 ) -> Self {
653 let selectable = selectable.to_signal(self.cx);
654 self.bind(selectable, move |handle| {
655 let selectable = selectable.get().into();
656 handle.modify(|table: &mut Table<T, V, Id, K>| table.selectable.set(selectable));
657 })
658 }
659
660 fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
661 self,
662 flag: impl Res<U> + 'static,
663 ) -> Self {
664 let flag = flag.to_signal(self.cx);
665 self.bind(flag, move |handle| {
666 let flag = flag.get().into();
667 handle.modify(|table: &mut Table<T, V, Id, K>| table.selection_follows_focus.set(flag));
668 })
669 }
670
671 fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
672 where
673 R: Deref<Target = [Id]> + Clone + 'static,
674 {
675 let selected_row_ids = selected_row_ids.to_signal(self.cx);
676 self.bind(selected_row_ids, move |handle| {
677 let ids = selected_row_ids.with(|ids| ids.deref().to_vec());
678 handle.modify(|table: &mut Table<T, V, Id, K>| table.selected_row_ids.set(ids));
679 })
680 }
681
682 fn on_sort<F>(self, callback: F) -> Self
683 where
684 F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync,
685 {
686 self.modify(|table: &mut Table<T, V, Id, K>| table.on_sort = Some(Arc::new(callback)))
687 }
688
689 fn on_row_select<F>(self, callback: F) -> Self
690 where
691 F: 'static + Fn(&mut EventContext, Id),
692 {
693 self.modify(|table: &mut Table<T, V, Id, K>| table.on_row_select = Some(Box::new(callback)))
694 }
695}