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}