1use std::{ops::Deref, rc::Rc, sync::Arc};
2
3use crate::prelude::*;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum TableSortDirection {
8 None,
10 Ascending,
12 Descending,
14}
15
16impl_res_simple!(TableSortDirection);
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum TableSortCycle {
21 BiState,
23 TriState,
25}
26
27impl_res_simple!(TableSortCycle);
28
29pub(super) fn sort_direction_for_column<K: PartialEq>(
30 sort_state: Option<&TableSortState<K>>,
31 column_key: &K,
32) -> TableSortDirection {
33 match sort_state {
34 Some(state) if &state.key == column_key => state.direction,
35 _ => TableSortDirection::None,
36 }
37}
38
39pub(super) fn next_sort_direction(
40 sort_cycle: TableSortCycle,
41 current_direction: TableSortDirection,
42) -> TableSortDirection {
43 match (sort_cycle, current_direction) {
44 (TableSortCycle::BiState, TableSortDirection::Ascending) => TableSortDirection::Descending,
45 (TableSortCycle::BiState, _) => TableSortDirection::Ascending,
46 (TableSortCycle::TriState, TableSortDirection::None) => TableSortDirection::Ascending,
47 (TableSortCycle::TriState, TableSortDirection::Ascending) => TableSortDirection::Descending,
48 (TableSortCycle::TriState, TableSortDirection::Descending) => TableSortDirection::None,
49 }
50}
51
52type TableHeaderContent<S> = dyn Fn(&mut Context, Memo<TableSortDirection>) -> Handle<S>;
53type TableCellContent<T> = dyn Fn(&mut Context, Memo<T>);
54
55impl<T: PartialEq + 'static, S: View, K: Clone + PartialEq + Send + Sync + 'static>
56 Res<Vec<TableColumn<T, S, K>>> for Vec<TableColumn<T, S, K>>
57{
58 fn get_value(&self, _: &impl DataContext) -> Vec<TableColumn<T, S, K>> {
59 self.clone()
60 }
61}
62
63#[derive(Clone)]
65pub struct TableHeader;
66
67impl TableHeader {
68 pub fn new(
69 cx: &mut Context,
70 title: impl Into<String>,
71 sort_direction: Memo<TableSortDirection>,
72 ) -> Handle<'_, TableHeader> {
73 Self.build(cx, move |cx| {
74 let title = title.into();
75 Label::new(cx, title).class("table-header-title").width(Stretch(1.0)).min_width(Auto);
76 let sort_indicator = Memo::new(move |_| match sort_direction.get() {
77 TableSortDirection::Ascending => "^".to_string(),
78 TableSortDirection::Descending => "v".to_string(),
79 TableSortDirection::None => "ยท".to_string(),
80 });
81
82 Label::new(cx, sort_indicator).class("table-sort-indicator").text_wrap(false);
83 })
84 .layout_type(LayoutType::Row)
85 .width(Stretch(1.0))
86 .min_width(Auto)
87 }
88}
89
90impl View for TableHeader {
91 fn element(&self) -> Option<&'static str> {
92 Some("table-header")
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct TableSortState<K = String> {
99 pub key: K,
101 pub direction: TableSortDirection,
103}
104
105pub struct TableColumn<T: PartialEq + 'static, S: View, K = String>
107where
108 K: Clone + PartialEq + Send + Sync + 'static,
109{
110 pub key: K,
112 pub width: Signal<f32>,
114 pub min_width: Signal<f32>,
116 pub sortable: Signal<bool>,
118 pub resizable: Signal<bool>,
120 pub hidden: Signal<bool>,
122 pub cell_content: Rc<TableCellContent<T>>,
124 pub header_content: Rc<TableHeaderContent<S>>,
126}
127
128impl<T: PartialEq + 'static, S: View, K: Clone + PartialEq + Send + Sync + 'static> Clone
129 for TableColumn<T, S, K>
130{
131 fn clone(&self) -> Self {
132 Self {
133 key: self.key.clone(),
134 width: self.width,
135 min_width: self.min_width,
136 sortable: self.sortable,
137 resizable: self.resizable,
138 hidden: self.hidden,
139 cell_content: self.cell_content.clone(),
140 header_content: self.header_content.clone(),
141 }
142 }
143}
144
145impl<T: PartialEq + 'static, S: View, K: Clone + PartialEq + Send + Sync + 'static>
146 TableColumn<T, S, K>
147{
148 pub fn new(
165 key: impl Into<K>,
166 header_content: impl Fn(&mut Context, Memo<TableSortDirection>) -> Handle<S> + 'static,
167 cell_content: impl Fn(&mut Context, Memo<T>) + 'static,
168 ) -> Self {
169 Self {
170 key: key.into(),
171 width: Signal::new(180.0),
172 min_width: Signal::new(80.0),
173 sortable: Signal::new(true),
174 resizable: Signal::new(false),
175 hidden: Signal::new(false),
176 cell_content: Rc::new(cell_content),
177 header_content: Rc::new(header_content),
178 }
179 }
180
181 pub fn width(self, width: f32) -> Self {
183 self.width.set(width.max(self.min_width.get_untracked()));
184 self
185 }
186
187 pub fn min_width(self, min_width: f32) -> Self {
189 self.min_width.set(min_width);
190 self.width.set(self.width.get_untracked().max(min_width));
191 self
192 }
193
194 pub fn sortable(self, sortable: bool) -> Self {
196 self.sortable.set(sortable);
197 self
198 }
199
200 pub fn resizable(self, resizable: bool) -> Self {
202 self.resizable.set(resizable);
203 self
204 }
205
206 pub fn hidden(self, hidden: bool) -> Self {
208 self.hidden.set(hidden);
209 self
210 }
211
212 pub fn hidden_res<U: Into<bool> + Clone + 'static>(
214 self,
215 cx: &mut Context,
216 hidden: impl Res<U> + 'static,
217 ) -> Self {
218 let hidden_signal = self.hidden;
219 hidden.set_or_bind(cx, move |cx, res| {
220 hidden_signal.set(res.get_value(cx).into());
221 });
222 self
223 }
224}
225
226pub struct Table<T, V, Id, K = String>
231where
232 V: Deref<Target = [T]> + Clone + 'static,
233 T: PartialEq + Clone + 'static,
234 Id: PartialEq + Clone + 'static,
235 K: Clone + PartialEq + Send + Sync + 'static,
236{
237 rows: Signal<V>,
238 row_id: Rc<dyn Fn(&T) -> Id>,
239 sort_state: Signal<Option<TableSortState<K>>>,
240 sort_cycle: Signal<TableSortCycle>,
241 resizable_columns: Signal<bool>,
242 selectable: Signal<Selectable>,
243 selection_follows_focus: Signal<bool>,
244 selected_row_ids: Signal<Vec<Id>>,
245 on_sort: Option<Arc<dyn Fn(&mut EventContext, K, TableSortDirection) + Send + Sync>>,
246 on_row_select: Option<Box<dyn Fn(&mut EventContext, Id)>>,
247}
248
249enum TableEvent<K> {
250 RequestSort(K, TableSortDirection),
251 SelectRow(usize),
252}
253
254impl<T, V, Id, K> Table<T, V, Id, K>
255where
256 V: Deref<Target = [T]> + Clone + 'static,
257 T: PartialEq + Clone + 'static,
258 Id: PartialEq + Clone + 'static,
259 K: Clone + PartialEq + Send + Sync + 'static,
260{
261 pub fn new<S, C, R, H>(
280 cx: &mut Context,
281 rows: S,
282 columns: C,
283 row_id: impl Fn(&T) -> Id + 'static,
284 ) -> Handle<Self>
285 where
286 S: Res<V> + 'static,
287 C: Res<R> + 'static,
288 R: Deref<Target = [TableColumn<T, H, K>]> + Clone + 'static,
289 H: Clone + View,
290 {
291 let row_signal = rows.to_signal(cx);
292 let column_signal = columns.to_signal(cx);
293 let row_id: Rc<dyn Fn(&T) -> Id> = Rc::new(row_id);
294 let sort_state = Signal::new(None);
295 let sort_cycle = Signal::new(TableSortCycle::BiState);
296 let resizable_columns = Signal::new(false);
297 let selectable = Signal::new(Selectable::None);
298 let selection_follows_focus = Signal::new(false);
299 let selected_row_ids = Signal::new(Vec::new());
300 let selected_indices = Memo::new({
301 let row_id = row_id.clone();
302 move |_| {
303 row_signal.with(|rows| {
304 selected_row_ids.with(|selected_ids| {
305 rows.deref()
306 .iter()
307 .enumerate()
308 .filter_map(|(index, row)| {
309 let id = (row_id)(row);
310 if selected_ids.contains(&id) { Some(index) } else { None }
311 })
312 .collect::<Vec<usize>>()
313 })
314 })
315 }
316 });
317
318 let column_layout = Memo::new(move |_| {
319 column_signal.with(|columns| {
320 columns
321 .deref()
322 .iter()
323 .map(|column| (column.key.clone(), column.hidden.get()))
324 .collect::<Vec<_>>()
325 })
326 });
327
328 Self {
329 rows: row_signal,
330 row_id,
331 sort_state,
332 sort_cycle,
333 resizable_columns,
334 selectable,
335 selection_follows_focus,
336 selected_row_ids,
337 on_sort: None,
338 on_row_select: None,
339 }
340 .build(cx, move |cx| {
341 Binding::new(cx, column_layout, move |cx| {
342 let visible_columns = column_signal.with(|columns| {
343 columns
344 .deref()
345 .iter()
346 .filter(|column| !column.hidden.get())
347 .cloned()
348 .collect::<Vec<_>>()
349 });
350 let last_header_index = visible_columns.len().saturating_sub(1);
351
352 let header_columns = Rc::new(visible_columns);
353 let body_columns = header_columns.clone();
354
355 HStack::new(cx, move |cx| {
356 for (column_index, column) in header_columns.iter().cloned().enumerate() {
357 let width_signal = column.width;
358 let sort_state = sort_state;
359 let sort_cycle = sort_cycle;
360 let resizable_columns = resizable_columns;
361 let min_width = column.min_width;
362 let sortable = column.sortable;
363 let resizable = column.resizable;
364 let is_last_column = column_index == last_header_index;
365 let header_content = column.header_content.clone();
366 let column_key = column.key.clone();
367 let sort_direction = sort_state.map({
368 let column_key = column_key.clone();
369 move |state| sort_direction_for_column(state.as_ref(), &column_key)
370 });
371
372 if is_last_column {
373 HStack::new(cx, move |cx| {
374 let header = header_content(cx, sort_direction);
375
376 let column_key = column_key.clone();
377 header.on_press(move |cx| {
378 if sortable.get() {
379 let current_direction = sort_direction_for_column(
380 sort_state.get().as_ref(),
381 &column_key,
382 );
383 let next_direction = next_sort_direction(
384 sort_cycle.get(),
385 current_direction,
386 );
387 cx.emit(TableEvent::RequestSort(
388 column_key.clone(),
389 next_direction,
390 ));
391 }
392 });
393 })
394 .class("table-header-cell")
395 .toggle_class("sortable", sortable)
396 .toggle_class("resizable", false)
397 .width(Stretch(1.0))
398 .min_width(Auto);
399 } else {
400 Resizable::new(
401 cx,
402 width_signal.map(|value| Pixels(*value)),
403 ResizeStackDirection::Right,
404 move |_cx, new_size| {
405 if resizable_columns.get() && resizable.get() {
406 width_signal.set(new_size.max(min_width.get()));
407 }
408 },
409 move |cx| {
410 let header = header_content(cx, sort_direction);
411
412 let column_key = column_key.clone();
413 header.on_press(move |cx| {
414 if sortable.get() {
415 let current_direction = sort_direction_for_column(
416 sort_state.get().as_ref(),
417 &column_key,
418 );
419 let next_direction = next_sort_direction(
420 sort_cycle.get(),
421 current_direction,
422 );
423 cx.emit(TableEvent::RequestSort(
424 column_key.clone(),
425 next_direction,
426 ));
427 }
428 });
429 },
430 )
431 .class("table-header-cell")
432 .toggle_class("sortable", sortable)
433 .toggle_class(
434 "resizable",
435 resizable_columns.map(move |enabled| *enabled && resizable.get()),
436 )
437 .min_width(min_width.map(|value| Pixels(*value)));
438 }
439 }
440 })
441 .class("table-header-row")
442 .height(Auto)
443 .width(Stretch(1.0))
444 .min_width(Auto);
445
446 List::new(cx, row_signal, move |cx, row_index, row| {
447 HStack::new(cx, |cx| {
448 for (column_index, column) in body_columns.iter().enumerate() {
449 let width_signal = column.width;
450 let min_width = column.min_width;
451 let cell_content = column.cell_content.clone();
452 let is_last_column = column_index + 1 == body_columns.len();
453
454 if is_last_column {
455 VStack::new(cx, move |cx| {
456 cell_content(cx, row.map(|value| value.clone()));
457 })
458 .class("table-cell")
459 .width(Stretch(1.0))
460 .min_width(Auto)
461 .height(Auto);
462 } else {
463 VStack::new(cx, move |cx| {
464 cell_content(cx, row.map(|value| value.clone()));
465 })
466 .class("table-cell")
467 .width(width_signal.map(|value| Pixels(*value)))
468 .min_width(min_width.map(|value| Pixels(*value)))
469 .height(Auto);
470 }
471 }
472 })
473 .class("table-row")
474 .toggle_class("odd", row_index % 2 == 1)
475 .toggle_class("even", row_index % 2 == 0)
476 .alignment(Alignment::Left)
477 .height(Auto)
478 .width(Stretch(1.0))
479 .min_width(Auto);
480 })
481 .width(Stretch(1.0))
482 .min_width(Auto)
483 .height(Stretch(1.0))
484 .min_height(Auto)
485 .class("table-body")
486 .selection(selected_indices)
487 .selectable(selectable)
488 .selection_follows_focus(selection_follows_focus)
489 .on_select(move |cx, index| cx.emit(TableEvent::<K>::SelectRow(index)));
490 });
491 })
492 }
493}
494
495impl<T, V, Id, K> View for Table<T, V, Id, K>
496where
497 V: Deref<Target = [T]> + Clone + 'static,
498 T: PartialEq + Clone + 'static,
499 Id: PartialEq + Clone + 'static,
500 K: Clone + PartialEq + Send + Sync + 'static,
501{
502 fn element(&self) -> Option<&'static str> {
503 Some("table")
504 }
505
506 fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
507 event.map(|table_event: &TableEvent<K>, _| match table_event {
508 TableEvent::RequestSort(key, direction) => {
509 if let Some(callback) = &self.on_sort {
510 (callback)(cx, key.clone(), *direction);
511 }
512 }
513
514 TableEvent::SelectRow(index) => {
515 let rows = self.rows.get();
516 if let Some(row) = rows.deref().get(*index) {
517 if let Some(callback) = &self.on_row_select {
518 (callback)(cx, (self.row_id)(row));
519 }
520 }
521 }
522 });
523 }
524}
525
526pub trait TableModifiers<Id, K = String>: Sized
528where
529 K: Clone + PartialEq + Send + Sync + 'static,
530{
531 fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self;
533
534 fn resizable_columns<U: Into<bool> + Clone + 'static>(
536 self,
537 flag: impl Res<U> + 'static,
538 ) -> Self;
539
540 fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
542 self,
543 cycle: impl Res<U> + 'static,
544 ) -> Self;
545
546 fn selectable<U: Into<Selectable> + Clone + 'static>(
548 self,
549 selectable: impl Res<U> + 'static,
550 ) -> Self;
551
552 fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
554 self,
555 flag: impl Res<U> + 'static,
556 ) -> Self;
557
558 fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
560 where
561 R: Deref<Target = [Id]> + Clone + 'static;
562
563 fn on_sort<F>(self, callback: F) -> Self
565 where
566 F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync;
567
568 fn on_row_select<F>(self, callback: F) -> Self
570 where
571 F: 'static + Fn(&mut EventContext, Id);
572}
573
574impl<T, V, Id, K> TableModifiers<Id, K> for Handle<'_, Table<T, V, Id, K>>
575where
576 V: Deref<Target = [T]> + Clone + 'static,
577 T: PartialEq + Clone + 'static,
578 Id: PartialEq + Clone + 'static,
579 K: Clone + PartialEq + Send + Sync + 'static,
580{
581 fn sort_state(self, sort_state: impl Res<Option<TableSortState<K>>> + 'static) -> Self {
582 let sort_state = sort_state.to_signal(self.cx);
583 self.bind(sort_state, move |handle| {
584 let sort_state = sort_state.get();
585 handle.modify(|table: &mut Table<T, V, Id, K>| table.sort_state.set(sort_state));
586 })
587 }
588
589 fn resizable_columns<U: Into<bool> + Clone + 'static>(
590 self,
591 flag: impl Res<U> + 'static,
592 ) -> Self {
593 let flag = flag.to_signal(self.cx);
594 self.bind(flag, move |handle| {
595 let flag = flag.get().into();
596 handle.modify(|table: &mut Table<T, V, Id, K>| table.resizable_columns.set(flag));
597 })
598 }
599
600 fn sort_cycle<U: Into<TableSortCycle> + Clone + 'static>(
601 self,
602 cycle: impl Res<U> + 'static,
603 ) -> Self {
604 let cycle = cycle.to_signal(self.cx);
605 self.bind(cycle, move |handle| {
606 let cycle = cycle.get().into();
607 handle.modify(|table: &mut Table<T, V, Id, K>| table.sort_cycle.set(cycle));
608 })
609 }
610
611 fn selectable<U: Into<Selectable> + Clone + 'static>(
612 self,
613 selectable: impl Res<U> + 'static,
614 ) -> Self {
615 let selectable = selectable.to_signal(self.cx);
616 self.bind(selectable, move |handle| {
617 let selectable = selectable.get().into();
618 handle.modify(|table: &mut Table<T, V, Id, K>| table.selectable.set(selectable));
619 })
620 }
621
622 fn selection_follows_focus<U: Into<bool> + Clone + 'static>(
623 self,
624 flag: impl Res<U> + 'static,
625 ) -> Self {
626 let flag = flag.to_signal(self.cx);
627 self.bind(flag, move |handle| {
628 let flag = flag.get().into();
629 handle.modify(|table: &mut Table<T, V, Id, K>| table.selection_follows_focus.set(flag));
630 })
631 }
632
633 fn selected_row_ids<R>(self, selected_row_ids: impl Res<R> + 'static) -> Self
634 where
635 R: Deref<Target = [Id]> + Clone + 'static,
636 {
637 let selected_row_ids = selected_row_ids.to_signal(self.cx);
638 self.bind(selected_row_ids, move |handle| {
639 let ids = selected_row_ids.with(|ids| ids.deref().to_vec());
640 handle.modify(|table: &mut Table<T, V, Id, K>| table.selected_row_ids.set(ids));
641 })
642 }
643
644 fn on_sort<F>(self, callback: F) -> Self
645 where
646 F: 'static + Fn(&mut EventContext, K, TableSortDirection) + Send + Sync,
647 {
648 self.modify(|table: &mut Table<T, V, Id, K>| table.on_sort = Some(Arc::new(callback)))
649 }
650
651 fn on_row_select<F>(self, callback: F) -> Self
652 where
653 F: 'static + Fn(&mut EventContext, Id),
654 {
655 self.modify(|table: &mut Table<T, V, Id, K>| table.on_row_select = Some(Box::new(callback)))
656 }
657}