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)>) {
178 if self.mode != TableSelectionMode::MultiCell {
179 return;
180 }
181 let set: BTreeSet<(usize, usize)> = cells.into_iter().collect();
182 self.selection.set(set.clone());
183 *self.base.borrow_mut() = set;
184 self.anchor.set(None);
185 }
186
187 pub fn select_all(&self, row_count: usize, col_count: usize) {
188 if self.mode == TableSelectionMode::None {
189 return;
190 }
191 let mut s = BTreeSet::new();
192 for r in 0..row_count {
193 for c in 0..col_count {
194 s.insert((r, c));
195 }
196 }
197 self.selection.set(s.clone());
198 *self.base.borrow_mut() = s;
201 }
202
203 pub fn clear(&self) {
204 self.selection.set(BTreeSet::new());
205 self.anchor.set(None);
206 self.base.borrow_mut().clear();
207 }
208
209 fn remap_base(&self, f: impl Fn((usize, usize)) -> Option<(usize, usize)>) {
213 let mut b = self.base.borrow_mut();
214 if b.is_empty() {
215 return;
216 }
217 *b = b.iter().filter_map(|&cell| f(cell)).collect();
218 }
219
220 pub fn adjust_for_row_insert(&self, at_row: usize, count: usize) {
223 let old = self.selection.get();
224 let mut new = BTreeSet::new();
225 for &(r, c) in &old {
226 if r >= at_row {
227 new.insert((r + count, c));
228 } else {
229 new.insert((r, c));
230 }
231 }
232 if new != old {
233 self.selection.set(new);
234 }
235 self.remap_base(|(r, c)| Some(if r >= at_row { (r + count, c) } else { (r, c) }));
236 if let Some((r, c)) = self.anchor.get()
237 && r >= at_row
238 {
239 self.anchor.set(Some((r + count, c)));
240 }
241 }
242
243 pub fn adjust_for_row_remove(&self, at_row: usize, count: usize) {
247 let old = self.selection.get();
248 let end = at_row + count;
249 let mut new = BTreeSet::new();
250 for &(r, c) in &old {
251 if r < at_row {
252 new.insert((r, c));
253 } else if r >= end {
254 new.insert((r - count, c));
255 }
256 }
258 if new != old {
259 self.selection.set(new);
260 }
261 self.remap_base(|(r, c)| {
262 if r < at_row {
263 Some((r, c))
264 } else if r >= end {
265 Some((r - count, c))
266 } else {
267 None
268 }
269 });
270 if let Some((r, c)) = self.anchor.get() {
271 if r >= end {
272 self.anchor.set(Some((r - count, c)));
273 } else if r >= at_row {
274 self.anchor.set(None);
275 }
276 }
277 }
278
279 pub fn adjust_for_row_move(&self, from: usize, to: usize, count: usize) {
283 if from == to || count == 0 {
284 return;
285 }
286 let map = |r: usize| teksilo_data::map_index_after_move(r, from, to, count);
287 let old = self.selection.get();
288 let new: BTreeSet<(usize, usize)> = old.iter().map(|&(r, c)| (map(r), c)).collect();
289 if new != old {
290 self.selection.set(new);
291 }
292 self.remap_base(|(r, c)| Some((map(r), c)));
293 if let Some((r, c)) = self.anchor.get() {
294 self.anchor.set(Some((map(r), c)));
295 }
296 }
297
298 pub fn adjust_for_column_insert(&self, at_col: usize, count: usize) {
310 let old = self.selection.get();
311 let mut new = BTreeSet::new();
312 for &(r, c) in &old {
313 if c >= at_col {
314 new.insert((r, c + count));
315 } else {
316 new.insert((r, c));
317 }
318 }
319 if new != old {
320 self.selection.set(new);
321 }
322 self.remap_base(|(r, c)| Some(if c >= at_col { (r, c + count) } else { (r, c) }));
323 if let Some((r, c)) = self.anchor.get()
324 && c >= at_col
325 {
326 self.anchor.set(Some((r, c + count)));
327 }
328 }
329
330 pub fn adjust_for_column_remove(&self, at_col: usize, count: usize) {
337 let old = self.selection.get();
338 let end = at_col + count;
339 let mut new = BTreeSet::new();
340 for &(r, c) in &old {
341 if c < at_col {
342 new.insert((r, c));
343 } else if c >= end {
344 new.insert((r, c - count));
345 }
346 }
347 if new != old {
348 self.selection.set(new);
349 }
350 self.remap_base(|(r, c)| {
351 if c < at_col {
352 Some((r, c))
353 } else if c >= end {
354 Some((r, c - count))
355 } else {
356 None
357 }
358 });
359 if let Some((r, c)) = self.anchor.get() {
360 if c >= end {
361 self.anchor.set(Some((r, c - count)));
362 } else if c >= at_col {
363 self.anchor.set(None);
364 }
365 }
366 }
367
368 pub(crate) fn remap_columns(&self, old_to_new: &[Option<usize>]) {
379 let map = |c: usize| old_to_new.get(c).copied().flatten();
380 let old = self.selection.get();
381 let new: BTreeSet<(usize, usize)> = old
382 .iter()
383 .filter_map(|&(r, c)| map(c).map(|nc| (r, nc)))
384 .collect();
385 if new != old {
386 self.selection.set(new);
387 }
388 self.remap_base(|(r, c)| map(c).map(|nc| (r, nc)));
389 if let Some((r, c)) = self.anchor.get() {
390 self.anchor.set(map(c).map(|nc| (r, nc)));
391 }
392 }
393}
394
395impl Clone for CellSelectionModel {
396 fn clone(&self) -> Self {
397 Self {
398 mode: self.mode,
399 selection: self.selection.clone(),
400 anchor: self.anchor.clone(),
401 base: self.base.clone(),
402 }
403 }
404}
405
406impl std::fmt::Debug for CellSelectionModel {
407 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
408 f.debug_struct("CellSelectionModel")
409 .field("mode", &self.mode)
410 .field("selected_count", &self.selection.get().len())
411 .finish()
412 }
413}
414
415#[cfg(test)]
416mod tests {
417 use super::*;
418
419 #[test]
420 fn select_replaces_and_sets_anchor() {
421 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
422 m.select(2, 3);
423 assert!(m.is_selected(2, 3));
424 assert_eq!(m.count(), 1);
425 m.select(5, 5);
426 assert!(m.is_selected(5, 5));
427 assert!(!m.is_selected(2, 3));
428 }
429
430 #[test]
431 fn toggle_in_multi_cell_adds_and_removes() {
432 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
433 m.toggle(0, 0);
434 m.toggle(1, 1);
435 assert_eq!(m.count(), 2);
436 m.toggle(0, 0);
437 assert_eq!(m.count(), 1);
438 }
439
440 #[test]
441 fn extend_in_multi_cell_fills_rectangle() {
442 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
443 m.select(2, 1);
444 m.extend_to(4, 3);
445 assert_eq!(m.count(), 9);
447 assert!(m.is_selected(3, 2));
448 }
449
450 #[test]
451 fn extend_shrinks_when_target_moves_inward() {
452 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
455 m.select(0, 0);
456 m.extend_to(2, 2); assert_eq!(m.count(), 9);
458 m.extend_to(1, 1); assert_eq!(m.count(), 4, "rectangle must shrink, not accrete");
460 assert!(
461 !m.is_selected(2, 2),
462 "the dropped corner must be deselected"
463 );
464 }
465
466 #[test]
467 fn ctrl_committed_cells_survive_a_later_shift_extend() {
468 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
471 m.select(0, 0); m.toggle(5, 5); m.extend_to(6, 6); assert!(m.is_selected(0, 0), "Ctrl-committed cell must survive");
475 assert!(m.is_selected(6, 6));
476 m.extend_to(5, 5); assert!(m.is_selected(0, 0), "committed cell still there");
478 assert!(!m.is_selected(6, 6), "shrunk-away cell gone");
479 assert_eq!(m.count(), 2); }
481
482 #[test]
483 fn select_all_in_multi_cell() {
484 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
485 m.select_all(3, 4);
486 assert_eq!(m.count(), 12);
487 }
488
489 #[test]
490 fn single_cell_mode_keeps_one_selection() {
491 let m = CellSelectionModel::new(TableSelectionMode::SingleCell);
492 m.select(1, 1);
493 m.toggle(2, 2);
494 assert_eq!(m.count(), 1);
495 assert!(m.is_selected(2, 2));
496 }
497
498 #[test]
499 fn adjust_for_row_insert_shifts_higher_rows() {
500 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
501 m.select(2, 0);
502 m.toggle(4, 0);
503 m.adjust_for_row_insert(3, 2);
504 assert!(m.is_selected(2, 0));
505 assert!(m.is_selected(6, 0));
506 }
507
508 #[test]
509 fn adjust_for_row_remove_drops_in_range() {
510 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
511 m.select(1, 0);
512 m.toggle(3, 0);
513 m.toggle(5, 0);
514 m.adjust_for_row_remove(2, 2);
515 assert!(m.is_selected(1, 0));
518 assert!(m.is_selected(3, 0));
520 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
521 assert_eq!(v, vec![(1, 0), (3, 0)]);
522 }
523
524 #[test]
525 fn adjust_for_row_move_follows_cells() {
526 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
527 m.select(0, 1); m.toggle(1, 2); m.adjust_for_row_move(0, 2, 1);
532 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
533 assert_eq!(v, vec![(0, 2), (2, 1)]);
534 }
535
536 #[test]
537 fn adjust_for_column_insert_and_remove() {
538 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
539 m.select(0, 1);
540 m.toggle(0, 4);
541 m.adjust_for_column_insert(2, 2);
542 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
544 assert_eq!(v, vec![(0, 1), (0, 6)]);
545
546 m.adjust_for_column_remove(0, 2);
547 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
548 assert_eq!(v, vec![(0, 4)]);
550 }
551
552 #[test]
553 fn remap_columns_follows_reorder_and_drops_removed_columns() {
554 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
555 m.select(0, 0); m.toggle(1, 2); let old_to_new = vec![Some(2), None, Some(0)];
560 m.remap_columns(&old_to_new);
561 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
562 assert_eq!(v, vec![(0, 2), (1, 0)], "columns follow their new position");
563 m.extend_to(1, 1);
566 assert!(
567 m.is_selected(1, 0),
568 "remapped anchor (1,0) must survive into the next extend"
569 );
570 }
571
572 #[test]
573 fn remap_columns_drops_selection_in_a_removed_column() {
574 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
575 m.select(3, 1);
576 m.remap_columns(&[Some(0), None]);
578 assert_eq!(m.count(), 0);
579 }
580
581 #[test]
582 #[should_panic]
583 fn cell_model_rejects_row_mode() {
584 let _ = CellSelectionModel::new(TableSelectionMode::MultiRow);
585 }
586
587 #[test]
588 fn mode_is_cell_mode() {
589 assert!(TableSelectionMode::SingleCell.is_cell_mode());
590 assert!(TableSelectionMode::MultiCell.is_cell_mode());
591 assert!(!TableSelectionMode::MultiRow.is_cell_mode());
592 }
593}