egui_selectable_table/
row_selection.rs

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