dioxus_tabular/
context.rs

1use dioxus::prelude::*;
2
3use crate::{Columns, Row};
4use std::marker::PhantomData;
5
6mod column_order;
7pub use column_order::ColumnOrder;
8
9/// The direction of sorting.
10#[derive(Clone, Copy, PartialEq, Debug)]
11pub enum SortDirection {
12    /// Sort in ascending order (A to Z, 0 to 9).
13    Ascending,
14    /// Sort in descending order (Z to A, 9 to 0).
15    Descending,
16}
17
18/// A sort operation with a direction.
19///
20/// # Example
21///
22/// ```
23/// use dioxus_tabular::{Sort, SortDirection};
24///
25/// let sort = Sort {
26///     direction: SortDirection::Ascending,
27/// };
28/// ```
29#[derive(Clone, Copy, PartialEq)]
30pub struct Sort {
31    /// The direction of this sort.
32    pub direction: SortDirection,
33}
34
35/// Information about the current sort state of a column.
36///
37/// Returned by [`ColumnContext::sort_info`] to check if a column is currently sorted.
38///
39/// # Example
40///
41/// ```
42/// # use dioxus::prelude::*;
43/// # use dioxus_tabular::*;
44/// # fn example(context: ColumnContext) {
45/// if let Some(info) = context.sort_info() {
46///     println!("Column is sorted with priority {} in {:?} order",
47///              info.priority, info.direction);
48/// }
49/// # }
50/// ```
51#[derive(Clone, Copy, PartialEq, Debug)]
52pub struct SortInfo {
53    /// The sort priority (0 = highest priority).
54    pub priority: usize,
55    /// The direction of the sort.
56    pub direction: SortDirection,
57}
58
59/// A user gesture to change sorting state.
60///
61/// Used with [`ColumnContext::request_sort`] to control how columns are sorted.
62///
63/// # Example
64///
65/// ```
66/// # use dioxus::prelude::*;
67/// # use dioxus_tabular::*;
68/// # fn example(context: ColumnContext) {
69/// // Add this column as primary sort
70/// context.request_sort(SortGesture::AddFirst(Sort {
71///     direction: SortDirection::Ascending,
72/// }));
73///
74/// // Toggle between ascending/descending
75/// context.request_sort(SortGesture::Toggle);
76///
77/// // Remove sort from this column
78/// context.request_sort(SortGesture::Cancel);
79/// # }
80/// ```
81#[derive(Clone, Copy, PartialEq)]
82pub enum SortGesture {
83    /// Remove sorting from this column.
84    Cancel,
85    /// Add this column as the primary (first) sort, pushing others down in priority.
86    AddFirst(Sort),
87    /// Add this column as the last (lowest priority) sort.
88    AddLast(Sort),
89    /// Toggle the sort direction of this column (Ascending ↔ Descending).
90    /// Does nothing if the column is not currently sorted.
91    Toggle,
92}
93
94#[derive(Clone, Copy, PartialEq)]
95pub struct SortRecord {
96    column: usize,
97    sort: Sort,
98}
99
100#[derive(Clone, Copy, PartialEq)]
101pub(crate) struct TableContextData {
102    sorts: Signal<Vec<SortRecord>>,
103    // The columns names of the table.
104    column_names: Signal<Vec<String>>,
105    // Manages the order and visibility of columns.
106    column_order: Signal<ColumnOrder>,
107}
108
109#[derive(PartialEq)]
110pub struct TableContext<C: 'static> {
111    pub(crate) data: TableContextData,
112    /// Does not need to be Signal, but for Copy trait.
113    pub(crate) columns: Signal<C>,
114}
115
116impl<C: 'static> Copy for TableContext<C> {}
117
118impl<C: 'static> Clone for TableContext<C> {
119    fn clone(&self) -> Self {
120        *self
121    }
122}
123
124impl<C> TableContext<C> {
125    pub fn use_table_context<R>(columns: C) -> Self
126    where
127        C: Columns<R>,
128        R: Row,
129    {
130        let sorts = use_signal(Vec::new);
131        let column_names = use_signal(|| columns.column_names());
132        let total_columns = column_names.read().len();
133        let column_order = use_signal(|| ColumnOrder::new(total_columns));
134        let columns = use_signal(|| columns);
135        Self {
136            data: TableContextData {
137                sorts,
138                column_names,
139                column_order,
140            },
141            columns,
142        }
143    }
144
145    pub fn table_data<R>(self, rows: ReadSignal<Vec<R>>) -> TableData<C, R>
146    where
147        C: Columns<R>,
148        R: Row,
149    {
150        TableData {
151            context: self,
152            rows,
153        }
154    }
155
156    fn get_column_order(&self) -> Vec<usize> {
157        self.data.get_column_order()
158    }
159
160    pub fn headers<R>(self) -> impl Iterator<Item = HeaderData<C, R>>
161    where
162        C: Columns<R>,
163        R: Row,
164    {
165        let order = self.get_column_order();
166        order.into_iter().map(move |column_index| HeaderData {
167            context: self,
168            column_index,
169            _phantom: PhantomData,
170        })
171    }
172
173    /// Returns an iterator over all column headers, including hidden ones.
174    pub fn all_headers<R>(self) -> impl Iterator<Item = HeaderData<C, R>>
175    where
176        C: Columns<R>,
177        R: Row,
178    {
179        let num_columns = self.data.num_columns();
180        (0..num_columns).map(move |column_index| HeaderData {
181            context: self,
182            column_index,
183            _phantom: PhantomData,
184        })
185    }
186
187    pub fn cells<R>(self, row: RowData<C, R>) -> impl Iterator<Item = CellData<C, R>>
188    where
189        C: Columns<R>,
190        R: Row,
191    {
192        let order = self.get_column_order();
193        let row_copy = row;
194        order.into_iter().map(move |column_index| CellData {
195            row: row_copy,
196            column_index,
197        })
198    }
199
200    pub fn rows<R>(self, rows: ReadSignal<Vec<R>>) -> impl Iterator<Item = RowData<C, R>>
201    where
202        C: Columns<R>,
203        R: Row,
204    {
205        let rows_data = rows.read();
206        let columns = self.columns.read();
207
208        // Step 1: Apply filter - collect indices of rows that pass the filter
209        let mut filtered_indices: Vec<usize> = (0..rows_data.len())
210            .filter(|&i| columns.filter(&rows_data[i]))
211            .collect();
212
213        // Step 2: Apply sort if any sort records exist
214        let sort_records = self.data.sorts.read();
215        if !sort_records.is_empty() {
216            let comparators = columns.compare();
217
218            // Sort the filtered indices based on multi-column sort priority
219            filtered_indices.sort_by(|&a, &b| {
220                // Iterate through sort records in priority order
221                for sort_record in sort_records.iter() {
222                    let ordering = comparators[sort_record.column](&rows_data[a], &rows_data[b]);
223
224                    // Apply direction (ascending or descending)
225                    let directed_ordering = match sort_record.sort.direction {
226                        SortDirection::Ascending => ordering,
227                        SortDirection::Descending => ordering.reverse(),
228                    };
229
230                    // If not equal, return this ordering
231                    if directed_ordering != std::cmp::Ordering::Equal {
232                        return directed_ordering;
233                    }
234                    // If equal, continue to next sort column
235                }
236
237                // All sort columns are equal, maintain stable sort
238                std::cmp::Ordering::Equal
239            });
240        }
241
242        // Step 3: Return iterator over sorted and filtered indices
243        filtered_indices.into_iter().map(move |i| RowData {
244            context: self,
245            rows,
246            index: i,
247            _phantom: PhantomData,
248        })
249    }
250}
251
252impl TableContextData {
253    pub fn column_context(&self, column: usize) -> ColumnContext {
254        ColumnContext {
255            table_context: *self,
256            column,
257        }
258    }
259
260    pub fn get_column_order(&self) -> Vec<usize> {
261        self.column_order.read().get_order().to_vec()
262    }
263
264    pub fn num_columns(&self) -> usize {
265        self.column_names.read().len()
266    }
267
268    pub fn get_column_name(&self, index: usize) -> String {
269        self.column_names.read()[index].clone()
270    }
271
272    pub fn request_sort(&self, column: usize, sort: SortGesture) {
273        match sort {
274            SortGesture::Cancel => {
275                let mut signal = self.sorts;
276                signal.write().retain(|record| record.column != column);
277            }
278            SortGesture::AddFirst(sort) => {
279                let mut signal = self.sorts;
280                let mut write = signal.write();
281                write.retain(|record| record.column != column);
282                write.insert(0, SortRecord { column, sort });
283            }
284            SortGesture::AddLast(sort) => {
285                let mut signal = self.sorts;
286                let mut write = signal.write();
287                write.retain(|record| record.column != column);
288                write.push(SortRecord { column, sort });
289            }
290            SortGesture::Toggle => {
291                let mut signal = self.sorts;
292                if let Some(record) = signal.write().iter_mut().find(|r| r.column == column) {
293                    record.sort.direction = match record.sort.direction {
294                        SortDirection::Ascending => SortDirection::Descending,
295                        SortDirection::Descending => SortDirection::Ascending,
296                    };
297                }
298            }
299        }
300    }
301
302    // Column order management methods
303
304    pub fn swap_columns(&self, col_a: usize, col_b: usize) {
305        let mut signal = self.column_order;
306        signal.write().swap(col_a, col_b);
307    }
308
309    pub fn hide_column(&self, col: usize) {
310        let mut signal = self.column_order;
311        signal.write().hide_column(col);
312    }
313
314    pub fn show_column(&self, col: usize, at_index: Option<usize>) {
315        let mut signal = self.column_order;
316        signal.write().show_column(col, at_index);
317    }
318
319    pub fn move_column_to(&self, col: usize, new_index: usize) {
320        let mut signal = self.column_order;
321        signal.write().move_to(col, new_index);
322    }
323
324    pub fn move_column_forward(&self, col: usize) {
325        let mut signal = self.column_order;
326        signal.write().move_forward(col);
327    }
328
329    pub fn move_column_backward(&self, col: usize) {
330        let mut signal = self.column_order;
331        signal.write().move_backward(col);
332    }
333
334    pub fn is_column_visible(&self, col: usize) -> bool {
335        self.column_order.read().is_visible(col)
336    }
337
338    pub fn column_position(&self, col: usize) -> Option<usize> {
339        self.column_order.read().position(col)
340    }
341
342    pub fn reset_column_order(&self) {
343        let mut signal = self.column_order;
344        signal.write().reset();
345    }
346}
347
348/// Context for a specific column, providing access to sorting and visibility controls.
349///
350/// This type is passed to [`TableColumn::render_header`](crate::TableColumn::render_header)
351/// and [`TableColumn::render_cell`](crate::TableColumn::render_cell),
352/// allowing columns to interact with table state.
353///
354/// # Sorting
355///
356/// - [`request_sort`](Self::request_sort): Request a sort operation
357/// - [`sort_info`](Self::sort_info): Get current sort state
358///
359/// # Column Visibility and Ordering
360///
361/// - [`hide`](Self::hide) / [`show`](Self::show): Toggle visibility
362/// - [`move_to`](Self::move_to), [`move_forward`](Self::move_forward), [`move_backward`](Self::move_backward): Reorder
363/// - [`is_visible`](Self::is_visible), [`position`](Self::position): Query state
364///
365/// # Example
366///
367/// ```
368/// # use dioxus::prelude::*;
369/// # use dioxus_tabular::*;
370/// # #[derive(Clone, PartialEq)]
371/// # struct User { id: u32 }
372/// # impl Row for User {
373/// #     fn key(&self) -> impl Into<String> { self.id.to_string() }
374/// # }
375/// # #[derive(Clone, PartialEq)]
376/// # struct Col;
377/// impl TableColumn<User> for Col {
378///     fn column_name(&self) -> String {
379///         "col".into()
380///     }
381///
382///     fn render_header(&self, context: ColumnContext, attributes: Vec<Attribute>) -> Element {
383///         rsx! {
384///             th { ..attributes,
385///                 button {
386///                     onclick: move |_| {
387///                         // Request ascending sort
388///                         context.request_sort(SortGesture::AddLast(Sort {
389///                             direction: SortDirection::Ascending,
390///                         }));
391///                     },
392///                     "Sort"
393///                 }
394///                 // Show sort indicator
395///                 if let Some(info) = context.sort_info() {
396///                     match info.direction {
397///                         SortDirection::Ascending => " ↑",
398///                         SortDirection::Descending => " ↓",
399///                     }
400///                 }
401///             }
402///         }
403///     }
404///
405///     fn render_cell(&self, _context: ColumnContext, _row: &User, _attributes: Vec<Attribute>) -> Element {
406///         rsx! { td {} }
407///     }
408/// }
409/// ```
410#[derive(Clone, Copy, PartialEq)]
411pub struct ColumnContext {
412    table_context: TableContextData,
413    column: usize,
414}
415
416impl ColumnContext {
417    /// Requests a sort operation on this column.
418    ///
419    /// Use `SortGesture::AddFirst` to make this the primary sort,
420    /// `AddLast` to add as secondary, `Toggle` to flip direction, or `Cancel` to remove.
421    pub fn request_sort(&self, sort: SortGesture) {
422        self.table_context.request_sort(self.column, sort);
423    }
424
425    /// Returns the sort information for this column, or `None` if not sorted.
426    ///
427    /// Use `SortInfo.priority` to show sort order (0 = primary) and `SortInfo.direction` for the arrow.
428    pub fn sort_info(&self) -> Option<SortInfo> {
429        let sorts = self.table_context.sorts.read();
430        sorts
431            .iter()
432            .position(|record| record.column == self.column)
433            .map(|priority| SortInfo {
434                priority,
435                direction: sorts[priority].sort.direction,
436            })
437    }
438
439    // Column order management delegate methods
440
441    /// Swaps this column with another column in the display order.
442    pub fn swap_with(&self, other_col: usize) {
443        self.table_context.swap_columns(self.column, other_col);
444    }
445
446    /// Hides this column from the display.
447    pub fn hide(&self) {
448        self.table_context.hide_column(self.column);
449    }
450
451    /// Shows this column in the display. If `at_index` is `None`, appends to the end.
452    pub fn show(&self, at_index: Option<usize>) {
453        self.table_context.show_column(self.column, at_index);
454    }
455
456    /// Moves this column to a specific display position (0-indexed).
457    pub fn move_to(&self, new_index: usize) {
458        self.table_context.move_column_to(self.column, new_index);
459    }
460
461    /// Moves this column one position forward (towards index 0).
462    pub fn move_forward(&self) {
463        self.table_context.move_column_forward(self.column);
464    }
465
466    /// Moves this column one position backward (towards the end).
467    pub fn move_backward(&self) {
468        self.table_context.move_column_backward(self.column);
469    }
470
471    /// Returns whether this column is currently visible.
472    pub fn is_visible(&self) -> bool {
473        self.table_context.is_column_visible(self.column)
474    }
475
476    /// Returns the display position (0-indexed), or `None` if hidden.
477    pub fn position(&self) -> Option<usize> {
478        self.table_context.column_position(self.column)
479    }
480
481    /// Resets all columns to default visibility and order.
482    pub fn reset_order(&self) {
483        self.table_context.reset_column_order();
484    }
485}
486
487/// Data for rendering a single header cell.
488///
489/// Returned by iterating over `TableContext::headers()`. Primarily used internally.
490#[derive(Copy, Clone, PartialEq)]
491pub struct HeaderData<C: Columns<R>, R: Row> {
492    pub(crate) context: TableContext<C>,
493    pub(crate) column_index: usize,
494    _phantom: PhantomData<R>,
495}
496
497impl<C: Columns<R>, R: Row> HeaderData<C, R> {
498    /// Returns the unique key for this header.
499    pub fn key(&self) -> String {
500        self.context.data.get_column_name(self.column_index)
501    }
502
503    /// Returns the column context for this header.
504    pub fn column_context(&self) -> ColumnContext {
505        self.context.data.column_context(self.column_index)
506    }
507
508    /// Renders this header with the given attributes.
509    pub fn render(&self, attributes: Vec<Attribute>) -> Element {
510        let binding = self.context.columns.read();
511        let headers = binding.headers();
512        headers[self.column_index](&self.context, attributes)
513    }
514}
515
516/// The main table data structure returned by [`use_tabular`](crate::use_tabular).
517///
518/// Use this with [`TableHeaders`](crate::TableHeaders) and [`TableCells`](crate::TableCells) components.
519#[derive(PartialEq)]
520pub struct TableData<C: Columns<R>, R: Row> {
521    /// The table context (provides access to sorting/filtering state).
522    pub context: TableContext<C>,
523    /// The reactive signal containing row data.
524    pub rows: ReadSignal<Vec<R>>,
525}
526
527impl<C: Columns<R>, R: Row> Clone for TableData<C, R> {
528    fn clone(&self) -> Self {
529        *self
530    }
531}
532
533impl<C: Columns<R>, R: Row> Copy for TableData<C, R> {}
534
535impl<C: Columns<R>, R: Row> TableData<C, R> {
536    /// Returns an iterator over filtered and sorted rows.
537    pub fn rows(&self) -> impl Iterator<Item = RowData<C, R>> {
538        self.context.rows(self.rows)
539    }
540}
541
542/// Data for a single cell in the table.
543///
544/// Returned by iterating over `RowData::cells()`. Primarily used internally.
545#[derive(Copy, Clone, PartialEq)]
546pub struct CellData<C: Columns<R>, R: Row> {
547    pub(crate) row: RowData<C, R>,
548    pub(crate) column_index: usize,
549}
550
551impl<C: Columns<R>, R: Row> CellData<C, R> {
552    /// Returns the unique key for this cell.
553    pub fn key(&self) -> String {
554        self.row.context.data.get_column_name(self.column_index)
555    }
556
557    /// Renders this cell with the given attributes.
558    pub fn render(&self, attributes: Vec<Attribute>) -> Element {
559        let binding = self.row.context.columns.read();
560        let columns = binding.columns();
561        columns[self.column_index](
562            &self.row.context,
563            &self.row.rows.read()[self.row.index],
564            attributes,
565        )
566    }
567}
568
569/// Data for a single row in the table.
570///
571/// Returned by iterating over `TableData::rows()`. Pass to [`TableCells`](crate::TableCells) component.
572#[derive(PartialEq)]
573pub struct RowData<C: Columns<R>, R: Row> {
574    pub(crate) context: TableContext<C>,
575    pub(crate) rows: ReadSignal<Vec<R>>,
576    pub(crate) index: usize,
577    pub(crate) _phantom: PhantomData<R>,
578}
579
580impl<C: Columns<R>, R: Row> Copy for RowData<C, R> {}
581
582impl<C: Columns<R>, R: Row> Clone for RowData<C, R> {
583    fn clone(&self) -> Self {
584        *self
585    }
586}
587
588impl<C: Columns<R>, R: Row> RowData<C, R> {
589    /// Returns the unique key for this row.
590    pub fn key(&self) -> String {
591        self.rows.read()[self.index].key().into()
592    }
593
594    /// Returns an iterator over the cells in this row.
595    pub fn cells(self) -> impl Iterator<Item = CellData<C, R>> {
596        self.context.cells(self)
597    }
598
599    /// Returns the data for this row.
600    pub fn data(
601        &self,
602    ) -> impl Readable<Target = R> + Copy + std::ops::Deref<Target = dyn Fn() -> R> + 'static {
603        let index = self.index;
604        self.rows.map(move |rows| &rows[index])
605    }
606}
607
608#[cfg(test)]
609mod tests_sort_request;
610
611#[cfg(test)]
612mod tests_rows_filter_and_sort;
613
614#[cfg(test)]
615mod tests_column_context;