Skip to main content

egui_selectable_table/
row_selection.rs

1use ahash::{HashMap, HashMapExt, HashSet};
2use egui::Ui;
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    /// Selects all cells in the rectangle from (`start_id`, `start_col`) to
26    /// (`end_id`, `end_col`). Used by shift+click range selection.
27    /// For Ctrl+drag, `extend` is used to union with existing selections.
28    pub(crate) fn select_rectangle(
29        &mut self,
30        start_id: i64,
31        start_col: &F,
32        end_id: i64,
33        end_col: &F,
34        extend: bool,
35    ) {
36        let Some(start_idx) = self.indexed_ids.get(&start_id).copied() else {
37            return;
38        };
39        let Some(end_idx) = self.indexed_ids.get(&end_id).copied() else {
40            return;
41        };
42
43        let start_col_num = self.column_to_num(start_col);
44        let end_col_num = self.column_to_num(end_col);
45        let col_min = start_col_num.min(end_col_num);
46        let col_max = start_col_num.max(end_col_num);
47        let row_min = start_idx.min(end_idx);
48        let row_max = start_idx.max(end_idx);
49
50        let col_range: HashSet<F> = (col_min..=col_max)
51            .map(|i| self.all_columns[i].clone())
52            .collect();
53
54        if extend {
55            for col in &col_range {
56                self.active_columns.insert(col.clone());
57            }
58        } else {
59            self.active_columns.clone_from(&col_range);
60        }
61
62        for row_idx in row_min..=row_max {
63            let Some(row) = self.formatted_rows.get_mut(row_idx) else {
64                continue;
65            };
66            if self.select_full_row {
67                row.selected_columns.extend(self.all_columns.clone());
68            } else if extend {
69                row.selected_columns.extend(col_range.clone());
70            } else {
71                row.selected_columns.clone_from(&col_range);
72            }
73            self.active_rows.insert(row.id);
74        }
75    }
76
77    pub(crate) fn select_single_row_cell(&mut self, id: i64, column_name: &F) {
78        self.active_columns.insert(column_name.clone());
79        self.active_rows.insert(id);
80
81        let Some(target_index) = self.indexed_ids.get(&id) else {
82            return;
83        };
84        let Some(target_row) = self.formatted_rows.get_mut(*target_index) else {
85            return;
86        };
87
88        if self.select_full_row {
89            self.active_columns.extend(self.all_columns.clone());
90            target_row.selected_columns.extend(self.all_columns.clone());
91        } else {
92            target_row.selected_columns.insert(column_name.clone());
93        }
94
95        self.active_rows.insert(id);
96    }
97
98    /// Marks a row as selected, optionally selecting specific columns within the row.
99    ///
100    /// If a list of columns is provided, only those columns are marked as selected for the row.
101    /// If no column list is provided, all columns in the row are marked as selected.
102    ///
103    /// # Parameters:
104    /// - `id`: The unique identifier of the row to mark as selected.
105    /// - `column`: An optional list of columns (`Vec<F>`) to mark as selected within the row. If `None`, all columns are selected.
106    ///
107    /// # Example:
108    /// ```rust,ignore
109    /// table.mark_row_as_selected(42, Some(vec!["Name", "Age"]));
110    /// table.mark_row_as_selected(43, None); // Selects all columns in row 43
111    /// ```
112    pub fn mark_row_as_selected(&mut self, id: i64, column: Option<Vec<F>>) {
113        let Some(target_index) = self.indexed_ids.get(&id) else {
114            return;
115        };
116
117        let Some(target_row) = self.formatted_rows.get_mut(*target_index) else {
118            return;
119        };
120
121        self.active_rows.insert(id);
122
123        if let Some(column_list) = column {
124            self.active_columns.extend(column_list.clone());
125
126            target_row.selected_columns.extend(column_list);
127        } else {
128            self.active_columns.extend(self.all_columns.clone());
129
130            target_row.selected_columns.extend(self.all_columns.clone());
131        }
132    }
133
134    /// Called on every frame per cell that the pointer rests on during an active drag.
135    ///
136    /// Handles the core drag-selection logic:
137    /// 1. Computes the column range from drag start to current pointer position
138    /// 2. Updates `active_columns` to reflect exactly the rectangle's column set (not accumulated
139    ///    across multiple drags). For Ctrl+drag this unions with existing columns on each row so
140    ///    independent drag regions don't interfere.
141    /// 3. Handles "backwards movement": when the mouse moves back through previously-selected
142    ///    territory within a drag, it may need to unselect cells that are no longer in the
143    ///    active path (the `row_contains_column` / `no_checking` block)
144    /// 4. Calls `check_row_selection` bidirectionally to fill any rows skipped by fast mouse
145    ///    movement between the drag start row and current row
146    /// 5. Calls `remove_row_selection` to sync rows inside the drag range and clear rows that
147    ///    fell out of it (only for non-Ctrl; Ctrl+drag preserves committed selections)
148    pub(crate) fn select_dragged_row_cell(
149        &mut self,
150        id: i64,
151        column_name: &F,
152        is_ctrl_pressed: bool,
153    ) {
154        // If the pointer hasn't actually moved to a new cell since last frame, nothing to do
155        if self.last_active_row == Some(id) && self.last_active_column == Some(column_name.clone())
156        {
157            return;
158        }
159
160        if self.formatted_rows.is_empty() {
161            return;
162        }
163
164        self.beyond_drag_point = true;
165
166        let drag_start = self.drag_started_on.clone().expect("Drag start not found");
167
168        // Compute the column range from drag start to current position.
169        let drag_start_num = self.column_to_num(&drag_start.1);
170        let ongoing_column_num = self.column_to_num(column_name);
171
172        let col_min = drag_start_num.min(ongoing_column_num);
173        let col_max = drag_start_num.max(ongoing_column_num);
174
175        let new_column_set: HashSet<F> = (col_min..=col_max)
176            .map(|i| self.all_columns[i].clone())
177            .collect();
178
179        if is_ctrl_pressed {
180            for col in &new_column_set {
181                self.active_columns.insert(col.clone());
182            }
183        } else {
184            self.active_columns = new_column_set;
185        }
186
187        let Some(current_row_index) = self.indexed_ids.get(&id).copied() else {
188            return;
189        };
190        let Some(current_row) = self.formatted_rows.get_mut(current_row_index) else {
191            return;
192        };
193
194        // Detect "backwards movement": if the current row already has this column
195        // selected, the mouse may be moving back through previously-selected territory.
196        // We need to unselect cells that are no longer on the active path.
197        let row_contains_column = current_row.selected_columns.contains(column_name);
198
199        let mut no_checking = false;
200        // Backwards movement handling:
201        //   Row: col1 col2 (mouse here) col3 col4
202        // If mouse was on col4 last frame and is now on col2 on the same row,
203        // col4 should be unselected from this row since the mouse retracted.
204        // When crossing to a different row altogether, we clear the entire
205        // previous row's selection so the drag rectangle resizes cleanly.
206        if row_contains_column
207            && self.last_active_row.is_some()
208            && self.last_active_column.is_some()
209        {
210            if let (Some(last_active_column), Some(last_active_row)) =
211                (self.last_active_column.clone(), self.last_active_row)
212            {
213                if &last_active_column != column_name && last_active_row == id {
214                    current_row.selected_columns.remove(&last_active_column);
215                    self.active_columns.remove(&last_active_column);
216                }
217
218                let Some(last_row_index) = self.indexed_ids.get(&last_active_row).copied() else {
219                    return;
220                };
221                let Some(last_row) = self.formatted_rows.get_mut(last_row_index) else {
222                    return;
223                };
224
225                self.last_active_row = Some(id);
226
227                if id == last_row.id {
228                    if &last_active_column != column_name {
229                        self.last_active_column = Some(column_name.clone());
230                    }
231                } else {
232                    no_checking = true;
233                    last_row.selected_columns.clear();
234                }
235            }
236        } else {
237            // First time this drag touches this row. Apply the active column set.
238            // For Ctrl+drag, extend (union) to preserve columns from prior selections.
239            self.active_rows.insert(current_row.id);
240            self.last_active_row = Some(id);
241            self.last_active_column = Some(column_name.clone());
242            if is_ctrl_pressed {
243                current_row
244                    .selected_columns
245                    .extend(self.active_columns.clone());
246            } else {
247                current_row
248                    .selected_columns
249                    .clone_from(&self.active_columns);
250            }
251        }
252
253        let Some(current_row_index) = self.indexed_ids.get(&id).copied() else {
254            return;
255        };
256
257        let Some(drag_start_index) = self.indexed_ids.get(&drag_start.0).copied() else {
258            return;
259        };
260
261        if !no_checking {
262            // Walk both directions from current row toward drag start to fill
263            // any rows skipped by fast mouse movement (missed pointer-in-cell events).
264            self.check_row_selection(true, current_row_index, drag_start_index, is_ctrl_pressed);
265            self.check_row_selection(false, current_row_index, drag_start_index, is_ctrl_pressed);
266        }
267        // Sync rows inside the drag range and clear rows that left it
268        self.remove_row_selection(current_row_index, drag_start_index, is_ctrl_pressed);
269    }
270
271    /// Walks from the current row toward (or away from) the drag-start row, selecting
272    /// intermediate rows to fill gaps from fast mouse movement. Stops when hitting either
273    /// the drag-start row boundary or a row with no selection (end of previously-selected
274    /// territory). Uses a loop instead of recursion to avoid stack overflow on large tables.
275    fn check_row_selection(
276        &mut self,
277        check_previous: bool,
278        index: usize,
279        drag_start: usize,
280        is_ctrl_pressed: bool,
281    ) {
282        let mut idx = index;
283        loop {
284            if idx == 0 && check_previous {
285                return;
286            }
287            if idx + 1 == self.formatted_rows.len() && !check_previous {
288                return;
289            }
290            idx = if check_previous { idx - 1 } else { idx + 1 };
291
292            let Some(current_row) = self.formatted_rows.get(idx) else {
293                return;
294            };
295
296            // Consider a row "unselected" when we've walked past the drag-start
297            // boundary AND the row has no columns selected (meaning it's beyond
298            // the previously-selected range). Rows between current and drag-start
299            // are always treated as selected (unselected_row = false) so they
300            // get filled in even if they were missed by fast mouse movement.
301            let unselected_row = if (check_previous && idx >= drag_start)
302                || (!check_previous && idx <= drag_start)
303            {
304                false
305            } else {
306                current_row.selected_columns.is_empty()
307            };
308
309            if unselected_row {
310                return;
311            }
312
313            let Some(target_row) = self.formatted_rows.get_mut(idx) else {
314                return;
315            };
316
317            if self.select_full_row {
318                target_row.selected_columns.extend(self.all_columns.clone());
319            } else if is_ctrl_pressed {
320                target_row
321                    .selected_columns
322                    .extend(self.active_columns.clone());
323            } else {
324                target_row.selected_columns.clone_from(&self.active_columns);
325            }
326            self.active_rows.insert(target_row.id);
327        }
328    }
329
330    /// Iterates all currently-active rows and syncs their selection state.
331    /// Rows inside the drag range get their columns updated to match `active_columns`.
332    /// For non-Ctrl drags, rows outside the range are cleared entirely.
333    /// For Ctrl+drags, rows outside the range are left untouched (preserving prior selections).
334    fn remove_row_selection(
335        &mut self,
336        current_index: usize,
337        drag_start: usize,
338        is_ctrl_pressed: bool,
339    ) {
340        let active_ids = self.active_rows.clone();
341        for id in active_ids {
342            let Some(ongoing_index) = self.indexed_ids.get(&id).copied() else {
343                continue;
344            };
345            let Some(target_row) = self.formatted_rows.get_mut(ongoing_index) else {
346                continue;
347            };
348
349            if current_index > drag_start {
350                if ongoing_index >= drag_start && ongoing_index <= current_index {
351                    if self.select_full_row {
352                        target_row.selected_columns.extend(self.all_columns.clone());
353                    } else if is_ctrl_pressed {
354                        target_row
355                            .selected_columns
356                            .extend(self.active_columns.clone());
357                    } else {
358                        target_row.selected_columns.clone_from(&self.active_columns);
359                    }
360                } else if !is_ctrl_pressed {
361                    target_row.selected_columns.clear();
362                    self.active_rows.remove(&target_row.id);
363                }
364            } else if ongoing_index <= drag_start && ongoing_index >= current_index {
365                if self.select_full_row {
366                    target_row.selected_columns.extend(self.all_columns.clone());
367                } else if is_ctrl_pressed {
368                    target_row
369                        .selected_columns
370                        .extend(self.active_columns.clone());
371                } else {
372                    target_row.selected_columns.clone_from(&self.active_columns);
373                }
374            } else if !is_ctrl_pressed {
375                target_row.selected_columns.clear();
376                self.active_rows.remove(&target_row.id);
377            }
378        }
379    }
380
381    /// Unselects all currently selected rows and columns.
382    ///
383    /// Clears the selection in both rows and columns, and resets internal tracking of active rows
384    /// and columns. After this call, there will be no selected rows or columns in the table.
385    ///
386    /// # Panics:
387    /// This method will panic if the indexed ID or the corresponding row cannot be found.
388    ///
389    /// # Example:
390    /// ```rust,ignore
391    /// table.unselect_all(); // Unselects everything in the table.
392    /// ```
393    pub fn unselect_all(&mut self) {
394        for id in &self.active_rows {
395            if let Some(id_index) = self.indexed_ids.get(id)
396                && let Some(target_row) = self.formatted_rows.get_mut(*id_index)
397            {
398                target_row.selected_columns.clear();
399            }
400        }
401        self.active_columns.clear();
402        self.last_active_row = None;
403        self.last_active_column = None;
404        self.active_rows.clear();
405    }
406
407    /// Selects all rows and columns in the table.
408    ///
409    /// After calling this method, all rows will have all columns selected and visible immediately.
410    ///
411    /// # Example:
412    /// ```rust,ignore
413    /// table.select_all(); // Selects all rows and columns.
414    /// ```
415    pub fn select_all(&mut self) {
416        self.active_columns.extend(self.all_columns.clone());
417        self.active_rows.clear();
418        self.last_active_row = None;
419        self.last_active_column = None;
420
421        for row in &mut self.formatted_rows {
422            if row.selected_columns.len() != self.all_columns.len() {
423                row.selected_columns.extend(self.all_columns.clone());
424            }
425            self.active_rows.insert(row.id);
426        }
427    }
428
429    /// Retrieves the currently selected rows.
430    ///
431    /// This method returns a vector of the rows that have one or more columns selected.
432    ///
433    /// If the `select_full_row` flag is enabled, it will ensure that all columns are selected for
434    /// each active row.
435    ///
436    /// # Returns:
437    /// A `Vec` of `SelectableRow` instances that are currently selected.
438    ///
439    /// # Example:
440    /// ```rust,ignore
441    /// let selected_rows = table.get_selected_rows();
442    /// ```
443    pub fn get_selected_rows(&mut self) -> Vec<SelectableRow<Row, F>> {
444        let mut selected_rows = Vec::new();
445        if self.select_full_row {
446            self.active_columns.extend(self.all_columns.clone());
447        }
448
449        // Cannot use active rows to iter as that does not maintain any proper format
450        for row in &self.formatted_rows {
451            if row.selected_columns.is_empty() {
452                continue;
453            }
454            selected_rows.push(row.clone());
455
456            // We already got all the active rows if this matches
457            if selected_rows.len() == self.active_rows.len() {
458                break;
459            }
460        }
461        selected_rows
462    }
463
464    /// Retrieves the currently selected rows but in no particular order. Can be faster than
465    /// [`get_selected_rows`](#method.get_selected_rows) as it uses rayon for parallel processing
466    /// and only checks the active rows instead of every single row.
467    ///
468    /// This method returns a vector of the rows that have one or more columns selected.
469    ///
470    /// If the `select_full_row` flag is enabled, it will ensure that all columns are selected for
471    /// each active row.
472    ///
473    /// # Returns:
474    /// A `Vec` of `SelectableRow` instances that are currently selected.
475    ///
476    /// # Example:
477    /// ```rust,ignore
478    /// let selected_rows = table.get_selected_rows();
479    /// ```
480    pub fn get_selected_rows_unsorted(&mut self) -> Vec<SelectableRow<Row, F>> {
481        if self.select_full_row {
482            self.active_columns.extend(self.all_columns.clone());
483        }
484
485        self.active_rows
486            .par_iter()
487            .map(|row_id| {
488                let row_index = self
489                    .indexed_ids
490                    .get(row_id)
491                    .expect("Could not get id index");
492                let target_row = self
493                    .formatted_rows
494                    .get(*row_index)
495                    .expect("Could not get row");
496
497                target_row.clone()
498            })
499            .collect()
500    }
501
502    /// Copies selected cells to the system clipboard in a tabular format.
503    ///
504    /// This method copies only the selected cells from each row to the clipboard, and ensures
505    /// that the column widths align for better readability when pasted into a text editor or spreadsheet.
506    ///
507    /// # Parameters:
508    /// - `ui`: The UI context used for clipboard interaction.
509    ///
510    /// # Example:
511    /// ```rust,ignore
512    /// table.copy_selected_cells(&mut ui);
513    /// ```
514    pub fn copy_selected_cells(&mut self, ui: &mut Ui) {
515        let mut selected_rows = Vec::new();
516        if self.select_full_row {
517            self.active_columns.extend(self.all_columns.clone());
518        }
519
520        let mut column_max_length = HashMap::new();
521
522        // Iter through all the rows and find the rows that have at least one column as selected.
523        // Keep track of the biggest length of a value of a column
524        // active rows cannot be used here because hashset does not maintain an order.
525        // So iterating will give the rows in a different order than what is shown in the ui
526        for row in &self.formatted_rows {
527            if row.selected_columns.is_empty() {
528                continue;
529            }
530
531            for column in &self.active_columns {
532                if row.selected_columns.contains(column) {
533                    let column_text = column.column_text(&row.row_data);
534                    let field_length = column_text.len();
535                    let entry = column_max_length.entry(column).or_insert(0);
536                    if field_length > *entry {
537                        column_max_length.insert(column, field_length);
538                    }
539                }
540            }
541            selected_rows.push(row);
542            // We already got all the active rows if this matches
543            if selected_rows.len() == self.active_rows.len() {
544                break;
545            }
546        }
547
548        let mut to_copy = String::new();
549
550        // Target is to ensure a fixed length after each column value of a row.
551        // If for example highest len is 10 but the current row's
552        // column value is 5, we will add the column value and add 5 more space after that
553        // to ensure alignment
554        for row in selected_rows {
555            let mut ongoing_column = self.first_column();
556            let mut row_text = String::new();
557            loop {
558                if self.active_columns.contains(&ongoing_column)
559                    && row.selected_columns.contains(&ongoing_column)
560                {
561                    let column_text = ongoing_column.column_text(&row.row_data);
562                    let _ = write!(
563                        row_text,
564                        "{:<width$}",
565                        column_text,
566                        width = column_max_length[&ongoing_column] + 1
567                    );
568                } else if self.active_columns.contains(&ongoing_column)
569                    && !row.selected_columns.contains(&ongoing_column)
570                {
571                    let _ = write!(
572                        row_text,
573                        "{:<width$}",
574                        "",
575                        width = column_max_length[&ongoing_column] + 1
576                    );
577                }
578                if self.last_column() == ongoing_column {
579                    break;
580                }
581                ongoing_column = self.next_column(&ongoing_column);
582            }
583            to_copy.push_str(&row_text);
584            to_copy.push('\n');
585        }
586        ui.ctx().copy_text(to_copy);
587    }
588
589    /// Enables the selection of full rows in the table.
590    ///
591    /// After calling this method, selecting any column in a row will result in the entire row being selected.
592    ///
593    /// # Returns:
594    /// A new instance of the table with full row selection enabled.
595    ///
596    /// # Example:
597    /// ```rust,ignore
598    /// let table = SelectableTable::new(vec![col1, col2, col3])
599    ///     .select_full_row();
600    /// ```
601    #[must_use]
602    pub const fn select_full_row(mut self) -> Self {
603        self.select_full_row = true;
604        self
605    }
606
607    /// Sets whether the table should select full rows when a column is selected.
608    ///
609    /// # Parameters:
610    /// - `status`: `true` to enable full row selection, `false` to disable it.
611    ///
612    /// # Example:
613    /// ```rust,ignore
614    /// table.set_select_full_row(true); // Enable full row selection.
615    /// ```
616    pub const fn set_select_full_row(&mut self, status: bool) {
617        self.select_full_row = status;
618    }
619
620    /// Returns the total number of currently selected rows.
621    ///
622    /// # Returns:
623    /// - `usize`: The number of selected rows.
624    ///
625    /// # Example:
626    /// ```rust,ignore
627    /// let selected_count = table.get_total_selected_rows();
628    /// println!("{} rows selected", selected_count);
629    /// ```
630    pub fn get_total_selected_rows(&mut self) -> usize {
631        self.active_rows.len()
632    }
633}