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}