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_all(&self, row_count: usize, col_count: usize) {
169 if self.mode == TableSelectionMode::None {
170 return;
171 }
172 let mut s = BTreeSet::new();
173 for r in 0..row_count {
174 for c in 0..col_count {
175 s.insert((r, c));
176 }
177 }
178 self.selection.set(s.clone());
179 *self.base.borrow_mut() = s;
182 }
183
184 pub fn clear(&self) {
185 self.selection.set(BTreeSet::new());
186 self.anchor.set(None);
187 self.base.borrow_mut().clear();
188 }
189
190 fn remap_base(&self, f: impl Fn((usize, usize)) -> Option<(usize, usize)>) {
194 let mut b = self.base.borrow_mut();
195 if b.is_empty() {
196 return;
197 }
198 *b = b.iter().filter_map(|&cell| f(cell)).collect();
199 }
200
201 pub fn adjust_for_row_insert(&self, at_row: usize, count: usize) {
204 let old = self.selection.get();
205 let mut new = BTreeSet::new();
206 for &(r, c) in &old {
207 if r >= at_row {
208 new.insert((r + count, c));
209 } else {
210 new.insert((r, c));
211 }
212 }
213 if new != old {
214 self.selection.set(new);
215 }
216 self.remap_base(|(r, c)| Some(if r >= at_row { (r + count, c) } else { (r, c) }));
217 if let Some((r, c)) = self.anchor.get()
218 && r >= at_row
219 {
220 self.anchor.set(Some((r + count, c)));
221 }
222 }
223
224 pub fn adjust_for_row_remove(&self, at_row: usize, count: usize) {
228 let old = self.selection.get();
229 let end = at_row + count;
230 let mut new = BTreeSet::new();
231 for &(r, c) in &old {
232 if r < at_row {
233 new.insert((r, c));
234 } else if r >= end {
235 new.insert((r - count, c));
236 }
237 }
239 if new != old {
240 self.selection.set(new);
241 }
242 self.remap_base(|(r, c)| {
243 if r < at_row {
244 Some((r, c))
245 } else if r >= end {
246 Some((r - count, c))
247 } else {
248 None
249 }
250 });
251 if let Some((r, c)) = self.anchor.get() {
252 if r >= end {
253 self.anchor.set(Some((r - count, c)));
254 } else if r >= at_row {
255 self.anchor.set(None);
256 }
257 }
258 }
259
260 pub fn adjust_for_row_move(&self, from: usize, to: usize, count: usize) {
264 if from == to || count == 0 {
265 return;
266 }
267 let map = |r: usize| teksilo_data::map_index_after_move(r, from, to, count);
268 let old = self.selection.get();
269 let new: BTreeSet<(usize, usize)> = old.iter().map(|&(r, c)| (map(r), c)).collect();
270 if new != old {
271 self.selection.set(new);
272 }
273 self.remap_base(|(r, c)| Some((map(r), c)));
274 if let Some((r, c)) = self.anchor.get() {
275 self.anchor.set(Some((map(r), c)));
276 }
277 }
278
279 pub fn adjust_for_column_insert(&self, at_col: usize, count: usize) {
291 let old = self.selection.get();
292 let mut new = BTreeSet::new();
293 for &(r, c) in &old {
294 if c >= at_col {
295 new.insert((r, c + count));
296 } else {
297 new.insert((r, c));
298 }
299 }
300 if new != old {
301 self.selection.set(new);
302 }
303 self.remap_base(|(r, c)| Some(if c >= at_col { (r, c + count) } else { (r, c) }));
304 if let Some((r, c)) = self.anchor.get()
305 && c >= at_col
306 {
307 self.anchor.set(Some((r, c + count)));
308 }
309 }
310
311 pub fn adjust_for_column_remove(&self, at_col: usize, count: usize) {
318 let old = self.selection.get();
319 let end = at_col + count;
320 let mut new = BTreeSet::new();
321 for &(r, c) in &old {
322 if c < at_col {
323 new.insert((r, c));
324 } else if c >= end {
325 new.insert((r, c - count));
326 }
327 }
328 if new != old {
329 self.selection.set(new);
330 }
331 self.remap_base(|(r, c)| {
332 if c < at_col {
333 Some((r, c))
334 } else if c >= end {
335 Some((r, c - count))
336 } else {
337 None
338 }
339 });
340 if let Some((r, c)) = self.anchor.get() {
341 if c >= end {
342 self.anchor.set(Some((r, c - count)));
343 } else if c >= at_col {
344 self.anchor.set(None);
345 }
346 }
347 }
348
349 pub(crate) fn remap_columns(&self, old_to_new: &[Option<usize>]) {
360 let map = |c: usize| old_to_new.get(c).copied().flatten();
361 let old = self.selection.get();
362 let new: BTreeSet<(usize, usize)> = old
363 .iter()
364 .filter_map(|&(r, c)| map(c).map(|nc| (r, nc)))
365 .collect();
366 if new != old {
367 self.selection.set(new);
368 }
369 self.remap_base(|(r, c)| map(c).map(|nc| (r, nc)));
370 if let Some((r, c)) = self.anchor.get() {
371 self.anchor.set(map(c).map(|nc| (r, nc)));
372 }
373 }
374}
375
376impl Clone for CellSelectionModel {
377 fn clone(&self) -> Self {
378 Self {
379 mode: self.mode,
380 selection: self.selection.clone(),
381 anchor: self.anchor.clone(),
382 base: self.base.clone(),
383 }
384 }
385}
386
387impl std::fmt::Debug for CellSelectionModel {
388 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
389 f.debug_struct("CellSelectionModel")
390 .field("mode", &self.mode)
391 .field("selected_count", &self.selection.get().len())
392 .finish()
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn select_replaces_and_sets_anchor() {
402 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
403 m.select(2, 3);
404 assert!(m.is_selected(2, 3));
405 assert_eq!(m.count(), 1);
406 m.select(5, 5);
407 assert!(m.is_selected(5, 5));
408 assert!(!m.is_selected(2, 3));
409 }
410
411 #[test]
412 fn toggle_in_multi_cell_adds_and_removes() {
413 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
414 m.toggle(0, 0);
415 m.toggle(1, 1);
416 assert_eq!(m.count(), 2);
417 m.toggle(0, 0);
418 assert_eq!(m.count(), 1);
419 }
420
421 #[test]
422 fn extend_in_multi_cell_fills_rectangle() {
423 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
424 m.select(2, 1);
425 m.extend_to(4, 3);
426 assert_eq!(m.count(), 9);
428 assert!(m.is_selected(3, 2));
429 }
430
431 #[test]
432 fn extend_shrinks_when_target_moves_inward() {
433 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
436 m.select(0, 0);
437 m.extend_to(2, 2); assert_eq!(m.count(), 9);
439 m.extend_to(1, 1); assert_eq!(m.count(), 4, "rectangle must shrink, not accrete");
441 assert!(
442 !m.is_selected(2, 2),
443 "the dropped corner must be deselected"
444 );
445 }
446
447 #[test]
448 fn ctrl_committed_cells_survive_a_later_shift_extend() {
449 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
452 m.select(0, 0); m.toggle(5, 5); m.extend_to(6, 6); assert!(m.is_selected(0, 0), "Ctrl-committed cell must survive");
456 assert!(m.is_selected(6, 6));
457 m.extend_to(5, 5); assert!(m.is_selected(0, 0), "committed cell still there");
459 assert!(!m.is_selected(6, 6), "shrunk-away cell gone");
460 assert_eq!(m.count(), 2); }
462
463 #[test]
464 fn select_all_in_multi_cell() {
465 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
466 m.select_all(3, 4);
467 assert_eq!(m.count(), 12);
468 }
469
470 #[test]
471 fn single_cell_mode_keeps_one_selection() {
472 let m = CellSelectionModel::new(TableSelectionMode::SingleCell);
473 m.select(1, 1);
474 m.toggle(2, 2);
475 assert_eq!(m.count(), 1);
476 assert!(m.is_selected(2, 2));
477 }
478
479 #[test]
480 fn adjust_for_row_insert_shifts_higher_rows() {
481 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
482 m.select(2, 0);
483 m.toggle(4, 0);
484 m.adjust_for_row_insert(3, 2);
485 assert!(m.is_selected(2, 0));
486 assert!(m.is_selected(6, 0));
487 }
488
489 #[test]
490 fn adjust_for_row_remove_drops_in_range() {
491 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
492 m.select(1, 0);
493 m.toggle(3, 0);
494 m.toggle(5, 0);
495 m.adjust_for_row_remove(2, 2);
496 assert!(m.is_selected(1, 0));
499 assert!(m.is_selected(3, 0));
501 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
502 assert_eq!(v, vec![(1, 0), (3, 0)]);
503 }
504
505 #[test]
506 fn adjust_for_row_move_follows_cells() {
507 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
508 m.select(0, 1); m.toggle(1, 2); m.adjust_for_row_move(0, 2, 1);
513 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
514 assert_eq!(v, vec![(0, 2), (2, 1)]);
515 }
516
517 #[test]
518 fn adjust_for_column_insert_and_remove() {
519 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
520 m.select(0, 1);
521 m.toggle(0, 4);
522 m.adjust_for_column_insert(2, 2);
523 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
525 assert_eq!(v, vec![(0, 1), (0, 6)]);
526
527 m.adjust_for_column_remove(0, 2);
528 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
529 assert_eq!(v, vec![(0, 4)]);
531 }
532
533 #[test]
534 fn remap_columns_follows_reorder_and_drops_removed_columns() {
535 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
536 m.select(0, 0); m.toggle(1, 2); let old_to_new = vec![Some(2), None, Some(0)];
541 m.remap_columns(&old_to_new);
542 let v: Vec<_> = m.selection_signal().get().into_iter().collect();
543 assert_eq!(v, vec![(0, 2), (1, 0)], "columns follow their new position");
544 m.extend_to(1, 1);
547 assert!(
548 m.is_selected(1, 0),
549 "remapped anchor (1,0) must survive into the next extend"
550 );
551 }
552
553 #[test]
554 fn remap_columns_drops_selection_in_a_removed_column() {
555 let m = CellSelectionModel::new(TableSelectionMode::MultiCell);
556 m.select(3, 1);
557 m.remap_columns(&[Some(0), None]);
559 assert_eq!(m.count(), 0);
560 }
561
562 #[test]
563 #[should_panic]
564 fn cell_model_rejects_row_mode() {
565 let _ = CellSelectionModel::new(TableSelectionMode::MultiRow);
566 }
567
568 #[test]
569 fn mode_is_cell_mode() {
570 assert!(TableSelectionMode::SingleCell.is_cell_mode());
571 assert!(TableSelectionMode::MultiCell.is_cell_mode());
572 assert!(!TableSelectionMode::MultiRow.is_cell_mode());
573 }
574}