use std::cell::RefCell;
use std::collections::{BTreeSet, HashMap};
use std::rc::Rc;
use teksilo_core::signal::{ObserverHandle, Signal};
struct Inner {
per_index: HashMap<usize, Signal<bool>>,
observers: HashMap<usize, ObserverHandle>,
}
pub struct CheckedModel {
checked: Signal<BTreeSet<usize>>,
inner: Rc<RefCell<Inner>>,
}
impl CheckedModel {
pub fn new() -> Self {
Self {
checked: Signal::new(BTreeSet::new()),
inner: Rc::new(RefCell::new(Inner {
per_index: HashMap::new(),
observers: HashMap::new(),
})),
}
}
pub fn checked_signal(&self) -> Signal<BTreeSet<usize>> {
self.checked.clone()
}
pub fn signal_for(&self, index: usize) -> Signal<bool> {
if let Some(sig) = self.inner.borrow().per_index.get(&index) {
return sig.clone();
}
let sig = Signal::new(false);
let mut inner = self.inner.borrow_mut();
Self::install_signal(&mut inner, &self.checked, index, sig.clone());
sig
}
fn install_signal(
inner: &mut Inner,
central: &Signal<BTreeSet<usize>>,
index: usize,
sig: Signal<bool>,
) {
let central = central.clone();
let handle = sig.observe(move |checked| {
let mut set = central.get();
let changed = if *checked {
set.insert(index)
} else {
set.remove(&index)
};
if changed {
central.set(set);
}
});
inner.per_index.insert(index, sig);
inner.observers.insert(index, handle);
}
fn rekey(&self, map: impl Fn(usize) -> Option<usize>) {
let entries: Vec<(usize, Signal<bool>)> = {
let inner = self.inner.borrow();
inner
.per_index
.iter()
.map(|(&i, s)| (i, s.clone()))
.collect()
};
{
let mut inner = self.inner.borrow_mut();
inner.per_index.clear();
inner.observers.clear();
for (idx, sig) in entries {
if let Some(new_idx) = map(idx) {
Self::install_signal(&mut inner, &self.checked, new_idx, sig);
}
}
}
let old = self.checked.get();
let new: BTreeSet<usize> = old.iter().filter_map(|&i| map(i)).collect();
if new != old {
self.checked.set(new);
}
}
pub fn adjust_for_insert(&self, start: usize, count: usize) {
if count == 0 {
return;
}
self.rekey(|i| Some(if i >= start { i + count } else { i }));
}
pub fn adjust_for_remove(&self, start: usize, count: usize) {
if count == 0 {
return;
}
let end = start + count;
self.rekey(|i| {
if i < start {
Some(i)
} else if i >= end {
Some(i - count)
} else {
None
}
});
}
pub fn adjust_for_move(&self, from: usize, to: usize, count: usize) {
if from == to || count == 0 {
return;
}
self.rekey(|i| Some(crate::map_index_after_move(i, from, to, count)));
}
pub fn is_checked(&self, index: usize) -> bool {
self.inner
.borrow()
.per_index
.get(&index)
.map(|s| s.get())
.unwrap_or(false)
}
pub fn checked_indices(&self) -> Vec<usize> {
self.checked.get().into_iter().collect()
}
pub fn checked_count(&self) -> usize {
self.checked.get().len()
}
pub fn check(&self, index: usize) {
let sig = self.signal_for(index);
if !sig.get() {
sig.set(true);
}
}
pub fn uncheck(&self, index: usize) {
let sig = self.signal_for(index);
if sig.get() {
sig.set(false);
}
}
pub fn toggle(&self, index: usize) {
let sig = self.signal_for(index);
sig.set(!sig.get());
}
pub fn check_all(&self, count: usize) {
for i in 0..count {
self.check(i);
}
}
pub fn clear(&self) {
let keys: Vec<usize> = self.inner.borrow().per_index.keys().copied().collect();
for i in keys {
self.uncheck(i);
}
}
}
impl Default for CheckedModel {
fn default() -> Self {
Self::new()
}
}
impl Clone for CheckedModel {
fn clone(&self) -> Self {
Self {
checked: self.checked.clone(),
inner: self.inner.clone(),
}
}
}
impl std::fmt::Debug for CheckedModel {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("CheckedModel")
.field("checked_count", &self.checked.get().len())
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn signal_for_returns_same_root_per_index() {
let m = CheckedModel::new();
let a = m.signal_for(2);
let b = m.signal_for(2);
assert_eq!(a.get(), b.get());
a.set(true);
assert!(b.get(), "cached signal handles share the same root");
}
#[test]
fn writing_per_index_signal_updates_central() {
let m = CheckedModel::new();
let s = m.signal_for(7);
s.set(true);
assert_eq!(m.checked_indices(), vec![7]);
s.set(false);
assert_eq!(m.checked_indices(), Vec::<usize>::new());
}
#[test]
fn check_uncheck_toggle_round_trip() {
let m = CheckedModel::new();
assert!(!m.is_checked(3));
m.check(3);
assert!(m.is_checked(3));
m.toggle(3);
assert!(!m.is_checked(3));
m.toggle(3);
assert!(m.is_checked(3));
m.uncheck(3);
assert!(!m.is_checked(3));
}
#[test]
fn check_all_then_clear() {
let m = CheckedModel::new();
m.check_all(5);
assert_eq!(m.checked_indices(), vec![0, 1, 2, 3, 4]);
m.clear();
assert_eq!(m.checked_count(), 0);
}
#[test]
fn signal_updates_propagate() {
let m = CheckedModel::new();
let s = m.signal_for(7);
assert!(!s.get());
m.check(7);
assert!(s.get());
m.uncheck(7);
assert!(!s.get());
}
#[test]
fn unrelated_index_does_not_flip_signal() {
let m = CheckedModel::new();
let s = m.signal_for(1);
m.check(2);
assert!(!s.get());
}
#[test]
fn adjust_for_insert_shifts_checked_rows() {
let m = CheckedModel::new();
m.check(2);
m.check(4);
m.adjust_for_insert(3, 2);
assert_eq!(m.checked_indices(), vec![2, 6]);
assert!(m.is_checked(2));
assert!(m.is_checked(6));
assert!(!m.is_checked(4));
}
#[test]
fn adjust_for_remove_drops_in_range_and_shifts() {
let m = CheckedModel::new();
m.check(1);
m.check(3);
m.check(5);
m.adjust_for_remove(2, 2); assert_eq!(m.checked_indices(), vec![1, 3]);
assert!(m.is_checked(3), "row that shifted in is checked");
}
#[test]
fn adjust_for_move_follows_checked_row() {
let m = CheckedModel::new();
m.check(0); m.adjust_for_move(0, 2, 1); assert_eq!(m.checked_indices(), vec![2]);
assert!(m.is_checked(2));
}
#[test]
fn rekey_rewires_observer_so_later_clicks_target_the_new_index() {
let m = CheckedModel::new();
let s = m.signal_for(2);
s.set(true);
assert_eq!(m.checked_indices(), vec![2]);
m.adjust_for_insert(0, 1); assert_eq!(m.checked_indices(), vec![3]);
s.set(false);
assert_eq!(m.checked_indices(), Vec::<usize>::new());
s.set(true);
assert_eq!(m.checked_indices(), vec![3], "observer re-keyed to 3");
}
#[test]
fn adjust_is_noop_on_empty_and_zero_count() {
let m = CheckedModel::new();
m.check(1);
m.adjust_for_insert(0, 0);
m.adjust_for_remove(5, 0);
m.adjust_for_move(2, 2, 1);
assert_eq!(m.checked_indices(), vec![1]);
}
}