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    pub fn cells<R>(self, row: RowData<C, R>) -> impl Iterator<Item = CellData<C, R>>
174    where
175        C: Columns<R>,
176        R: Row,
177    {
178        let order = self.get_column_order();
179        let row_copy = row;
180        order.into_iter().map(move |column_index| CellData {
181            row: row_copy,
182            column_index,
183        })
184    }
185
186    pub fn rows<R>(self, rows: ReadSignal<Vec<R>>) -> impl Iterator<Item = RowData<C, R>>
187    where
188        C: Columns<R>,
189        R: Row,
190    {
191        let rows_data = rows.read();
192        let columns = self.columns.read();
193
194        // Step 1: Apply filter - collect indices of rows that pass the filter
195        let mut filtered_indices: Vec<usize> = (0..rows_data.len())
196            .filter(|&i| columns.filter(&rows_data[i]))
197            .collect();
198
199        // Step 2: Apply sort if any sort records exist
200        let sort_records = self.data.sorts.read();
201        if !sort_records.is_empty() {
202            let comparators = columns.compare();
203
204            // Sort the filtered indices based on multi-column sort priority
205            filtered_indices.sort_by(|&a, &b| {
206                // Iterate through sort records in priority order
207                for sort_record in sort_records.iter() {
208                    let ordering = comparators[sort_record.column](&rows_data[a], &rows_data[b]);
209
210                    // Apply direction (ascending or descending)
211                    let directed_ordering = match sort_record.sort.direction {
212                        SortDirection::Ascending => ordering,
213                        SortDirection::Descending => ordering.reverse(),
214                    };
215
216                    // If not equal, return this ordering
217                    if directed_ordering != std::cmp::Ordering::Equal {
218                        return directed_ordering;
219                    }
220                    // If equal, continue to next sort column
221                }
222
223                // All sort columns are equal, maintain stable sort
224                std::cmp::Ordering::Equal
225            });
226        }
227
228        // Step 3: Return iterator over sorted and filtered indices
229        filtered_indices.into_iter().map(move |i| RowData {
230            context: self,
231            rows,
232            index: i,
233            _phantom: PhantomData,
234        })
235    }
236}
237
238impl TableContextData {
239    pub fn column_context(&self, column: usize) -> ColumnContext {
240        ColumnContext {
241            table_context: *self,
242            column,
243        }
244    }
245
246    pub fn get_column_order(&self) -> Vec<usize> {
247        self.column_order.read().get_order().to_vec()
248    }
249
250    pub fn get_column_name(&self, index: usize) -> String {
251        self.column_names.read()[index].clone()
252    }
253
254    pub fn request_sort(&self, column: usize, sort: SortGesture) {
255        match sort {
256            SortGesture::Cancel => {
257                let mut signal = self.sorts;
258                signal.write().retain(|record| record.column != column);
259            }
260            SortGesture::AddFirst(sort) => {
261                let mut signal = self.sorts;
262                let mut write = signal.write();
263                write.retain(|record| record.column != column);
264                write.insert(0, SortRecord { column, sort });
265            }
266            SortGesture::AddLast(sort) => {
267                let mut signal = self.sorts;
268                let mut write = signal.write();
269                write.retain(|record| record.column != column);
270                write.push(SortRecord { column, sort });
271            }
272            SortGesture::Toggle => {
273                let mut signal = self.sorts;
274                if let Some(record) = signal.write().iter_mut().find(|r| r.column == column) {
275                    record.sort.direction = match record.sort.direction {
276                        SortDirection::Ascending => SortDirection::Descending,
277                        SortDirection::Descending => SortDirection::Ascending,
278                    };
279                }
280            }
281        }
282    }
283
284    // Column order management methods
285
286    pub fn swap_columns(&self, col_a: usize, col_b: usize) {
287        let mut signal = self.column_order;
288        signal.write().swap(col_a, col_b);
289    }
290
291    pub fn hide_column(&self, col: usize) {
292        let mut signal = self.column_order;
293        signal.write().hide_column(col);
294    }
295
296    pub fn show_column(&self, col: usize, at_index: Option<usize>) {
297        let mut signal = self.column_order;
298        signal.write().show_column(col, at_index);
299    }
300
301    pub fn move_column_to(&self, col: usize, new_index: usize) {
302        let mut signal = self.column_order;
303        signal.write().move_to(col, new_index);
304    }
305
306    pub fn move_column_forward(&self, col: usize) {
307        let mut signal = self.column_order;
308        signal.write().move_forward(col);
309    }
310
311    pub fn move_column_backward(&self, col: usize) {
312        let mut signal = self.column_order;
313        signal.write().move_backward(col);
314    }
315
316    pub fn is_column_visible(&self, col: usize) -> bool {
317        self.column_order.read().is_visible(col)
318    }
319
320    pub fn column_position(&self, col: usize) -> Option<usize> {
321        self.column_order.read().position(col)
322    }
323
324    pub fn reset_column_order(&self) {
325        let mut signal = self.column_order;
326        signal.write().reset();
327    }
328}
329
330/// Context for a specific column, providing access to sorting and visibility controls.
331///
332/// This type is passed to [`TableColumn::render_header`](crate::TableColumn::render_header)
333/// and [`TableColumn::render_cell`](crate::TableColumn::render_cell),
334/// allowing columns to interact with table state.
335///
336/// # Sorting
337///
338/// - [`request_sort`](Self::request_sort): Request a sort operation
339/// - [`sort_info`](Self::sort_info): Get current sort state
340///
341/// # Column Visibility and Ordering
342///
343/// - [`hide`](Self::hide) / [`show`](Self::show): Toggle visibility
344/// - [`move_to`](Self::move_to), [`move_forward`](Self::move_forward), [`move_backward`](Self::move_backward): Reorder
345/// - [`is_visible`](Self::is_visible), [`position`](Self::position): Query state
346///
347/// # Example
348///
349/// ```
350/// # use dioxus::prelude::*;
351/// # use dioxus_tabular::*;
352/// # #[derive(Clone, PartialEq)]
353/// # struct User { id: u32 }
354/// # impl Row for User {
355/// #     fn key(&self) -> impl Into<String> { self.id.to_string() }
356/// # }
357/// # #[derive(Clone, PartialEq)]
358/// # struct Col;
359/// impl TableColumn<User> for Col {
360///     fn column_name(&self) -> String {
361///         "col".into()
362///     }
363///
364///     fn render_header(&self, context: ColumnContext, attributes: Vec<Attribute>) -> Element {
365///         rsx! {
366///             th { ..attributes,
367///                 button {
368///                     onclick: move |_| {
369///                         // Request ascending sort
370///                         context.request_sort(SortGesture::AddLast(Sort {
371///                             direction: SortDirection::Ascending,
372///                         }));
373///                     },
374///                     "Sort"
375///                 }
376///                 // Show sort indicator
377///                 if let Some(info) = context.sort_info() {
378///                     match info.direction {
379///                         SortDirection::Ascending => " ↑",
380///                         SortDirection::Descending => " ↓",
381///                     }
382///                 }
383///             }
384///         }
385///     }
386///
387///     fn render_cell(&self, _context: ColumnContext, _row: &User, _attributes: Vec<Attribute>) -> Element {
388///         rsx! { td {} }
389///     }
390/// }
391/// ```
392#[derive(Clone, Copy, PartialEq)]
393pub struct ColumnContext {
394    table_context: TableContextData,
395    column: usize,
396}
397
398impl ColumnContext {
399    /// Requests a sort operation on this column.
400    ///
401    /// Use `SortGesture::AddFirst` to make this the primary sort,
402    /// `AddLast` to add as secondary, `Toggle` to flip direction, or `Cancel` to remove.
403    pub fn request_sort(&self, sort: SortGesture) {
404        self.table_context.request_sort(self.column, sort);
405    }
406
407    /// Returns the sort information for this column, or `None` if not sorted.
408    ///
409    /// Use `SortInfo.priority` to show sort order (0 = primary) and `SortInfo.direction` for the arrow.
410    pub fn sort_info(&self) -> Option<SortInfo> {
411        let sorts = self.table_context.sorts.read();
412        sorts
413            .iter()
414            .position(|record| record.column == self.column)
415            .map(|priority| SortInfo {
416                priority,
417                direction: sorts[priority].sort.direction,
418            })
419    }
420
421    // Column order management delegate methods
422
423    /// Swaps this column with another column in the display order.
424    pub fn swap_with(&self, other_col: usize) {
425        self.table_context.swap_columns(self.column, other_col);
426    }
427
428    /// Hides this column from the display.
429    pub fn hide(&self) {
430        self.table_context.hide_column(self.column);
431    }
432
433    /// Shows this column in the display. If `at_index` is `None`, appends to the end.
434    pub fn show(&self, at_index: Option<usize>) {
435        self.table_context.show_column(self.column, at_index);
436    }
437
438    /// Moves this column to a specific display position (0-indexed).
439    pub fn move_to(&self, new_index: usize) {
440        self.table_context.move_column_to(self.column, new_index);
441    }
442
443    /// Moves this column one position forward (towards index 0).
444    pub fn move_forward(&self) {
445        self.table_context.move_column_forward(self.column);
446    }
447
448    /// Moves this column one position backward (towards the end).
449    pub fn move_backward(&self) {
450        self.table_context.move_column_backward(self.column);
451    }
452
453    /// Returns whether this column is currently visible.
454    pub fn is_visible(&self) -> bool {
455        self.table_context.is_column_visible(self.column)
456    }
457
458    /// Returns the display position (0-indexed), or `None` if hidden.
459    pub fn position(&self) -> Option<usize> {
460        self.table_context.column_position(self.column)
461    }
462
463    /// Resets all columns to default visibility and order.
464    pub fn reset_order(&self) {
465        self.table_context.reset_column_order();
466    }
467}
468
469/// Data for rendering a single header cell.
470///
471/// Returned by iterating over `TableContext::headers()`. Primarily used internally.
472#[derive(Copy, Clone, PartialEq)]
473pub struct HeaderData<C: Columns<R>, R: Row> {
474    pub(crate) context: TableContext<C>,
475    pub(crate) column_index: usize,
476    _phantom: PhantomData<R>,
477}
478
479impl<C: Columns<R>, R: Row> HeaderData<C, R> {
480    /// Returns the unique key for this header.
481    pub fn key(&self) -> String {
482        self.context.data.get_column_name(self.column_index)
483    }
484
485    /// Renders this header with the given attributes.
486    pub fn render(&self, attributes: Vec<Attribute>) -> Element {
487        let binding = self.context.columns.read();
488        let headers = binding.headers();
489        headers[self.column_index](&self.context, attributes)
490    }
491}
492
493/// The main table data structure returned by [`use_tabular`](crate::use_tabular).
494///
495/// Use this with [`TableHeaders`](crate::TableHeaders) and [`TableCells`](crate::TableCells) components.
496#[derive(PartialEq)]
497pub struct TableData<C: Columns<R>, R: Row> {
498    /// The table context (provides access to sorting/filtering state).
499    pub context: TableContext<C>,
500    /// The reactive signal containing row data.
501    pub rows: ReadSignal<Vec<R>>,
502}
503
504impl<C: Columns<R>, R: Row> Clone for TableData<C, R> {
505    fn clone(&self) -> Self {
506        *self
507    }
508}
509
510impl<C: Columns<R>, R: Row> Copy for TableData<C, R> {}
511
512impl<C: Columns<R>, R: Row> TableData<C, R> {
513    /// Returns an iterator over filtered and sorted rows.
514    pub fn rows(&self) -> impl Iterator<Item = RowData<C, R>> {
515        self.context.rows(self.rows)
516    }
517}
518
519/// Data for a single cell in the table.
520///
521/// Returned by iterating over `RowData::cells()`. Primarily used internally.
522#[derive(Copy, Clone, PartialEq)]
523pub struct CellData<C: Columns<R>, R: Row> {
524    pub(crate) row: RowData<C, R>,
525    pub(crate) column_index: usize,
526}
527
528impl<C: Columns<R>, R: Row> CellData<C, R> {
529    /// Returns the unique key for this cell.
530    pub fn key(&self) -> String {
531        self.row.context.data.get_column_name(self.column_index)
532    }
533
534    /// Renders this cell with the given attributes.
535    pub fn render(&self, attributes: Vec<Attribute>) -> Element {
536        let binding = self.row.context.columns.read();
537        let columns = binding.columns();
538        columns[self.column_index](
539            &self.row.context,
540            &self.row.rows.read()[self.row.index],
541            attributes,
542        )
543    }
544}
545
546/// Data for a single row in the table.
547///
548/// Returned by iterating over `TableData::rows()`. Pass to [`TableCells`](crate::TableCells) component.
549#[derive(PartialEq)]
550pub struct RowData<C: Columns<R>, R: Row> {
551    pub(crate) context: TableContext<C>,
552    pub(crate) rows: ReadSignal<Vec<R>>,
553    pub(crate) index: usize,
554    pub(crate) _phantom: PhantomData<R>,
555}
556
557impl<C: Columns<R>, R: Row> Copy for RowData<C, R> {}
558
559impl<C: Columns<R>, R: Row> Clone for RowData<C, R> {
560    fn clone(&self) -> Self {
561        *self
562    }
563}
564
565impl<C: Columns<R>, R: Row> RowData<C, R> {
566    /// Returns the unique key for this row.
567    pub fn key(&self) -> String {
568        self.rows.read()[self.index].key().into()
569    }
570
571    /// Returns an iterator over the cells in this row.
572    pub fn cells(self) -> impl Iterator<Item = CellData<C, R>> {
573        self.context.cells(self)
574    }
575}
576
577#[cfg(test)]
578mod tests_sort_request;
579
580#[cfg(test)]
581mod tests_rows_filter_and_sort;