egui_selectable_table/
lib.rs

1mod auto_reload;
2mod auto_scroll;
3mod row_modification;
4mod row_selection;
5
6use auto_reload::AutoReload;
7pub use auto_scroll::AutoScroll;
8use egui::ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
9use egui::{Event, Key, Label, Response, ScrollArea, Sense, Ui};
10use egui_extras::{Column, TableBuilder, TableRow};
11use std::cmp::Ordering;
12use std::hash::Hash;
13
14/// Enum representing the possible sort orders for table columns.
15#[derive(Default, Clone, Copy)]
16pub enum SortOrder {
17    /// Sorts in ascending order (e.g., A to Z or 1 to 10).
18    #[default]
19    Ascending,
20    /// Sorts in descending order (e.g., Z to A or 10 to 1).
21    Descending,
22}
23
24/// Trait for defining how to order rows based on a specific column.
25///
26/// This trait should be implemented by users to specify how rows should be
27/// compared for sorting purposes. The implementation can vary depending on
28/// the type of column. For instance, string comparisons or numeric comparisons
29/// can be handled differently depending on the column. Should only be implemented for Ascending
30/// ordering, in case of Descending, it is handled internally.
31///
32/// # Example
33/// Suppose you have a struct `MyRow` with fields like `user_id`, `name`, and `username`.
34/// You could implement this trait for each column to specify how rows should be compared.
35///
36/// ```rust,ignore
37/// impl ColumnOrdering<MyRow> for ColumnName {
38///     fn order_by(&self, row_1: &MyRow, row_2: &MyRow) -> Ordering {
39///         match self {
40///             ColumnName::UserID => row_1.user_id.cmp(&row_2.user_id),
41///             ColumnName::Name => row_1.name.cmp(&row_2.name),
42///             ColumnName::Username => row_1.username.cmp(&row_2.username),
43///         }
44///     }
45/// }
46/// ```
47pub trait ColumnOrdering<Row>
48where
49    Row: Clone + Send + Sync,
50{
51    /// Compare two rows and return the ordering result (`Ordering`).
52    ///
53    /// This function defines how to order two rows based on the specific column.
54    /// It returns `Ordering::Less`, `Ordering::Equal`, or `Ordering::Greater`
55    /// to indicate whether `row_1` should be placed before, after, or at the same
56    /// position as `row_2` when sorting. Should only be implemented for ascending ordering, in
57    /// case of Descending, it is handled internally.
58    ///
59    /// # Arguments
60    /// * `row_1` - The first row for comparison.
61    /// * `row_2` - The second row for comparison.
62    ///
63    /// # Returns
64    /// * `Ordering` - Indicates the relative order between the two rows.
65    fn order_by(&self, row_1: &Row, row_2: &Row) -> Ordering;
66}
67
68/// Trait for defining column-specific operations in a table UI.
69///
70/// This trait allows users to define how each column should behave within a table.
71/// This includes how headers should be displayed, how each row in the table should be rendered,
72/// and how to extract column-specific text.
73///
74/// # Type Parameters:
75/// * `Row` - The type representing each row in the table.
76/// * `F` - A type that identifies columns, usually an enum or a field type.
77/// * `Conf` - Configuration type for the table, useful for passing additional settings.
78///
79/// # Requirements:
80/// You must implement this trait to specify the behavior of each column within
81/// the context of your table UI.
82pub trait ColumnOperations<Row, F, Conf>
83where
84    Row: Clone + Send + Sync,
85    F: Eq
86        + Hash
87        + Clone
88        + Ord
89        + Send
90        + Sync
91        + Default
92        + ColumnOperations<Row, F, Conf>
93        + ColumnOrdering<Row>,
94    Conf: Default,
95{
96    /// Create the header UI for this column.
97    ///
98    /// This function is responsible for creating the visual representation of the column header.
99    /// The `sort_order` argument indicates whether the column is currently used for sorting and, if so, in which
100    /// direction (ascending or descending). You can customize the header appearance based on
101    /// this information, for example by adding icons or text. Return `None` for no header.
102    ///
103    /// # Arguments
104    /// * `ui` - A mutable reference to the UI context.
105    /// * `sort_order` - An optional `SortOrder` representing the current sort state of the column.
106    /// * `table` - A mutable reference to the `SelectableTable`, allowing you to interact with the table state.
107    ///
108    /// # Returns
109    /// * `Option<Response>` - An optional response representing interaction with the UI.
110    fn create_header(
111        &self,
112        ui: &mut Ui,
113        sort_order: Option<SortOrder>,
114        table: &mut SelectableTable<Row, F, Conf>,
115    ) -> Option<Response>;
116
117    /// Create the UI for a specific row in this column.
118    ///
119    /// This function is responsible for rendering the content of this column for a given row.
120    /// It should handle user interactions like clicking or selection as necessary. Mutable table
121    /// access is provided for modifyiing other rows as necessary.
122    ///
123    /// # Arguments
124    /// * `ui` - A mutable reference to the UI context.
125    /// * `row` - A reference to the current `SelectableRow` for this table.
126    /// * `column_selected` - A boolean indicating whether this column is selected.
127    /// * `table` - A mutable reference to the `SelectableTable` for modifying table data
128    ///
129    /// # Returns
130    /// * `Response` - The result of the UI interaction for this row.
131    fn create_table_row(
132        &self,
133        ui: &mut Ui,
134        row: &SelectableRow<Row, F>,
135        column_selected: bool,
136        table: &mut SelectableTable<Row, F, Conf>,
137    ) -> Response;
138
139    /// Extract the text representation of the column for the given row.
140    ///
141    /// This function should return the appropriate text representation of this column
142    /// for the given row. It can be used to display the data in a simplified form, such
143    /// as for debugging or plain text rendering.
144    ///
145    /// # Arguments
146    /// * `row` - A reference to the row from which to extract the column text.
147    ///
148    /// # Returns
149    /// * `String` - The text representation of this column for the row.
150    fn column_text(&self, row: &Row) -> String;
151}
152
153/// Represents a row in a table with selectable columns.
154///
155/// This struct is used to store the data of a row along with its unique identifier (`id`)
156/// and the set of selected columns for this row.
157///
158/// # Type Parameters:
159/// * `Row` - The type representing the data stored in each row.
160/// * `F` - The type used to identify each column, typically an enum or a type with unique values.
161///
162/// # Fields:
163/// * `row_data` - The actual data stored in the row.
164/// * `id` - A unique identifier for the row.
165/// * `selected_columns` - A set of columns that are selected in this row.
166#[derive(Clone)]
167pub struct SelectableRow<Row, F>
168where
169    Row: Clone + Send + Sync,
170    F: Eq + Hash + Clone + Ord + Send + Sync + Default,
171{
172    pub row_data: Row,
173    pub id: i64,
174    pub selected_columns: HashSet<F>,
175}
176
177/// A table structure that hold data for performing selection on drag, sorting, and displaying rows and more.
178///
179/// # Type Parameters
180/// * `Row` - The type representing each row in the table.
181/// * `F` - A type used to identify columns, often an enum or field type.
182/// * `Conf` - Configuration type for additional table settings passed by the user. This is made available anytime when creating or modifying rows
183pub struct SelectableTable<Row, F, Conf>
184where
185    Row: Clone + Send + Sync,
186    F: Eq
187        + Hash
188        + Clone
189        + Ord
190        + Send
191        + Sync
192        + Default
193        + ColumnOperations<Row, F, Conf>
194        + ColumnOrdering<Row>,
195    Conf: Default,
196{
197    /// List of all columns available in the table.
198    all_columns: Vec<F>,
199    /// Maps each column to its index in the table for quick lookup.
200    column_number: HashMap<F, usize>,
201    /// Stores all rows in the table, keyed by their unique ID.
202    rows: HashMap<i64, SelectableRow<Row, F>>,
203    /// The current set of formatted rows for display.
204    formatted_rows: Vec<SelectableRow<Row, F>>,
205    /// The column currently being used to sort the table.
206    sorted_by: F,
207    /// The current sort order (ascending or descending).
208    sort_order: SortOrder,
209    /// Tracks where a drag operation started in the table, if any.
210    drag_started_on: Option<(i64, F)>,
211    /// The columns that have at least 1 row with the column as selected
212    active_columns: HashSet<F>,
213    /// The rows that have at least 1 column as selected
214    active_rows: HashSet<i64>,
215    /// The last row where the pointer was
216    last_active_row: Option<i64>,
217    /// The last column where the pointer was
218    last_active_column: Option<F>,
219    /// Whether the pointer moved from the dragged point at least once
220    beyond_drag_point: bool,
221    /// Map of the row IDs to the indices of `formatted_rows`
222    indexed_ids: HashMap<i64, usize>,
223    /// The last ID that was used for a new row in the table.
224    last_id_used: i64,
225    /// Handles auto scroll operation when dragging
226    auto_scroll: AutoScroll,
227    /// Handles auto recreating the displayed rows with the latest data
228    auto_reload: AutoReload,
229    /// Whether to select the entire row when dragging and selecting instead of a single cell
230    select_full_row: bool,
231    /// Whether to add a horizontal scrollbar
232    horizontal_scroll: bool,
233    /// Additional Parameters passed by you, available when creating new rows or header. Can
234    /// contain anything implementing the `Default` trait
235    pub config: Conf,
236    /// Whether to add the row serial column to the table
237    add_serial_column: bool,
238    /// The row height for the table, defaults to 25.0
239    row_height: f32,
240}
241
242impl<Row, F, Conf> SelectableTable<Row, F, Conf>
243where
244    Row: Clone + Send + Sync,
245    F: Eq
246        + Hash
247        + Clone
248        + Ord
249        + Send
250        + Sync
251        + Default
252        + ColumnOperations<Row, F, Conf>
253        + ColumnOrdering<Row>,
254    Conf: Default,
255{
256    /// Creates a new `SelectableTable` with the provided columns in a specified order.
257    ///
258    /// # Parameters:
259    /// - `columns`: A `Vec<F>` representing the columns. Columns must be passed in the correct order (e.g., 1 to 10).
260    ///
261    /// # Returns:
262    /// - A new instance of `SelectableTable`.
263    ///
264    /// # Example:
265    /// ```rust,ignore
266    /// let table = SelectableTable::new(vec![col1, col2, col3]);
267    /// ```
268    #[must_use]
269    pub fn new(columns: Vec<F>) -> Self {
270        let all_columns = columns.clone();
271        let mut column_number = HashMap::new();
272
273        for (index, col) in columns.into_iter().enumerate() {
274            column_number.insert(col, index);
275        }
276        Self {
277            all_columns,
278            column_number,
279            last_id_used: 0,
280            rows: HashMap::new(),
281            formatted_rows: Vec::new(),
282            sorted_by: F::default(),
283            sort_order: SortOrder::default(),
284            drag_started_on: None,
285            active_columns: HashSet::new(),
286            active_rows: HashSet::new(),
287            last_active_row: None,
288            last_active_column: None,
289            beyond_drag_point: false,
290            indexed_ids: HashMap::new(),
291            auto_scroll: AutoScroll::default(),
292            auto_reload: AutoReload::default(),
293            select_full_row: false,
294            horizontal_scroll: false,
295            config: Conf::default(),
296            add_serial_column: false,
297            row_height: 25.0,
298        }
299    }
300
301    /// Updates the table's configuration with the given `conf`.
302    ///
303    /// # Parameters:
304    /// - `conf`: The new configuration of type `Conf`, which is user-defined and allows
305    ///   passing data to help with row/table modification.
306    ///
307    /// # Example:
308    /// ```rust,ignore
309    /// table.set_config(my_config);
310    /// ```
311    pub fn set_config(&mut self, conf: Conf) {
312        self.config = conf;
313    }
314
315    /// Sets a configuration in a builder-style pattern.
316    ///
317    /// # Parameters:
318    /// - `conf`: A configuration of type `Conf`. The user can pass any data to help with row creation or modification.
319    ///
320    /// # Returns:
321    /// - The updated `SelectableTable` with the new configuration applied.
322    ///
323    /// # Example:
324    /// ```rust,ignore
325    /// let table = SelectableTable::new(vec![col1, col2, col3]).config(my_config);
326    /// ```
327    #[must_use]
328    pub fn config(mut self, conf: Conf) -> Self {
329        self.config = conf;
330        self
331    }
332
333    /// Clears all rows from the table, including the displayed ones
334    ///
335    /// # Example:
336    /// ```rust,ignore
337    /// table.clear_all_rows();
338    /// ```
339    pub fn clear_all_rows(&mut self) {
340        self.rows.clear();
341        self.formatted_rows.clear();
342        self.active_rows.clear();
343        self.active_columns.clear();
344        self.last_id_used = 0;
345    }
346
347    /// Displays the UI for the table and uses the provided `TableBuilder` for creating the table UI.
348    ///
349    /// # Parameters:
350    /// - `ui`: The UI context where the table will be rendered.
351    /// - `table_builder`: A closure that receives and modifies the `TableBuilder`.
352    ///
353    /// # Example:
354    /// ```rust,ignore
355    /// table.show_ui(ui, |builder| builder.column(column1));
356    /// ```
357    pub fn show_ui<Fn>(&mut self, ui: &mut Ui, table_builder: Fn)
358    where
359        Fn: FnOnce(TableBuilder) -> TableBuilder,
360    {
361        let is_ctrl_pressed = ui.ctx().input(|i| i.modifiers.ctrl);
362        let key_a_pressed = ui.ctx().input(|i| i.key_pressed(Key::A));
363        let copy_initiated = ui.ctx().input(|i| i.events.contains(&Event::Copy));
364        let ctx = ui.ctx().clone();
365
366        if copy_initiated {
367            self.copy_selected_cells(ui);
368        }
369        if is_ctrl_pressed && key_a_pressed {
370            self.select_all();
371        }
372
373        let pointer = ui.input(|i| i.pointer.hover_pos());
374        let max_rect = ui.max_rect();
375
376        if self.horizontal_scroll {
377            ScrollArea::horizontal().show(ui, |ui| {
378                let mut table = TableBuilder::new(ui);
379
380                if self.add_serial_column {
381                    table = table.column(Column::initial(25.0).clip(true));
382                }
383
384                table = table_builder(table);
385
386                if self.drag_started_on.is_some() {
387                    if let Some(offset) = self.auto_scroll.start_scroll(max_rect, pointer) {
388                        table = table.vertical_scroll_offset(offset);
389                        ctx.request_repaint();
390                    }
391                }
392
393                let output = table
394                    .header(20.0, |header| {
395                        self.build_head(header);
396                    })
397                    .body(|body| {
398                        body.rows(self.row_height, self.formatted_rows.len(), |row| {
399                            let index = row.index();
400                            self.build_body(row, index);
401                        });
402                    });
403                let scroll_offset = output.state.offset.y;
404                self.update_scroll_offset(scroll_offset);
405            });
406        } else {
407            let mut table = TableBuilder::new(ui);
408
409            if self.add_serial_column {
410                table = table.column(Column::initial(25.0).clip(true));
411            }
412
413            table = table_builder(table);
414
415            if self.drag_started_on.is_some() {
416                if let Some(offset) = self.auto_scroll.start_scroll(max_rect, pointer) {
417                    table = table.vertical_scroll_offset(offset);
418                    ctx.request_repaint();
419                }
420            }
421
422            let output = table
423                .header(20.0, |header| {
424                    self.build_head(header);
425                })
426                .body(|body| {
427                    body.rows(self.row_height, self.formatted_rows.len(), |row| {
428                        let index = row.index();
429                        self.build_body(row, index);
430                    });
431                });
432            let scroll_offset = output.state.offset.y;
433            self.update_scroll_offset(scroll_offset);
434        }
435    }
436
437    fn build_head(&mut self, mut header: TableRow) {
438        if self.add_serial_column {
439            header.col(|ui| {
440                ui.add_sized(ui.available_size(), Label::new(""));
441            });
442        }
443        for column_name in &self.all_columns.clone() {
444            header.col(|ui| {
445                let sort_order = if &self.sorted_by == column_name {
446                    Some(self.sort_order)
447                } else {
448                    None
449                };
450
451                let Some(resp) = column_name.create_header(ui, sort_order, self) else {
452                    return;
453                };
454
455                // Response click sense is not forced. So if a header should not be used
456                // for sorting, without click there won't be any actions.
457
458                if resp.clicked() {
459                    let is_selected = &self.sorted_by == column_name;
460                    if is_selected {
461                        self.change_sort_order();
462                    } else {
463                        self.change_sorted_by(column_name);
464                    }
465                    self.recreate_rows();
466                }
467            });
468        }
469    }
470
471    fn build_body(&mut self, mut row: TableRow, index: usize) {
472        let row_data = self.formatted_rows[index].clone();
473
474        if self.add_serial_column {
475            row.col(|ui| {
476                ui.add_sized(ui.available_size(), Label::new(format!("{}", index + 1)));
477            });
478        }
479        self.handle_table_body(row, &row_data);
480    }
481
482    /// Change the current sort order from ascending to descending and vice versa. Will unselect
483    /// all selected rows
484    fn change_sort_order(&mut self) {
485        self.unselect_all();
486        if matches!(self.sort_order, SortOrder::Ascending) {
487            self.sort_order = SortOrder::Descending;
488        } else {
489            self.sort_order = SortOrder::Ascending;
490        }
491    }
492
493    /// Change the column that is currently being used for sorting. Will unselect all rows
494    fn change_sorted_by(&mut self, sort_by: &F) {
495        self.unselect_all();
496        self.sorted_by = sort_by.clone();
497        self.sort_order = SortOrder::default();
498    }
499
500    /// Recreates the rows shown in the UI for the next frame load.
501    ///
502    /// # Important:
503    /// - Any direct modifications made using [`modify_shown_row`](#method.modify_shown_row)
504    ///   will be **cleared** when this is called.
505    ///   To preserve changes, use [`add_modify_row`](#method.add_modify_row) to update row data instead.
506    ///
507    /// # Performance:
508    /// - Should be used sparingly for large datasets, as frequent calls can lead to performance issues.
509    /// - Consider calling after every X number of row updates, depending on update frequency,
510    ///   or use [`auto_reload`](#method.auto_reload) for automatic reload.
511    ///
512    /// # Example:
513    /// ```rust,ignore
514    /// table.recreate_rows();
515    /// ```
516    pub fn recreate_rows(&mut self) {
517        self.formatted_rows.clear();
518        self.active_rows.clear();
519        self.active_columns.clear();
520        self.sort_rows();
521    }
522
523    /// Recreates the rows shown in the UI for the next frame load.
524    ///
525    /// This function refreshes the internal row state by clearing and re-sorting the rows
526    /// similar to [`recreate_rows`](#method.recreate_rows), but it **preserves** the currently
527    /// selected rows and re-applies the active column selection to them.
528    ///
529    /// Useful when the UI needs to be refreshed without resetting user interaction state.
530    ///
531    /// # Important:
532    /// - Any direct modifications made to `formatted_rows` using [`modify_shown_row`](#method.modify_shown_row)
533    ///   will be **cleared** when this is called.
534    ///   To preserve changes, use [`add_modify_row`](#method.add_modify_row) to update row data instead.
535    ///
536    /// # Performance:
537    /// - Should be used sparingly for large datasets, as frequent calls can lead to performance issues.
538    /// - Consider calling after every X number of row updates, depending on update frequency,
539    ///   or use [`auto_reload`](#method.auto_reload) for automatic reload.
540    ///
541    /// # Example:
542    /// ```rust,ignore
543    /// table.recreate_rows_no_unselect();
544    /// ```
545    pub fn recreate_rows_no_unselect(&mut self) {
546        self.formatted_rows.clear();
547        self.sort_rows();
548
549        for row in &self.active_rows {
550            let Some(target_index) = self.indexed_ids.get(row) else {
551                continue;
552            };
553            self.formatted_rows[*target_index]
554                .selected_columns
555                .clone_from(&self.active_columns);
556        }
557    }
558
559    /// The first column that was passed by the user
560    fn first_column(&self) -> F {
561        self.all_columns[0].clone()
562    }
563
564    /// The last column that was passed by the user
565    fn last_column(&self) -> F {
566        self.all_columns[self.all_columns.len() - 1].clone()
567    }
568
569    /// Convert a number to a column value
570    fn column_to_num(&self, column: &F) -> usize {
571        *self
572            .column_number
573            .get(column)
574            .expect("Not in the column list")
575    }
576
577    /// Get the next column of the provided column
578    fn next_column(&self, column: &F) -> F {
579        let current_column_num = self.column_to_num(column);
580        if current_column_num == self.all_columns.len() - 1 {
581            self.all_columns[0].clone()
582        } else {
583            self.all_columns[current_column_num + 1].clone()
584        }
585    }
586
587    /// Get the previous column of the provided column
588    fn previous_column(&self, column: &F) -> F {
589        let current_column_num = self.column_to_num(column);
590        if current_column_num == 0 {
591            self.all_columns[self.all_columns.len() - 1].clone()
592        } else {
593            self.all_columns[current_column_num - 1].clone()
594        }
595    }
596
597    /// Builds the table's Body section
598    fn handle_table_body(&mut self, mut row: TableRow, row_data: &SelectableRow<Row, F>) {
599        for column_name in &self.all_columns.clone() {
600            row.col(|ui| {
601                let selected = row_data.selected_columns.contains(column_name);
602                let mut resp = column_name.create_table_row(ui, row_data, selected, self);
603
604                // Drag sense is forced otherwise there is no point of this library.
605                resp = resp.interact(Sense::drag());
606
607                if resp.drag_started() {
608                    // If CTRL is not pressed down and the mouse right click is not pressed, unselect all cells
609                    // Right click for context menu
610                    if !ui.ctx().input(|i| i.modifiers.ctrl)
611                        && !ui.ctx().input(|i| i.pointer.secondary_clicked())
612                    {
613                        self.unselect_all();
614                    }
615                    self.drag_started_on = Some((row_data.id, column_name.clone()));
616                }
617
618                let pointer_released = ui.input(|a| a.pointer.primary_released());
619
620                if pointer_released {
621                    self.last_active_row = None;
622                    self.last_active_column = None;
623                    self.drag_started_on = None;
624                    self.beyond_drag_point = false;
625                }
626
627                if resp.clicked() {
628                    // If CTRL is not pressed down and the mouse right click is not pressed, unselect all cells
629                    if !ui.ctx().input(|i| i.modifiers.ctrl)
630                        && !ui.ctx().input(|i| i.pointer.secondary_clicked())
631                    {
632                        self.unselect_all();
633                    }
634                    self.select_single_row_cell(row_data.id, column_name);
635                }
636
637                if ui.ui_contains_pointer() && self.drag_started_on.is_some() {
638                    if let Some(drag_start) = self.drag_started_on.as_ref() {
639                        // Only call drag either when not on the starting drag row/column or went beyond the
640                        // drag point at least once. Otherwise normal click would be considered as drag
641                        if drag_start.0 != row_data.id
642                            || &drag_start.1 != column_name
643                            || self.beyond_drag_point
644                        {
645                            let is_ctrl_pressed = ui.ctx().input(|i| i.modifiers.ctrl);
646                            self.select_dragged_row_cell(row_data.id, column_name, is_ctrl_pressed);
647                        }
648                    }
649                }
650            });
651        }
652    }
653
654    /// Returns the total number of rows currently being displayed in the UI.
655    ///
656    /// # Returns:
657    /// - `usize`: The number of rows that are formatted and ready for display.
658    pub const fn total_displayed_rows(&self) -> usize {
659        self.formatted_rows.len()
660    }
661
662    /// Returns the total number of rows in the table (both displayed and non-displayed).
663    ///
664    /// # Returns:
665    /// - `usize`: The total number of rows stored in the table, regardless of whether they are being displayed or not.
666    pub fn total_rows(&self) -> usize {
667        self.rows.len()
668    }
669
670    /// Provides a reference to the rows currently being displayed in the UI.
671    ///
672    /// # Returns:
673    /// - `&Vec<SelectableRow<Row, F>>`: A reference to the vector of formatted rows ready for display.
674    pub const fn get_displayed_rows(&self) -> &Vec<SelectableRow<Row, F>> {
675        &self.formatted_rows
676    }
677
678    /// Provides a reference to all rows in the table, regardless of whether they are displayed.
679    ///
680    /// # Returns:
681    /// - `&HashMap<i64, SelectableRow<Row, F>>`: A reference to the entire collection of rows in the table.
682    pub const fn get_all_rows(&self) -> &HashMap<i64, SelectableRow<Row, F>> {
683        &self.rows
684    }
685
686    /// Adds a serial column to the table.
687    ///
688    /// The serial column is automatically generated and displayed at the very left of the table.
689    /// It shows the row number (starting from 1) for each row.
690    ///
691    /// # Returns:
692    /// - `Self`: The modified table with the serial column enabled.
693    ///
694    /// # Example:
695    /// ```rust,ignore
696    /// let table = SelectableTable::new(vec![col1, col2, col3])
697    ///     .config(my_config).serial_column();
698    /// ```
699    #[must_use]
700    pub const fn serial_column(mut self) -> Self {
701        self.add_serial_column = true;
702        self
703    }
704
705    /// Add a horizontal scrollbar to the table
706    ///
707    /// # Returns:
708    /// - `Self`: The modified table with the serial column enabled.
709    ///
710    /// # Example:
711    /// ```rust,ignore
712    /// let table = SelectableTable::new(vec![col1, col2, col3])
713    ///     .horizontal_scroll();
714    /// ```
715    #[must_use]
716    pub const fn horizontal_scroll(mut self) -> Self {
717        self.horizontal_scroll = true;
718        self
719    }
720
721    /// Sets the height rows in the table.
722    ///
723    /// # Parameters:
724    /// - `height`: The desired height for each row in logical points.
725    ///
726    /// # Returns:
727    /// - `Self`: The modified table with the specified row height applied.
728    ///
729    /// # Example:
730    /// ```rust,ignore
731    /// let table = SelectableTable::new(vec![col1, col2, col3])
732    ///     .row_height(24.0);
733    /// ```
734    #[must_use]
735    pub const fn row_height(mut self, height: f32) -> Self {
736        self.row_height = height;
737        self
738    }
739}