1use std::cell::Cell;
15use std::cell::RefCell;
16use std::collections::BTreeSet;
17use std::rc::Rc;
18
19use teksilo_core::build_context::BuildContext;
20use teksilo_core::signal::Signal;
21
22use crate::common::list_nav::Cardinality;
23use crate::data_views::RowSelection;
24
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
27pub enum TableSelectionMode {
28 None,
30 SingleRow,
32 #[default]
35 MultiRow,
36 SingleCell,
38 MultiCell,
40}
41
42impl TableSelectionMode {
43 pub fn is_cell_mode(self) -> bool {
45 matches!(self, Self::SingleCell | Self::MultiCell)
46 }
47
48 pub fn is_multi(self) -> bool {
50 matches!(self, Self::MultiRow | Self::MultiCell)
51 }
52}
53
54pub(crate) fn cardinality(
63 mode: TableSelectionMode,
64 selection: Option<&RowSelection>,
65 cell_selection: Option<&CellSelectionModel>,
66) -> Cardinality {
67 let multi = match mode {
68 TableSelectionMode::MultiRow => {
69 selection.is_none_or(|s| s.mode() == teksilo_data::SelectionMode::Multi)
70 }
71 TableSelectionMode::MultiCell => cell_selection.is_none_or(|cs| cs.mode().is_multi()),
72 TableSelectionMode::SingleRow
73 | TableSelectionMode::SingleCell
74 | TableSelectionMode::None => false,
75 };
76 if multi {
77 Cardinality::Multi
78 } else {
79 Cardinality::Single
80 }
81}
82
83pub(crate) fn follow_the_selection_in_single_mode(
98 ctx: &mut BuildContext,
99 mode: TableSelectionMode,
100 focused_cell: &Signal<Option<(usize, usize)>>,
101 selection: Option<&RowSelection>,
102 cell_selection: Option<&CellSelectionModel>,
103) {
104 if mode == TableSelectionMode::None
105 || cardinality(mode, selection, cell_selection) != Cardinality::Single
106 {
107 return;
108 }
109 let focused = focused_cell.clone();
110 let rows = selection.cloned();
111 let cells = cell_selection.cloned();
112 let follow = move || {
113 put_the_cursor_on_the_selection(mode, &focused, rows.as_ref(), cells.as_ref());
114 };
115 if mode.is_cell_mode() {
116 if let Some(cells) = cell_selection {
117 ctx.effect(&cells.selection_signal(), move |_| follow());
118 }
119 } else if let Some(rows) = selection {
120 let handle = rows.observe_for_rebuild(follow);
121 ctx.own_handle(handle);
122 }
123}
124
125pub(crate) fn put_the_cursor_on_the_selection(
139 mode: TableSelectionMode,
140 focused_cell: &Signal<Option<(usize, usize)>>,
141 selection: Option<&RowSelection>,
142 cell_selection: Option<&CellSelectionModel>,
143) {
144 if mode == TableSelectionMode::None
145 || cardinality(mode, selection, cell_selection) != Cardinality::Single
146 {
147 return;
148 }
149 let Some((row, col)) = focused_cell.get() else {
150 return;
151 };
152 if mode.is_cell_mode() {
153 let Some(cells) = cell_selection else {
154 return;
155 };
156 if !cells.is_selected(row, col)
157 && let Some(&cell) = cells.selection_signal().get().iter().next()
158 {
159 focused_cell.set(Some(cell));
160 }
161 } else {
162 let Some(rows) = selection else {
163 return;
164 };
165 if !rows.is_selected(row)
166 && let Some(&selected) = rows.selected_indices().first()
167 {
168 focused_cell.set(Some((selected, col)));
169 }
170 }
171}
172
173pub struct CellSelectionModel {
180 mode: TableSelectionMode,
181 selection: Signal<BTreeSet<(usize, usize)>>,
182 anchor: Rc<Cell<Option<(usize, usize)>>>,
183 base: Rc<RefCell<BTreeSet<(usize, usize)>>>,
189}
190
191impl CellSelectionModel {
192 pub fn new(mode: TableSelectionMode) -> Self {
195 assert!(
196 mode.is_cell_mode(),
197 "CellSelectionModel requires SingleCell or MultiCell mode (got {mode:?})"
198 );
199 Self {
200 mode,
201 selection: Signal::new(BTreeSet::new()),
202 anchor: Rc::new(Cell::new(None)),
203 base: Rc::new(RefCell::new(BTreeSet::new())),
204 }
205 }
206
207 pub fn mode(&self) -> TableSelectionMode {
208 self.mode
209 }
210
211 pub fn selection_signal(&self) -> Signal<BTreeSet<(usize, usize)>> {
212 self.selection.clone()
213 }
214
215 pub fn is_selected(&self, row: usize, col: usize) -> bool {
216 self.selection.get().contains(&(row, col))
217 }
218
219 pub fn count(&self) -> usize {
220 self.selection.get().len()
221 }
222
223 pub fn select(&self, row: usize, col: usize) {
226 if self.mode == TableSelectionMode::None {
227 return;
228 }
229 let mut s = BTreeSet::new();
230 s.insert((row, col));
231 self.selection.set(s);
232 self.anchor.set(Some((row, col)));
233 self.base.borrow_mut().clear();
236 }
237
238 pub fn toggle(&self, row: usize, col: usize) {
241 match self.mode {
242 TableSelectionMode::None => {}
243 TableSelectionMode::SingleCell => self.select(row, col),
244 TableSelectionMode::MultiCell => {
245 let mut s = self.selection.get();
246 if !s.insert((row, col)) {
247 s.remove(&(row, col));
248 }
249 self.selection.set(s.clone());
250 self.anchor.set(Some((row, col)));
251 *self.base.borrow_mut() = s;
255 }
256 TableSelectionMode::SingleRow | TableSelectionMode::MultiRow => {}
257 }
258 }
259
260 pub fn extend_to(&self, row: usize, col: usize) {
264 match self.mode {
265 TableSelectionMode::None => {}
266 TableSelectionMode::SingleCell => self.select(row, col),
267 TableSelectionMode::MultiCell => {
268 let anchor = self.anchor.get().unwrap_or((row, col));
269 let r0 = anchor.0.min(row);
270 let r1 = anchor.0.max(row);
271 let c0 = anchor.1.min(col);
272 let c1 = anchor.1.max(col);
273 let mut s = self.base.borrow().clone();
278 for r in r0..=r1 {
279 for c in c0..=c1 {
280 s.insert((r, c));
281 }
282 }
283 self.selection.set(s);
284 }
286 TableSelectionMode::SingleRow | TableSelectionMode::MultiRow => {}
287 }
288 }
289
290 pub fn select_cells(&self, cells: impl IntoIterator<Item = (usize, usize)>) {
301 if self.mode != TableSelectionMode::MultiCell {
302 return;
303 }
304 let set: BTreeSet<(usize, usize)> = cells.into_iter().collect();
305 self.selection.set(set.clone());
306 *self.base.borrow_mut() = set;
307 self.anchor.set(None);
308 }
309
310 pub fn select_all(&self, row_count: usize, col_count: usize) {
311 if self.mode == TableSelectionMode::None {
312 return;
313 }
314 let mut s = BTreeSet::new();
315 for r in 0..row_count {
316 for c in 0..col_count {
317 s.insert((r, c));
318 }
319 }
320 self.selection.set(s.clone());
321 *self.base.borrow_mut() = s;
324 }
325
326 pub fn clear(&self) {
327 self.selection.set(BTreeSet::new());
328 self.anchor.set(None);
329 self.base.borrow_mut().clear();
330 }
331
332 fn remap_base(&self, f: impl Fn((usize, usize)) -> Option<(usize, usize)>) {
336 let mut b = self.base.borrow_mut();
337 if b.is_empty() {
338 return;
339 }
340 *b = b.iter().filter_map(|&cell| f(cell)).collect();
341 }
342
343 pub fn adjust_for_row_insert(&self, at_row: usize, count: usize) {
346 let old = self.selection.get();
347 let mut new = BTreeSet::new();
348 for &(r, c) in &old {
349 if r >= at_row {
350 new.insert((r + count, c));
351 } else {
352 new.insert((r, c));
353 }
354 }
355 if new != old {
356 self.selection.set(new);
357 }
358 self.remap_base(|(r, c)| Some(if r >= at_row { (r + count, c) } else { (r, c) }));
359 if let Some((r, c)) = self.anchor.get()
360 && r >= at_row
361 {
362 self.anchor.set(Some((r + count, c)));
363 }
364 }
365
366 pub fn adjust_for_row_remove(&self, at_row: usize, count: usize) {
370 let old = self.selection.get();
371 let end = at_row + count;
372 let mut new = BTreeSet::new();
373 for &(r, c) in &old {
374 if r < at_row {
375 new.insert((r, c));
376 } else if r >= end {
377 new.insert((r - count, c));
378 }
379 }
381 if new != old {
382 self.selection.set(new);
383 }
384 self.remap_base(|(r, c)| {
385 if r < at_row {
386 Some((r, c))
387 } else if r >= end {
388 Some((r - count, c))
389 } else {
390 None
391 }
392 });
393 if let Some((r, c)) = self.anchor.get() {
394 if r >= end {
395 self.anchor.set(Some((r - count, c)));
396 } else if r >= at_row {
397 self.anchor.set(None);
398 }
399 }
400 }
401
402 pub fn adjust_for_row_move(&self, from: usize, to: usize, count: usize) {
406 if from == to || count == 0 {
407 return;
408 }
409 let map = |r: usize| teksilo_data::map_index_after_move(r, from, to, count);
410 let old = self.selection.get();
411 let new: BTreeSet<(usize, usize)> = old.iter().map(|&(r, c)| (map(r), c)).collect();
412 if new != old {
413 self.selection.set(new);
414 }
415 self.remap_base(|(r, c)| Some((map(r), c)));
416 if let Some((r, c)) = self.anchor.get() {
417 self.anchor.set(Some((map(r), c)));
418 }
419 }
420
421 pub fn adjust_for_column_insert(&self, at_col: usize, count: usize) {
433 let old = self.selection.get();
434 let mut new = BTreeSet::new();
435 for &(r, c) in &old {
436 if c >= at_col {
437 new.insert((r, c + count));
438 } else {
439 new.insert((r, c));
440 }
441 }
442 if new != old {
443 self.selection.set(new);
444 }
445 self.remap_base(|(r, c)| Some(if c >= at_col { (r, c + count) } else { (r, c) }));
446 if let Some((r, c)) = self.anchor.get()
447 && c >= at_col
448 {
449 self.anchor.set(Some((r, c + count)));
450 }
451 }
452
453 pub fn adjust_for_column_remove(&self, at_col: usize, count: usize) {
460 let old = self.selection.get();
461 let end = at_col + count;
462 let mut new = BTreeSet::new();
463 for &(r, c) in &old {
464 if c < at_col {
465 new.insert((r, c));
466 } else if c >= end {
467 new.insert((r, c - count));
468 }
469 }
470 if new != old {
471 self.selection.set(new);
472 }
473 self.remap_base(|(r, c)| {
474 if c < at_col {
475 Some((r, c))
476 } else if c >= end {
477 Some((r, c - count))
478 } else {
479 None
480 }
481 });
482 if let Some((r, c)) = self.anchor.get() {
483 if c >= end {
484 self.anchor.set(Some((r, c - count)));
485 } else if c >= at_col {
486 self.anchor.set(None);
487 }
488 }
489 }
490
491 pub(crate) fn remap_columns(&self, old_to_new: &[Option<usize>]) {
502 let map = |c: usize| old_to_new.get(c).copied().flatten();
503 let old = self.selection.get();
504 let new: BTreeSet<(usize, usize)> = old
505 .iter()
506 .filter_map(|&(r, c)| map(c).map(|nc| (r, nc)))
507 .collect();
508 if new != old {
509 self.selection.set(new);
510 }
511 self.remap_base(|(r, c)| map(c).map(|nc| (r, nc)));
512 if let Some((r, c)) = self.anchor.get() {
513 self.anchor.set(map(c).map(|nc| (r, nc)));
514 }
515 }
516}
517
518impl Clone for CellSelectionModel {
519 fn clone(&self) -> Self {
520 Self {
521 mode: self.mode,
522 selection: self.selection.clone(),
523 anchor: self.anchor.clone(),
524 base: self.base.clone(),
525 }
526 }
527}
528
529impl std::fmt::Debug for CellSelectionModel {
530 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
531 f.debug_struct("CellSelectionModel")
532 .field("mode", &self.mode)
533 .field("selected_count", &self.selection.get().len())
534 .finish()
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
543 fn select_replaces_and_sets_anchor() {
544 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
545 m.select(2, 3);
546 assert!(m.is_selected(2, 3));
547 assert_eq!(m.count(), 1);
548 m.select(5, 5);
549 assert!(m.is_selected(5, 5));
550 assert!(!m.is_selected(2, 3));
551 }
552
553 #[test]
554 fn toggle_in_multi_cell_adds_and_removes() {
555 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
556 m.toggle(0, 0);
557 m.toggle(1, 1);
558 assert_eq!(m.count(), 2);
559 m.toggle(0, 0);
560 assert_eq!(m.count(), 1);
561 }
562
563 #[test]
564 fn extend_in_multi_cell_fills_rectangle() {
565 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
566 m.select(2, 1);
567 m.extend_to(4, 3);
568 assert_eq!(m.count(), 9);
570 assert!(m.is_selected(3, 2));
571 }
572
573 #[test]
574 fn extend_shrinks_when_target_moves_inward() {
575 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
578 m.select(0, 0);
579 m.extend_to(2, 2); assert_eq!(m.count(), 9);
581 m.extend_to(1, 1); assert_eq!(m.count(), 4, "rectangle must shrink, not accrete");
583 assert!(
584 !m.is_selected(2, 2),
585 "the dropped corner must be deselected"
586 );
587 }
588
589 #[test]
590 fn ctrl_committed_cells_survive_a_later_shift_extend() {
591 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
594 m.select(0, 0); m.toggle(5, 5); m.extend_to(6, 6); assert!(m.is_selected(0, 0), "Ctrl-committed cell must survive");
598 assert!(m.is_selected(6, 6));
599 m.extend_to(5, 5); assert!(m.is_selected(0, 0), "committed cell still there");
601 assert!(!m.is_selected(6, 6), "shrunk-away cell gone");
602 assert_eq!(m.count(), 2); }
604
605 #[test]
606 fn select_all_in_multi_cell() {
607 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
608 m.select_all(3, 4);
609 assert_eq!(m.count(), 12);
610 }
611
612 #[test]
613 fn single_cell_mode_keeps_one_selection() {
614 let m = CellSelectionModel::new(TableSelectionMode::SingleCell);
615 m.select(1, 1);
616 m.toggle(2, 2);
617 assert_eq!(m.count(), 1);
618 assert!(m.is_selected(2, 2));
619 }
620
621 #[test]
622 fn adjust_for_row_insert_shifts_higher_rows() {
623 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
624 m.select(2, 0);
625 m.toggle(4, 0);
626 m.adjust_for_row_insert(3, 2);
627 assert!(m.is_selected(2, 0));
628 assert!(m.is_selected(6, 0));
629 }
630
631 #[test]
632 fn adjust_for_row_remove_drops_in_range() {
633 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
634 m.select(1, 0);
635 m.toggle(3, 0);
636 m.toggle(5, 0);
637 m.adjust_for_row_remove(2, 2);
638 assert!(m.is_selected(1, 0));
641 assert!(m.is_selected(3, 0));
643 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
644 assert_eq!(v, vec![(1, 0), (3, 0)]);
645 }
646
647 #[test]
648 fn adjust_for_row_move_follows_cells() {
649 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
650 m.select(0, 1); m.toggle(1, 2); m.adjust_for_row_move(0, 2, 1);
655 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
656 assert_eq!(v, vec![(0, 2), (2, 1)]);
657 }
658
659 #[test]
660 fn adjust_for_column_insert_and_remove() {
661 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
662 m.select(0, 1);
663 m.toggle(0, 4);
664 m.adjust_for_column_insert(2, 2);
665 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
667 assert_eq!(v, vec![(0, 1), (0, 6)]);
668
669 m.adjust_for_column_remove(0, 2);
670 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
671 assert_eq!(v, vec![(0, 4)]);
673 }
674
675 #[test]
676 fn remap_columns_follows_reorder_and_drops_removed_columns() {
677 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
678 m.select(0, 0); m.toggle(1, 2); let old_to_new = vec![Some(2), None, Some(0)];
683 m.remap_columns(&old_to_new);
684 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
685 assert_eq!(v, vec![(0, 2), (1, 0)], "columns follow their new position");
686 m.extend_to(1, 1);
689 assert!(
690 m.is_selected(1, 0),
691 "remapped anchor (1,0) must survive into the next extend"
692 );
693 }
694
695 #[test]
696 fn remap_columns_drops_selection_in_a_removed_column() {
697 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
698 m.select(3, 1);
699 m.remap_columns(&[Some(0), None]);
701 assert_eq!(m.count(), 0);
702 }
703
704 #[test]
705 #[should_panic]
706 fn cell_model_rejects_row_mode() {
707 let _ = CellSelectionModel::new(TableSelectionMode::MultiRow);
708 }
709
710 #[test]
711 fn mode_is_cell_mode() {
712 assert!(TableSelectionMode::SingleCell.is_cell_mode());
713 assert!(TableSelectionMode::MultiCell.is_cell_mode());
714 assert!(!TableSelectionMode::MultiRow.is_cell_mode());
715 }
716}