1use std::cell::Cell;
15use std::cell::RefCell;
16use std::collections::BTreeSet;
17use std::rc::Rc;
18
19use teksilo_core::signal::Signal;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum TableSelectionMode {
24 None,
26 SingleRow,
28 #[default]
31 MultiRow,
32 SingleCell,
34 MultiCell,
36}
37
38impl TableSelectionMode {
39 pub fn is_cell_mode(self) -> bool {
41 matches!(self, Self::SingleCell | Self::MultiCell)
42 }
43
44 pub fn is_multi(self) -> bool {
46 matches!(self, Self::MultiRow | Self::MultiCell)
47 }
48}
49
50pub struct CellSelectionModel {
57 mode: TableSelectionMode,
58 selection: Signal<BTreeSet<(usize, usize)>>,
59 anchor: Rc<Cell<Option<(usize, usize)>>>,
60 base: Rc<RefCell<BTreeSet<(usize, usize)>>>,
66}
67
68impl CellSelectionModel {
69 pub fn new(mode: TableSelectionMode) -> Self {
72 assert!(
73 mode.is_cell_mode(),
74 "CellSelectionModel requires SingleCell or MultiCell mode (got {mode:?})"
75 );
76 Self {
77 mode,
78 selection: Signal::new(BTreeSet::new()),
79 anchor: Rc::new(Cell::new(None)),
80 base: Rc::new(RefCell::new(BTreeSet::new())),
81 }
82 }
83
84 pub fn mode(&self) -> TableSelectionMode {
85 self.mode
86 }
87
88 pub fn selection_signal(&self) -> Signal<BTreeSet<(usize, usize)>> {
89 self.selection.clone()
90 }
91
92 pub fn is_selected(&self, row: usize, col: usize) -> bool {
93 self.selection.get().contains(&(row, col))
94 }
95
96 pub fn count(&self) -> usize {
97 self.selection.get().len()
98 }
99
100 pub fn select(&self, row: usize, col: usize) {
103 if self.mode == TableSelectionMode::None {
104 return;
105 }
106 let mut s = BTreeSet::new();
107 s.insert((row, col));
108 self.selection.set(s);
109 self.anchor.set(Some((row, col)));
110 self.base.borrow_mut().clear();
113 }
114
115 pub fn toggle(&self, row: usize, col: usize) {
118 match self.mode {
119 TableSelectionMode::None => {}
120 TableSelectionMode::SingleCell => self.select(row, col),
121 TableSelectionMode::MultiCell => {
122 let mut s = self.selection.get();
123 if !s.insert((row, col)) {
124 s.remove(&(row, col));
125 }
126 self.selection.set(s.clone());
127 self.anchor.set(Some((row, col)));
128 *self.base.borrow_mut() = s;
132 }
133 TableSelectionMode::SingleRow | TableSelectionMode::MultiRow => {}
134 }
135 }
136
137 pub fn extend_to(&self, row: usize, col: usize) {
141 match self.mode {
142 TableSelectionMode::None => {}
143 TableSelectionMode::SingleCell => self.select(row, col),
144 TableSelectionMode::MultiCell => {
145 let anchor = self.anchor.get().unwrap_or((row, col));
146 let r0 = anchor.0.min(row);
147 let r1 = anchor.0.max(row);
148 let c0 = anchor.1.min(col);
149 let c1 = anchor.1.max(col);
150 let mut s = self.base.borrow().clone();
155 for r in r0..=r1 {
156 for c in c0..=c1 {
157 s.insert((r, c));
158 }
159 }
160 self.selection.set(s);
161 }
163 TableSelectionMode::SingleRow | TableSelectionMode::MultiRow => {}
164 }
165 }
166
167 pub fn select_cells(&self, cells: impl IntoIterator<Item = (usize, usize)>) {
179 if self.mode != TableSelectionMode::MultiCell {
180 return;
181 }
182 let set: BTreeSet<(usize, usize)> = cells.into_iter().collect();
183 self.selection.set(set.clone());
184 *self.base.borrow_mut() = set;
185 self.anchor.set(None);
186 }
187
188 pub fn select_all(&self, row_count: usize, col_count: usize) {
189 if self.mode == TableSelectionMode::None {
190 return;
191 }
192 let mut s = BTreeSet::new();
193 for r in 0..row_count {
194 for c in 0..col_count {
195 s.insert((r, c));
196 }
197 }
198 self.selection.set(s.clone());
199 *self.base.borrow_mut() = s;
202 }
203
204 pub fn clear(&self) {
205 self.selection.set(BTreeSet::new());
206 self.anchor.set(None);
207 self.base.borrow_mut().clear();
208 }
209
210 fn remap_base(&self, f: impl Fn((usize, usize)) -> Option<(usize, usize)>) {
214 let mut b = self.base.borrow_mut();
215 if b.is_empty() {
216 return;
217 }
218 *b = b.iter().filter_map(|&cell| f(cell)).collect();
219 }
220
221 pub fn adjust_for_row_insert(&self, at_row: usize, count: usize) {
224 let old = self.selection.get();
225 let mut new = BTreeSet::new();
226 for &(r, c) in &old {
227 if r >= at_row {
228 new.insert((r + count, c));
229 } else {
230 new.insert((r, c));
231 }
232 }
233 if new != old {
234 self.selection.set(new);
235 }
236 self.remap_base(|(r, c)| Some(if r >= at_row { (r + count, c) } else { (r, c) }));
237 if let Some((r, c)) = self.anchor.get()
238 && r >= at_row
239 {
240 self.anchor.set(Some((r + count, c)));
241 }
242 }
243
244 pub fn adjust_for_row_remove(&self, at_row: usize, count: usize) {
248 let old = self.selection.get();
249 let end = at_row + count;
250 let mut new = BTreeSet::new();
251 for &(r, c) in &old {
252 if r < at_row {
253 new.insert((r, c));
254 } else if r >= end {
255 new.insert((r - count, c));
256 }
257 }
259 if new != old {
260 self.selection.set(new);
261 }
262 self.remap_base(|(r, c)| {
263 if r < at_row {
264 Some((r, c))
265 } else if r >= end {
266 Some((r - count, c))
267 } else {
268 None
269 }
270 });
271 if let Some((r, c)) = self.anchor.get() {
272 if r >= end {
273 self.anchor.set(Some((r - count, c)));
274 } else if r >= at_row {
275 self.anchor.set(None);
276 }
277 }
278 }
279
280 pub fn adjust_for_row_move(&self, from: usize, to: usize, count: usize) {
284 if from == to || count == 0 {
285 return;
286 }
287 let map = |r: usize| teksilo_data::map_index_after_move(r, from, to, count);
288 let old = self.selection.get();
289 let new: BTreeSet<(usize, usize)> = old.iter().map(|&(r, c)| (map(r), c)).collect();
290 if new != old {
291 self.selection.set(new);
292 }
293 self.remap_base(|(r, c)| Some((map(r), c)));
294 if let Some((r, c)) = self.anchor.get() {
295 self.anchor.set(Some((map(r), c)));
296 }
297 }
298
299 pub fn adjust_for_column_insert(&self, at_col: usize, count: usize) {
311 let old = self.selection.get();
312 let mut new = BTreeSet::new();
313 for &(r, c) in &old {
314 if c >= at_col {
315 new.insert((r, c + count));
316 } else {
317 new.insert((r, c));
318 }
319 }
320 if new != old {
321 self.selection.set(new);
322 }
323 self.remap_base(|(r, c)| Some(if c >= at_col { (r, c + count) } else { (r, c) }));
324 if let Some((r, c)) = self.anchor.get()
325 && c >= at_col
326 {
327 self.anchor.set(Some((r, c + count)));
328 }
329 }
330
331 pub fn adjust_for_column_remove(&self, at_col: usize, count: usize) {
338 let old = self.selection.get();
339 let end = at_col + count;
340 let mut new = BTreeSet::new();
341 for &(r, c) in &old {
342 if c < at_col {
343 new.insert((r, c));
344 } else if c >= end {
345 new.insert((r, c - count));
346 }
347 }
348 if new != old {
349 self.selection.set(new);
350 }
351 self.remap_base(|(r, c)| {
352 if c < at_col {
353 Some((r, c))
354 } else if c >= end {
355 Some((r, c - count))
356 } else {
357 None
358 }
359 });
360 if let Some((r, c)) = self.anchor.get() {
361 if c >= end {
362 self.anchor.set(Some((r, c - count)));
363 } else if c >= at_col {
364 self.anchor.set(None);
365 }
366 }
367 }
368
369 pub(crate) fn remap_columns(&self, old_to_new: &[Option<usize>]) {
380 let map = |c: usize| old_to_new.get(c).copied().flatten();
381 let old = self.selection.get();
382 let new: BTreeSet<(usize, usize)> = old
383 .iter()
384 .filter_map(|&(r, c)| map(c).map(|nc| (r, nc)))
385 .collect();
386 if new != old {
387 self.selection.set(new);
388 }
389 self.remap_base(|(r, c)| map(c).map(|nc| (r, nc)));
390 if let Some((r, c)) = self.anchor.get() {
391 self.anchor.set(map(c).map(|nc| (r, nc)));
392 }
393 }
394}
395
396impl Clone for CellSelectionModel {
397 fn clone(&self) -> Self {
398 Self {
399 mode: self.mode,
400 selection: self.selection.clone(),
401 anchor: self.anchor.clone(),
402 base: self.base.clone(),
403 }
404 }
405}
406
407impl std::fmt::Debug for CellSelectionModel {
408 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
409 f.debug_struct("CellSelectionModel")
410 .field("mode", &self.mode)
411 .field("selected_count", &self.selection.get().len())
412 .finish()
413 }
414}
415
416#[cfg(test)]
417mod tests {
418 use super::*;
419
420 #[test]
421 fn select_replaces_and_sets_anchor() {
422 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
423 m.select(2, 3);
424 assert!(m.is_selected(2, 3));
425 assert_eq!(m.count(), 1);
426 m.select(5, 5);
427 assert!(m.is_selected(5, 5));
428 assert!(!m.is_selected(2, 3));
429 }
430
431 #[test]
432 fn toggle_in_multi_cell_adds_and_removes() {
433 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
434 m.toggle(0, 0);
435 m.toggle(1, 1);
436 assert_eq!(m.count(), 2);
437 m.toggle(0, 0);
438 assert_eq!(m.count(), 1);
439 }
440
441 #[test]
442 fn extend_in_multi_cell_fills_rectangle() {
443 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
444 m.select(2, 1);
445 m.extend_to(4, 3);
446 assert_eq!(m.count(), 9);
448 assert!(m.is_selected(3, 2));
449 }
450
451 #[test]
452 fn extend_shrinks_when_target_moves_inward() {
453 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
456 m.select(0, 0);
457 m.extend_to(2, 2); assert_eq!(m.count(), 9);
459 m.extend_to(1, 1); assert_eq!(m.count(), 4, "rectangle must shrink, not accrete");
461 assert!(
462 !m.is_selected(2, 2),
463 "the dropped corner must be deselected"
464 );
465 }
466
467 #[test]
468 fn ctrl_committed_cells_survive_a_later_shift_extend() {
469 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
472 m.select(0, 0); m.toggle(5, 5); m.extend_to(6, 6); assert!(m.is_selected(0, 0), "Ctrl-committed cell must survive");
476 assert!(m.is_selected(6, 6));
477 m.extend_to(5, 5); assert!(m.is_selected(0, 0), "committed cell still there");
479 assert!(!m.is_selected(6, 6), "shrunk-away cell gone");
480 assert_eq!(m.count(), 2); }
482
483 #[test]
484 fn select_all_in_multi_cell() {
485 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
486 m.select_all(3, 4);
487 assert_eq!(m.count(), 12);
488 }
489
490 #[test]
491 fn single_cell_mode_keeps_one_selection() {
492 let m = CellSelectionModel::new(TableSelectionMode::SingleCell);
493 m.select(1, 1);
494 m.toggle(2, 2);
495 assert_eq!(m.count(), 1);
496 assert!(m.is_selected(2, 2));
497 }
498
499 #[test]
500 fn adjust_for_row_insert_shifts_higher_rows() {
501 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
502 m.select(2, 0);
503 m.toggle(4, 0);
504 m.adjust_for_row_insert(3, 2);
505 assert!(m.is_selected(2, 0));
506 assert!(m.is_selected(6, 0));
507 }
508
509 #[test]
510 fn adjust_for_row_remove_drops_in_range() {
511 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
512 m.select(1, 0);
513 m.toggle(3, 0);
514 m.toggle(5, 0);
515 m.adjust_for_row_remove(2, 2);
516 assert!(m.is_selected(1, 0));
519 assert!(m.is_selected(3, 0));
521 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
522 assert_eq!(v, vec![(1, 0), (3, 0)]);
523 }
524
525 #[test]
526 fn adjust_for_row_move_follows_cells() {
527 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
528 m.select(0, 1); m.toggle(1, 2); m.adjust_for_row_move(0, 2, 1);
533 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
534 assert_eq!(v, vec![(0, 2), (2, 1)]);
535 }
536
537 #[test]
538 fn adjust_for_column_insert_and_remove() {
539 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
540 m.select(0, 1);
541 m.toggle(0, 4);
542 m.adjust_for_column_insert(2, 2);
543 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
545 assert_eq!(v, vec![(0, 1), (0, 6)]);
546
547 m.adjust_for_column_remove(0, 2);
548 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
549 assert_eq!(v, vec![(0, 4)]);
551 }
552
553 #[test]
554 fn remap_columns_follows_reorder_and_drops_removed_columns() {
555 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
556 m.select(0, 0); m.toggle(1, 2); let old_to_new = vec![Some(2), None, Some(0)];
561 m.remap_columns(&old_to_new);
562 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
563 assert_eq!(v, vec![(0, 2), (1, 0)], "columns follow their new position");
564 m.extend_to(1, 1);
567 assert!(
568 m.is_selected(1, 0),
569 "remapped anchor (1,0) must survive into the next extend"
570 );
571 }
572
573 #[test]
574 fn remap_columns_drops_selection_in_a_removed_column() {
575 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
576 m.select(3, 1);
577 m.remap_columns(&[Some(0), None]);
579 assert_eq!(m.count(), 0);
580 }
581
582 #[test]
583 #[should_panic]
584 fn cell_model_rejects_row_mode() {
585 let _ = CellSelectionModel::new(TableSelectionMode::MultiRow);
586 }
587
588 #[test]
589 fn mode_is_cell_mode() {
590 assert!(TableSelectionMode::SingleCell.is_cell_mode());
591 assert!(TableSelectionMode::MultiCell.is_cell_mode());
592 assert!(!TableSelectionMode::MultiRow.is_cell_mode());
593 }
594}