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}