1use std::cell::RefCell;
57use std::collections::HashSet;
58use std::rc::Rc;
59
60use teksilo_core::ObserverHandle;
61use teksilo_core::signal::Signal;
62
63use crate::chart_change::{ChartChange, SeriesId};
64use crate::chart_model::ChartModel;
65use crate::selection_model::SelectionMode;
66
67pub struct ChartSelection {
70 mode: SelectionMode,
71 selection: Signal<HashSet<(SeriesId, usize)>>,
72 anchor: Rc<RefCell<Option<(SeriesId, usize)>>>,
75 attach_handle: Rc<RefCell<Option<ObserverHandle>>>,
80 #[cfg(debug_assertions)]
85 debug_adapter_holder: Rc<RefCell<Option<Rc<dyn crate::debug_registry::ModelDebug>>>>,
86}
87
88impl ChartSelection {
89 pub fn new(mode: SelectionMode) -> Self {
91 Self {
92 mode,
93 selection: Signal::new(HashSet::new()),
94 anchor: Rc::new(RefCell::new(None)),
95 attach_handle: Rc::new(RefCell::new(None)),
96 #[cfg(debug_assertions)]
97 debug_adapter_holder: Rc::new(RefCell::new(None)),
98 }
99 }
100
101 pub fn attached<T: 'static>(mode: SelectionMode, model: &ChartModel<T>) -> Self {
111 let sel = Self::new(mode);
112 sel.attach(model);
113 sel
114 }
115
116 pub fn attach<T: 'static>(&self, model: &ChartModel<T>) {
129 let selection = self.selection.clone();
130 let anchor = self.anchor.clone();
131 let handle = model.observe_changes(move |change| {
132 Self::adjust_state(&selection, &anchor, change);
133 });
134 *self.attach_handle.borrow_mut() = Some(handle);
135 }
136
137 pub fn mode(&self) -> SelectionMode {
139 self.mode
140 }
141
142 pub fn selection_signal(&self) -> Signal<HashSet<(SeriesId, usize)>> {
144 self.selection.clone()
145 }
146
147 pub fn is_selected(&self, series: SeriesId, index: usize) -> bool {
149 self.selection.get().contains(&(series, index))
150 }
151
152 pub fn selected_points(&self) -> Vec<(SeriesId, usize)> {
154 self.selection.get().into_iter().collect()
155 }
156
157 pub fn count(&self) -> usize {
159 self.selection.get().len()
160 }
161
162 pub fn select_point(&self, series: SeriesId, index: usize) {
165 if self.mode == SelectionMode::None {
166 return;
167 }
168 let mut set = HashSet::new();
169 set.insert((series, index));
170 self.selection.set(set);
171 *self.anchor.borrow_mut() = Some((series, index));
172 }
173
174 pub fn toggle_point(&self, series: SeriesId, index: usize) {
177 match self.mode {
178 SelectionMode::None => {}
179 SelectionMode::Single => self.select_point(series, index),
180 SelectionMode::Multi => {
181 let key = (series, index);
182 let mut set = self.selection.get();
183 if set.contains(&key) {
184 set.remove(&key);
185 } else {
186 set.insert(key);
187 }
188 self.selection.set(set);
189 *self.anchor.borrow_mut() = Some(key);
190 }
191 }
192 }
193
194 pub fn extend_to(&self, series: SeriesId, target: usize) {
199 match self.mode {
200 SelectionMode::None => {}
201 SelectionMode::Single => self.select_point(series, target),
202 SelectionMode::Multi => {
203 let anchor = *self.anchor.borrow();
204 let Some((a_series, a_index)) = anchor else {
205 self.select_point(series, target);
206 return;
207 };
208 if a_series != series {
209 self.select_point(series, target);
210 return;
211 }
212 let start = a_index.min(target);
213 let end = a_index.max(target);
214 let mut set = self.selection.get();
215 for i in start..=end {
216 set.insert((series, i));
217 }
218 self.selection.set(set);
219 }
221 }
222 }
223
224 pub fn select_points(
229 &self,
230 points: impl IntoIterator<Item = (SeriesId, usize)>,
231 additive: bool,
232 ) {
233 if self.mode == SelectionMode::None {
234 return;
235 }
236 let mut set = if additive {
237 self.selection.get()
238 } else {
239 HashSet::new()
240 };
241 set.extend(points);
242 if self.mode == SelectionMode::Single && set.len() > 1 {
243 let keep = set.iter().next().copied();
244 set = keep.into_iter().collect();
245 }
246 self.selection.set(set);
247 }
248
249 pub fn clear(&self) {
251 self.selection.set(HashSet::new());
252 *self.anchor.borrow_mut() = None;
253 }
254
255 pub fn adjust(&self, change: &ChartChange) {
262 Self::adjust_state(&self.selection, &self.anchor, change);
263 }
264
265 fn adjust_state(
273 selection: &Signal<HashSet<(SeriesId, usize)>>,
274 anchor: &Rc<RefCell<Option<(SeriesId, usize)>>>,
275 change: &ChartChange,
276 ) {
277 match change {
278 ChartChange::SeriesRemoved { series } | ChartChange::SeriesDataReplaced { series } => {
279 let series = *series;
280 let old = selection.get();
281 let new: HashSet<(SeriesId, usize)> =
282 old.iter().filter(|(s, _)| *s != series).copied().collect();
283 if new.len() != old.len() {
284 selection.set(new);
285 }
286 let drop_anchor = anchor.borrow().as_ref().is_some_and(|(s, _)| *s == series);
287 if drop_anchor {
288 *anchor.borrow_mut() = None;
289 }
290 }
291 ChartChange::PointsInserted { series, range } => {
292 let series = *series;
293 let start = range.start;
294 let count = range.end - range.start;
295 let old = selection.get();
296 let new: HashSet<(SeriesId, usize)> = old
297 .iter()
298 .map(|&(s, i)| {
299 if s == series && i >= start {
300 (s, i + count)
301 } else {
302 (s, i)
303 }
304 })
305 .collect();
306 if new != old {
307 selection.set(new);
308 }
309 let mut anchor = anchor.borrow_mut();
310 if let Some((s, i)) = *anchor
311 && s == series
312 && i >= start
313 {
314 *anchor = Some((s, i + count));
315 }
316 }
317 ChartChange::PointsRemoved { series, range } => {
318 let series = *series;
319 let start = range.start;
320 let end = range.end;
321 let count = range.end - range.start;
322 let old = selection.get();
323 let new: HashSet<(SeriesId, usize)> = old
324 .iter()
325 .filter_map(|&(s, i)| {
326 if s != series {
327 return Some((s, i));
328 }
329 if i < start {
330 Some((s, i))
331 } else if i >= end {
332 Some((s, i - count))
333 } else {
334 None
335 }
336 })
337 .collect();
338 if new != old {
339 selection.set(new);
340 }
341 let mut anchor = anchor.borrow_mut();
342 if let Some((s, i)) = *anchor
343 && s == series
344 {
345 if i >= end {
346 *anchor = Some((s, i - count));
347 } else if i >= start {
348 *anchor = None;
349 }
350 }
351 }
352 ChartChange::Reset => {
353 selection.set(HashSet::new());
354 *anchor.borrow_mut() = None;
355 }
356 ChartChange::SeriesInserted { .. }
359 | ChartChange::SeriesMoved { .. }
360 | ChartChange::SeriesRenamed { .. }
361 | ChartChange::SeriesColorChanged { .. }
362 | ChartChange::SeriesPatternChanged { .. }
363 | ChartChange::SeriesVisibilityChanged { .. }
364 | ChartChange::PointUpdated { .. } => {}
365 }
366 }
367
368 pub fn prune(&self, exists: impl Fn(SeriesId, usize) -> bool) {
370 let old = self.selection.get();
371 let new: HashSet<(SeriesId, usize)> = old
372 .iter()
373 .filter(|(s, i)| exists(*s, *i))
374 .copied()
375 .collect();
376 if new.len() != old.len() {
377 self.selection.set(new);
378 }
379 let drop_anchor = self
380 .anchor
381 .borrow()
382 .as_ref()
383 .is_some_and(|(s, i)| !exists(*s, *i));
384 if drop_anchor {
385 *self.anchor.borrow_mut() = None;
386 }
387 }
388}
389
390impl Clone for ChartSelection {
391 fn clone(&self) -> Self {
392 Self {
393 mode: self.mode,
394 selection: self.selection.clone(),
395 anchor: self.anchor.clone(),
396 attach_handle: self.attach_handle.clone(),
397 #[cfg(debug_assertions)]
398 debug_adapter_holder: self.debug_adapter_holder.clone(),
399 }
400 }
401}
402
403impl ChartSelection {
404 pub fn debug_named(self, _name: impl Into<String>) -> Self {
413 #[cfg(debug_assertions)]
414 {
415 let adapter: Rc<dyn crate::debug_registry::ModelDebug> = Rc::new(ChartSelectionDebug {
416 selection: self.selection.clone(),
417 mode: self.mode,
418 });
419 crate::debug_registry::register(_name.into(), Rc::downgrade(&adapter));
420 *self.debug_adapter_holder.borrow_mut() = Some(adapter);
421 }
422 self
423 }
424}
425
426#[cfg(debug_assertions)]
427struct ChartSelectionDebug {
428 selection: Signal<HashSet<(SeriesId, usize)>>,
429 mode: SelectionMode,
430}
431
432#[cfg(debug_assertions)]
433impl crate::debug_registry::ModelDebug for ChartSelectionDebug {
434 fn kind(&self) -> &'static str {
435 "ChartSelection"
436 }
437 fn len(&self) -> usize {
438 self.selection.get().len()
439 }
440 fn debug_dump(&self, out: &mut dyn std::fmt::Write) {
441 let _ = writeln!(out, "mode = {:?}", self.mode);
442 let sel = self.selection.get();
443 if sel.is_empty() {
444 let _ = writeln!(out, "(empty)");
445 return;
446 }
447 for (s, i) in sel.iter() {
448 let _ = writeln!(out, "{:?}[{}]", s, i);
449 }
450 }
451}
452
453impl std::fmt::Debug for ChartSelection {
454 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
455 f.debug_struct("ChartSelection")
456 .field("mode", &self.mode)
457 .field("selected_count", &self.selection.get().len())
458 .finish()
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use super::*;
465
466 fn two_series() -> (SeriesId, SeriesId) {
467 let model: ChartModel<i32> = ChartModel::new();
468 let a = model.add_series("a");
469 let b = model.add_series("b");
470 (a, b)
471 }
472
473 fn set(points: impl IntoIterator<Item = (SeriesId, usize)>) -> HashSet<(SeriesId, usize)> {
474 points.into_iter().collect()
475 }
476
477 #[test]
478 fn select_toggle_extend_within_series() {
479 let (a, _b) = two_series();
480 let sel = ChartSelection::new(SelectionMode::Multi);
481 sel.select_point(a, 2);
482 sel.extend_to(a, 5);
483 assert_eq!(
484 sel.selection_signal().get(),
485 set([(a, 2), (a, 3), (a, 4), (a, 5)])
486 );
487 }
488
489 #[test]
490 fn toggle_deselects() {
491 let (a, _b) = two_series();
492 let sel = ChartSelection::new(SelectionMode::Multi);
493 sel.toggle_point(a, 1);
494 assert!(sel.is_selected(a, 1));
495 sel.toggle_point(a, 1);
496 assert!(!sel.is_selected(a, 1));
497 }
498
499 #[test]
500 fn extend_backwards_within_series() {
501 let (a, _b) = two_series();
502 let sel = ChartSelection::new(SelectionMode::Multi);
503 sel.select_point(a, 5);
504 sel.extend_to(a, 2);
505 assert_eq!(
506 sel.selection_signal().get(),
507 set([(a, 2), (a, 3), (a, 4), (a, 5)])
508 );
509 }
510
511 #[test]
512 fn cross_series_extend_falls_back_to_single_select() {
513 let (a, b) = two_series();
514 let sel = ChartSelection::new(SelectionMode::Multi);
515 sel.select_point(a, 2); sel.extend_to(b, 3);
517 assert_eq!(sel.selection_signal().get(), set([(b, 3)]));
518 }
519
520 #[test]
521 fn select_points_additive_and_replace() {
522 let (a, b) = two_series();
523 let sel = ChartSelection::new(SelectionMode::Multi);
524 sel.select_points([(a, 0), (a, 1)], false);
525 assert_eq!(sel.count(), 2);
526 sel.select_points([(b, 0)], true);
527 assert_eq!(sel.selection_signal().get(), set([(a, 0), (a, 1), (b, 0)]));
528 sel.select_points([(b, 1)], false);
529 assert_eq!(sel.selection_signal().get(), set([(b, 1)]));
530 }
531
532 #[test]
533 fn none_mode_ignores_all() {
534 let (a, _b) = two_series();
535 let sel = ChartSelection::new(SelectionMode::None);
536 sel.select_point(a, 0);
537 sel.toggle_point(a, 1);
538 sel.select_points([(a, 2)], false);
539 assert_eq!(sel.count(), 0);
540 }
541
542 #[test]
543 fn single_mode_extend_acts_as_select() {
544 let (a, b) = two_series();
545 let sel = ChartSelection::new(SelectionMode::Single);
546 sel.select_point(a, 1);
547 sel.extend_to(b, 5);
548 assert_eq!(sel.selection_signal().get(), set([(b, 5)]));
549 }
550
551 #[test]
552 fn adjust_drops_on_series_removed() {
553 let (a, b) = two_series();
554 let sel = ChartSelection::new(SelectionMode::Multi);
555 sel.select_points([(a, 0), (a, 1), (b, 0)], false);
556 sel.adjust(&ChartChange::SeriesRemoved { series: a });
557 assert_eq!(sel.selection_signal().get(), set([(b, 0)]));
558 }
559
560 #[test]
561 fn adjust_drops_anchor_on_series_removed() {
562 let (a, b) = two_series();
563 let sel = ChartSelection::new(SelectionMode::Multi);
564 sel.select_point(a, 0); sel.adjust(&ChartChange::SeriesRemoved { series: a });
566 sel.extend_to(b, 3);
568 assert_eq!(sel.selection_signal().get(), set([(b, 3)]));
569 }
570
571 #[test]
572 fn adjust_drops_on_series_data_replaced() {
573 let (a, b) = two_series();
574 let sel = ChartSelection::new(SelectionMode::Multi);
575 sel.select_points([(a, 0), (b, 0)], false);
576 sel.adjust(&ChartChange::SeriesDataReplaced { series: a });
577 assert_eq!(sel.selection_signal().get(), set([(b, 0)]));
578 }
579
580 #[test]
581 fn adjust_clears_on_reset() {
582 let (a, _b) = two_series();
583 let sel = ChartSelection::new(SelectionMode::Multi);
584 sel.select_point(a, 0);
585 sel.adjust(&ChartChange::Reset);
586 assert_eq!(sel.count(), 0);
587 }
588
589 #[test]
590 fn adjust_shifts_on_points_inserted() {
591 let (a, b) = two_series();
592 let sel = ChartSelection::new(SelectionMode::Multi);
593 sel.select_points([(a, 1), (a, 3), (b, 1)], false);
594 sel.adjust(&ChartChange::PointsInserted {
595 series: a,
596 range: 2..4,
597 });
598 assert_eq!(sel.selection_signal().get(), set([(a, 1), (a, 5), (b, 1)]));
599 }
600
601 #[test]
602 fn adjust_shifts_and_drops_on_points_removed() {
603 let (a, _b) = two_series();
604 let sel = ChartSelection::new(SelectionMode::Multi);
605 sel.select_points([(a, 1), (a, 3), (a, 5)], false);
606 sel.adjust(&ChartChange::PointsRemoved {
607 series: a,
608 range: 2..4,
609 });
610 assert_eq!(sel.selection_signal().get(), set([(a, 1), (a, 3)]));
612 }
613
614 #[test]
615 fn adjust_ignores_metadata_and_point_updated() {
616 let (a, _b) = two_series();
617 let sel = ChartSelection::new(SelectionMode::Multi);
618 sel.select_point(a, 2);
619 sel.adjust(&ChartChange::SeriesInserted {
620 index: 0,
621 series: a,
622 });
623 sel.adjust(&ChartChange::SeriesMoved {
624 series: a,
625 from: 0,
626 to: 1,
627 });
628 sel.adjust(&ChartChange::SeriesRenamed { series: a });
629 sel.adjust(&ChartChange::SeriesColorChanged { series: a });
630 sel.adjust(&ChartChange::SeriesVisibilityChanged { series: a });
631 sel.adjust(&ChartChange::PointUpdated {
632 series: a,
633 index: 2,
634 });
635 assert_eq!(sel.selection_signal().get(), set([(a, 2)]));
636 }
637
638 #[test]
639 fn prune_drops_missing_points() {
640 let (a, b) = two_series();
641 let sel = ChartSelection::new(SelectionMode::Multi);
642 sel.select_points([(a, 0), (a, 1), (b, 0)], false);
643 sel.prune(|s, i| !(s == a && i == 1));
644 assert_eq!(sel.selection_signal().get(), set([(a, 0), (b, 0)]));
645 }
646
647 #[test]
648 fn prune_drops_anchor_when_missing() {
649 let (a, _b) = two_series();
650 let sel = ChartSelection::new(SelectionMode::Multi);
651 sel.select_point(a, 3); sel.prune(|_, _| false);
653 sel.extend_to(a, 9);
654 assert_eq!(sel.selection_signal().get(), set([(a, 9)]));
656 }
657
658 #[test]
659 fn signal_reactivity() {
660 use std::cell::Cell;
661 let (a, _b) = two_series();
662 let sel = ChartSelection::new(SelectionMode::Single);
663 let signal = sel.selection_signal();
664 let changed = Rc::new(Cell::new(false));
665 let c = changed.clone();
666 let _handle = signal.observe(move |_| c.set(true));
667 sel.select_point(a, 0);
668 assert!(changed.get());
669 }
670
671 #[test]
672 fn clone_shares_selection_and_anchor() {
673 let (a, b) = two_series();
674 let sel = ChartSelection::new(SelectionMode::Multi);
675 let clone = sel.clone();
676 sel.select_point(a, 1);
677 assert!(clone.is_selected(a, 1));
678 clone.extend_to(a, 3); assert_eq!(sel.selection_signal().get(), set([(a, 1), (a, 2), (a, 3)]));
680 let _ = b;
681 }
682
683 #[test]
686 fn attached_shifts_selection_on_point_removed_before_it() {
687 let model: ChartModel<i32> = ChartModel::new();
688 let s = model.add_series("s");
689 for i in 0..5 {
690 model.push_point(s, i, i as f32);
691 }
692 let sel = ChartSelection::attached(SelectionMode::Multi, &model);
693 sel.select_point(s, 3);
694 assert!(sel.is_selected(s, 3));
695
696 model.remove_point(s, 0);
699 model.remove_point(s, 0);
700
701 assert!(!sel.is_selected(s, 3));
702 assert!(sel.is_selected(s, 1));
703 }
704
705 #[test]
706 fn attach_on_a_manually_constructed_selection_wires_it_up() {
707 let model: ChartModel<i32> = ChartModel::new();
708 let s = model.add_series("s");
709 model.push_point(s, 0, 0.0);
710 model.push_point(s, 1, 1.0);
711
712 let sel = ChartSelection::new(SelectionMode::Single);
713 sel.select_point(s, 1);
714 sel.attach(&model); model.remove_point(s, 0);
717 assert!(sel.is_selected(s, 0), "index 1 shifted down to 0");
718 }
719
720 #[test]
721 fn manual_adjust_still_works_without_attach() {
722 let (a, _b) = two_series();
725 let sel = ChartSelection::new(SelectionMode::Multi);
726 sel.select_point(a, 3);
727 sel.adjust(&ChartChange::PointsRemoved {
728 series: a,
729 range: 0..2,
730 });
731 assert!(sel.is_selected(a, 1));
732 }
733
734 #[test]
735 fn attach_does_not_leak_the_subscription_via_a_reference_cycle() {
736 let model: ChartModel<i32> = ChartModel::new();
737 let sel = ChartSelection::attached(SelectionMode::Multi, &model);
738 let weak_anchor = Rc::downgrade(&sel.anchor);
739 let weak_attach_handle = Rc::downgrade(&sel.attach_handle);
740
741 drop(sel);
742
743 assert!(
744 weak_anchor.upgrade().is_none(),
745 "anchor must not be kept alive by the subscription closure"
746 );
747 assert!(
748 weak_attach_handle.upgrade().is_none(),
749 "attach_handle must not be kept alive by its own subscription"
750 );
751 }
752}