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}