Skip to main content

egui_selectable_table/
row_selection.rs

1use egui::Ui;
2use egui::ahash::{HashMap, HashMapExt, HashSet, HashSetExt};
3use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
4use std::fmt::Write as _;
5use std::hash::Hash;
6
7use crate::{ColumnOperations, ColumnOrdering, SelectableRow, SelectableTable};
8
9/// Functions related to selection of rows and columns
10#[allow(clippy::too_many_lines)]
11impl<Row, F, Conf> SelectableTable<Row, F, Conf>
12where
13    Row: Clone + Send + Sync,
14    F: Eq
15        + Hash
16        + Clone
17        + Ord
18        + Send
19        + Sync
20        + Default
21        + ColumnOperations<Row, F, Conf>
22        + ColumnOrdering<Row>,
23    Conf: Default,
24{
25    pub(crate) fn select_single_row_cell(&mut self, id: i64, column_name: &F) {
26        self.active_columns.insert(column_name.clone());
27        self.active_rows.insert(id);
28
29        // Should never panic, if it does, either a library issue or it was used incorrectly
30        let target_index = self.indexed_ids.get(&id).expect("target_index not found");
31        let target_row = self
32            .formatted_rows
33            .get_mut(*target_index)
34            .expect("target_row not found");
35
36        if self.select_full_row {
37            self.active_columns.extend(self.all_columns.clone());
38
39            target_row.selected_columns.extend(self.all_columns.clone());
40        } else {
41            target_row.selected_columns.insert(column_name.clone());
42        }
43
44        self.active_rows.insert(id);
45    }
46
47    /// Marks a row as selected, optionally selecting specific columns within the row.
48    ///
49    /// If a list of columns is provided, only those columns are marked as selected for the row.
50    /// If no column list is provided, all columns in the row are marked as selected.
51    ///
52    /// # Parameters:
53    /// - `id`: The unique identifier of the row to mark as selected.
54    /// - `column`: An optional list of columns (`Vec<F>`) to mark as selected within the row. If `None`, all columns are selected.
55    ///
56    /// # Example:
57    /// ```rust,ignore
58    /// table.mark_row_as_selected(42, Some(vec!["Name", "Age"]));
59    /// table.mark_row_as_selected(43, None); // Selects all columns in row 43
60    /// ```
61    pub fn mark_row_as_selected(&mut self, id: i64, column: Option<Vec<F>>) {
62        let Some(target_index) = self.indexed_ids.get(&id) else {
63            return;
64        };
65
66        let Some(target_row) = self.formatted_rows.get_mut(*target_index) else {
67            return;
68        };
69
70        self.active_rows.insert(id);
71
72        if let Some(column_list) = column {
73            self.active_columns.extend(column_list.clone());
74
75            target_row.selected_columns.extend(column_list);
76        } else {
77            self.active_columns.extend(self.all_columns.clone());
78
79            target_row.selected_columns.extend(self.all_columns.clone());
80        }
81    }
82
83    pub(crate) fn select_dragged_row_cell(
84        &mut self,
85        id: i64,
86        column_name: &F,
87        is_ctrl_pressed: bool,
88    ) {
89        // If both same then the mouse is still on the same column on the same row so nothing to process
90        if self.last_active_row == Some(id) && self.last_active_column == Some(column_name.clone())
91        {
92            return;
93        }
94
95        if self.formatted_rows.is_empty() {
96            return;
97        }
98
99        self.active_columns.insert(column_name.clone());
100        self.beyond_drag_point = true;
101
102        let drag_start = self.drag_started_on.clone().expect("Drag start not found");
103
104        // Number of the column of drag starting point and the current cell that we are trying to select
105        let drag_start_num = self.column_to_num(&drag_start.1);
106        let ongoing_column_num = self.column_to_num(column_name);
107
108        let mut new_column_set = HashSet::new();
109
110        let get_previous = ongoing_column_num > drag_start_num;
111        let mut ongoing_val = Some(drag_start.1.clone());
112
113        // row1: column(drag started here) column column
114        // row2: column                    column column
115        // row3: column                    column column
116        // row4: column                    column column (currently here)
117        //
118        // The goal of this is to ensure from the drag starting point to all the columns till the currently here
119        // are considered selected and the rest are removed from active selection even if it was considered active
120        //
121        // During fast mouse movement active rows can contain columns that are not in the range we are targeting
122        // We go from one point to the other point and ensure except those columns nothing else is selected
123        //
124        // No active row removal if ctrl is being pressed!
125        if is_ctrl_pressed {
126            self.active_columns.insert(column_name.clone());
127        } else if ongoing_column_num == drag_start_num {
128            new_column_set.insert(drag_start.1.clone());
129            self.active_columns = new_column_set;
130        } else {
131            while let Some(col) = ongoing_val {
132                let next_column = if get_previous {
133                    self.next_column(&col)
134                } else {
135                    self.previous_column(&col)
136                };
137
138                new_column_set.insert(col);
139
140                if &next_column == column_name {
141                    new_column_set.insert(next_column);
142                    ongoing_val = None;
143                } else {
144                    ongoing_val = Some(next_column);
145                }
146            }
147            self.active_columns = new_column_set;
148        }
149
150        let current_row_index = self
151            .indexed_ids
152            .get(&id)
153            .expect("Current row index not found");
154        // The row the mouse pointer is on
155        let current_row = self
156            .formatted_rows
157            .get_mut(*current_row_index)
158            .expect("Current row not found");
159
160        // If this row already selects the column that we are trying to select, it means the mouse
161        // moved backwards from an active column to another active column.
162        //
163        // Row: column1 column2 (mouse is here) column3 column4
164        //
165        // In this case, if column 3 or 4 is also found in the active selection then
166        // the mouse moved backwards
167        let row_contains_column = current_row.selected_columns.contains(column_name);
168
169        let mut no_checking = false;
170        // If we have some data of the last row and column that the mouse was on, then try to unselect
171        if row_contains_column
172            && self.last_active_row.is_some()
173            && self.last_active_column.is_some()
174        {
175            if let (Some(last_active_column), Some(last_active_row)) =
176                (self.last_active_column.clone(), self.last_active_row)
177            {
178                // Remove the last column selection from the current row where the mouse is if
179                // the previous row and the current one matches
180                //
181                // column column column
182                // column column column
183                // column column (mouse is currently here) column(mouse was here)
184                //
185                // We unselect the bottom right corner column
186                if &last_active_column != column_name && last_active_row == id {
187                    current_row.selected_columns.remove(&last_active_column);
188                    self.active_columns.remove(&last_active_column);
189                }
190
191                // Get the last row where the mouse was
192                let last_row_index = self
193                    .indexed_ids
194                    .get(&last_active_row)
195                    .expect("Last row not found");
196                let last_row = self
197                    .formatted_rows
198                    .get_mut(*last_row_index)
199                    .expect("Last row not found");
200
201                self.last_active_row = Some(id);
202
203                // If on the same row as the last row, then unselect the column from all other select row
204                if id == last_row.id {
205                    if &last_active_column != column_name {
206                        self.last_active_column = Some(column_name.clone());
207                    }
208                } else {
209                    no_checking = true;
210                    // Mouse went 1 row above or below. So just clear all selection from that previous row
211                    last_row.selected_columns.clear();
212                }
213            }
214        } else {
215            // We are in a new row which we have not selected before
216            self.active_rows.insert(current_row.id);
217            self.last_active_row = Some(id);
218            self.last_active_column = Some(column_name.clone());
219            current_row
220                .selected_columns
221                .clone_from(&self.active_columns);
222        }
223
224        let current_row_index = self
225            .indexed_ids
226            .get(&id)
227            .expect("Current row index not found")
228            .to_owned();
229
230        // Get the row number where the drag started on
231        let drag_start_index = self
232            .indexed_ids
233            .get(&drag_start.0)
234            .expect("Could not find drag start")
235            .to_owned();
236
237        if !no_checking {
238            // If drag started on row 1, currently on row 5, check from row 4 to 1 and select all columns
239            // else go through all rows till a row without any selected column is found. Applied both by incrementing or decrementing index.
240            // In case of fast mouse movement following drag started point mitigates the risk of some rows not getting selected
241            self.check_row_selection(true, current_row_index, drag_start_index);
242            self.check_row_selection(false, current_row_index, drag_start_index);
243        }
244        self.remove_row_selection(current_row_index, drag_start_index, is_ctrl_pressed);
245    }
246
247    fn check_row_selection(&mut self, check_previous: bool, index: usize, drag_start: usize) {
248        if index == 0 && check_previous {
249            return;
250        }
251
252        if index + 1 == self.formatted_rows.len() && !check_previous {
253            return;
254        }
255
256        let index = if check_previous { index - 1 } else { index + 1 };
257
258        let current_row = self
259            .formatted_rows
260            .get(index)
261            .expect("Current row not found");
262
263        // If for example drag started on row 5 and ended on row 10 but missed drag on row 7
264        // Mark the rows as selected till the drag start row is hit (if recursively going that way)
265        let unselected_row = if (check_previous && index >= drag_start)
266            || (!check_previous && index <= drag_start)
267        {
268            false
269        } else {
270            current_row.selected_columns.is_empty()
271        };
272
273        let target_row = self
274            .formatted_rows
275            .get_mut(index)
276            .expect("Target row not found");
277
278        if !unselected_row {
279            if self.select_full_row {
280                target_row.selected_columns.extend(self.all_columns.clone());
281            } else {
282                target_row.selected_columns.clone_from(&self.active_columns);
283            }
284            self.active_rows.insert(target_row.id);
285
286            if check_previous {
287                if index != 0 {
288                    self.check_row_selection(check_previous, index, drag_start);
289                }
290            } else if index + 1 != self.formatted_rows.len() {
291                self.check_row_selection(check_previous, index, drag_start);
292            }
293        }
294    }
295
296    fn remove_row_selection(
297        &mut self,
298        current_index: usize,
299        drag_start: usize,
300        is_ctrl_pressed: bool,
301    ) {
302        let active_ids = self.active_rows.clone();
303        for id in active_ids {
304            let ongoing_index = self
305                .indexed_ids
306                .get(&id)
307                .expect("Could not get ongoing index")
308                .to_owned();
309            let target_row = self
310                .formatted_rows
311                .get_mut(ongoing_index)
312                .expect("target row not found");
313
314            if current_index > drag_start {
315                if ongoing_index >= drag_start && ongoing_index <= current_index {
316                    if self.select_full_row {
317                        target_row.selected_columns.extend(self.all_columns.clone());
318                    } else {
319                        target_row.selected_columns.clone_from(&self.active_columns);
320                    }
321                } else if !is_ctrl_pressed {
322                    target_row.selected_columns.clear();
323                    self.active_rows.remove(&target_row.id);
324                }
325            } else if ongoing_index <= drag_start && ongoing_index >= current_index {
326                if self.select_full_row {
327                    target_row.selected_columns.extend(self.all_columns.clone());
328                } else {
329                    target_row.selected_columns.clone_from(&self.active_columns);
330                }
331            } else if !is_ctrl_pressed {
332                target_row.selected_columns.clear();
333                self.active_rows.remove(&target_row.id);
334            }
335        }
336    }
337
338    /// Unselects all currently selected rows and columns.
339    ///
340    /// Clears the selection in both rows and columns, and resets internal tracking of active rows
341    /// and columns. After this call, there will be no selected rows or columns in the table.
342    ///
343    /// # Panics:
344    /// This method will panic if the indexed ID or the corresponding row cannot be found.
345    ///
346    /// # Example:
347    /// ```rust,ignore
348    /// table.unselect_all(); // Unselects everything in the table.
349    /// ```
350    pub fn unselect_all(&mut self) {
351        for id in &self.active_rows {
352            let id_index = self.indexed_ids.get(id).expect("Could not get id index");
353            let target_row = self
354                .formatted_rows
355                .get_mut(*id_index)
356                .expect("Could not get row");
357            target_row.selected_columns.clear();
358        }
359        self.active_columns.clear();
360        self.last_active_row = None;
361        self.last_active_column = None;
362        self.active_rows.clear();
363    }
364
365    /// Selects all rows and columns in the table.
366    ///
367    /// After calling this method, all rows will have all columns selected and visible immediately.
368    ///
369    /// # Example:
370    /// ```rust,ignore
371    /// table.select_all(); // Selects all rows and columns.
372    /// ```
373    pub fn select_all(&mut self) {
374        let mut all_rows = Vec::new();
375
376        for row in &mut self.formatted_rows {
377            row.selected_columns.extend(self.all_columns.clone());
378            all_rows.push(row.id);
379        }
380
381        self.active_columns.extend(self.all_columns.clone());
382        self.active_rows.extend(all_rows);
383        self.last_active_row = None;
384        self.last_active_column = None;
385    }
386
387    /// Retrieves the currently selected rows.
388    ///
389    /// This method returns a vector of the rows that have one or more columns selected.
390    ///
391    /// If the `select_full_row` flag is enabled, it will ensure that all columns are selected for
392    /// each active row.
393    ///
394    /// # Returns:
395    /// A `Vec` of `SelectableRow` instances that are currently selected.
396    ///
397    /// # Example:
398    /// ```rust,ignore
399    /// let selected_rows = table.get_selected_rows();
400    /// ```
401    pub fn get_selected_rows(&mut self) -> Vec<SelectableRow<Row, F>> {
402        let mut selected_rows = Vec::new();
403        if self.select_full_row {
404            self.active_columns.extend(self.all_columns.clone());
405        }
406
407        // Cannot use active rows to iter as that does not maintain any proper format
408        for row in &self.formatted_rows {
409            if row.selected_columns.is_empty() {
410                continue;
411            }
412            selected_rows.push(row.clone());
413
414            // We already got all the active rows if this matches
415            if selected_rows.len() == self.active_rows.len() {
416                break;
417            }
418        }
419        selected_rows
420    }
421
422    /// Retrieves the currently selected rows but in no particular order. Can be faster than
423    /// [`get_selected_rows`](#method.get_selected_rows) as it uses rayon for parallel processing
424    /// and only checks the active rows instead of every single row.
425    ///
426    /// This method returns a vector of the rows that have one or more columns selected.
427    ///
428    /// If the `select_full_row` flag is enabled, it will ensure that all columns are selected for
429    /// each active row.
430    ///
431    /// # Returns:
432    /// A `Vec` of `SelectableRow` instances that are currently selected.
433    ///
434    /// # Example:
435    /// ```rust,ignore
436    /// let selected_rows = table.get_selected_rows();
437    /// ```
438    pub fn get_selected_rows_unsorted(&mut self) -> Vec<SelectableRow<Row, F>> {
439        if self.select_full_row {
440            self.active_columns.extend(self.all_columns.clone());
441        }
442
443        self.active_rows
444            .par_iter()
445            .map(|row_id| {
446                let row_index = self
447                    .indexed_ids
448                    .get(row_id)
449                    .expect("Could not get id index");
450                let target_row = self
451                    .formatted_rows
452                    .get(*row_index)
453                    .expect("Could not get row");
454
455                target_row.clone()
456            })
457            .collect()
458    }
459
460    /// Copies selected cells to the system clipboard in a tabular format.
461    ///
462    /// This method copies only the selected cells from each row to the clipboard, and ensures
463    /// that the column widths align for better readability when pasted into a text editor or spreadsheet.
464    ///
465    /// # Parameters:
466    /// - `ui`: The UI context used for clipboard interaction.
467    ///
468    /// # Example:
469    /// ```rust,ignore
470    /// table.copy_selected_cells(&mut ui);
471    /// ```
472    pub fn copy_selected_cells(&mut self, ui: &mut Ui) {
473        let mut selected_rows = Vec::new();
474        if self.select_full_row {
475            self.active_columns.extend(self.all_columns.clone());
476        }
477
478        let mut column_max_length = HashMap::new();
479
480        // Iter through all the rows and find the rows that have at least one column as selected.
481        // Keep track of the biggest length of a value of a column
482        // active rows cannot be used here because hashset does not maintain an order.
483        // So iterating will give the rows in a different order than what is shown in the ui
484        for row in &self.formatted_rows {
485            if row.selected_columns.is_empty() {
486                continue;
487            }
488
489            for column in &self.active_columns {
490                if row.selected_columns.contains(column) {
491                    let column_text = column.column_text(&row.row_data);
492                    let field_length = column_text.len();
493                    let entry = column_max_length.entry(column).or_insert(0);
494                    if field_length > *entry {
495                        column_max_length.insert(column, field_length);
496                    }
497                }
498            }
499            selected_rows.push(row);
500            // We already got all the active rows if this matches
501            if selected_rows.len() == self.active_rows.len() {
502                break;
503            }
504        }
505
506        let mut to_copy = String::new();
507
508        // Target is to ensure a fixed length after each column value of a row.
509        // If for example highest len is 10 but the current row's
510        // column value is 5, we will add the column value and add 5 more space after that
511        // to ensure alignment
512        for row in selected_rows {
513            let mut ongoing_column = self.first_column();
514            let mut row_text = String::new();
515            loop {
516                if self.active_columns.contains(&ongoing_column)
517                    && row.selected_columns.contains(&ongoing_column)
518                {
519                    let column_text = ongoing_column.column_text(&row.row_data);
520                    let _ = write!(
521                        row_text,
522                        "{:<width$}",
523                        column_text,
524                        width = column_max_length[&ongoing_column] + 1
525                    );
526                } else if self.active_columns.contains(&ongoing_column)
527                    && !row.selected_columns.contains(&ongoing_column)
528                {
529                    let _ = write!(
530                        row_text,
531                        "{:<width$}",
532                        "",
533                        width = column_max_length[&ongoing_column] + 1
534                    );
535                }
536                if self.last_column() == ongoing_column {
537                    break;
538                }
539                ongoing_column = self.next_column(&ongoing_column);
540            }
541            to_copy.push_str(&row_text);
542            to_copy.push('\n');
543        }
544        ui.ctx().copy_text(to_copy);
545    }
546
547    /// Enables the selection of full rows in the table.
548    ///
549    /// After calling this method, selecting any column in a row will result in the entire row being selected.
550    ///
551    /// # Returns:
552    /// A new instance of the table with full row selection enabled.
553    ///
554    /// # Example:
555    /// ```rust,ignore
556    /// let table = SelectableTable::new(vec![col1, col2, col3])
557    ///     .select_full_row();
558    /// ```
559    #[must_use]
560    pub const fn select_full_row(mut self) -> Self {
561        self.select_full_row = true;
562        self
563    }
564
565    /// Sets whether the table should select full rows when a column is selected.
566    ///
567    /// # Parameters:
568    /// - `status`: `true` to enable full row selection, `false` to disable it.
569    ///
570    /// # Example:
571    /// ```rust,ignore
572    /// table.set_select_full_row(true); // Enable full row selection.
573    /// ```
574    pub const fn set_select_full_row(&mut self, status: bool) {
575        self.select_full_row = status;
576    }
577
578    /// Returns the total number of currently selected rows.
579    ///
580    /// # Returns:
581    /// - `usize`: The number of selected rows.
582    ///
583    /// # Example:
584    /// ```rust,ignore
585    /// let selected_count = table.get_total_selected_rows();
586    /// println!("{} rows selected", selected_count);
587    /// ```
588    pub fn get_total_selected_rows(&mut self) -> usize {
589        self.active_rows.len()
590    }
591}