teksilo_data/
checked_model.rs1use std::cell::RefCell;
38use std::collections::{BTreeSet, HashMap};
39use std::rc::Rc;
40
41use teksilo_core::signal::{ObserverHandle, Signal};
42
43struct Inner {
44 per_index: HashMap<usize, Signal<bool>>,
45 observers: HashMap<usize, ObserverHandle>,
48}
49
50pub struct CheckedModel {
52 checked: Signal<BTreeSet<usize>>,
56 inner: Rc<RefCell<Inner>>,
57}
58
59impl CheckedModel {
60 pub fn new() -> Self {
62 Self {
63 checked: Signal::new(BTreeSet::new()),
64 inner: Rc::new(RefCell::new(Inner {
65 per_index: HashMap::new(),
66 observers: HashMap::new(),
67 })),
68 }
69 }
70
71 pub fn checked_signal(&self) -> Signal<BTreeSet<usize>> {
73 self.checked.clone()
74 }
75
76 pub fn signal_for(&self, index: usize) -> Signal<bool> {
81 if let Some(sig) = self.inner.borrow().per_index.get(&index) {
83 return sig.clone();
84 }
85 let sig = Signal::new(false);
87 let mut inner = self.inner.borrow_mut();
88 Self::install_signal(&mut inner, &self.checked, index, sig.clone());
89 sig
90 }
91
92 fn install_signal(
98 inner: &mut Inner,
99 central: &Signal<BTreeSet<usize>>,
100 index: usize,
101 sig: Signal<bool>,
102 ) {
103 let central = central.clone();
104 let handle = sig.observe(move |checked| {
105 let mut set = central.get();
106 let changed = if *checked {
107 set.insert(index)
108 } else {
109 set.remove(&index)
110 };
111 if changed {
112 central.set(set);
113 }
114 });
115 inner.per_index.insert(index, sig);
116 inner.observers.insert(index, handle);
117 }
118
119 fn rekey(&self, map: impl Fn(usize) -> Option<usize>) {
124 let entries: Vec<(usize, Signal<bool>)> = {
125 let inner = self.inner.borrow();
126 inner
127 .per_index
128 .iter()
129 .map(|(&i, s)| (i, s.clone()))
130 .collect()
131 };
132 {
133 let mut inner = self.inner.borrow_mut();
134 inner.per_index.clear();
137 inner.observers.clear();
138 for (idx, sig) in entries {
139 if let Some(new_idx) = map(idx) {
140 Self::install_signal(&mut inner, &self.checked, new_idx, sig);
141 }
142 }
143 }
144 let old = self.checked.get();
145 let new: BTreeSet<usize> = old.iter().filter_map(|&i| map(i)).collect();
146 if new != old {
147 self.checked.set(new);
148 }
149 }
150
151 pub fn adjust_for_insert(&self, start: usize, count: usize) {
154 if count == 0 {
155 return;
156 }
157 self.rekey(|i| Some(if i >= start { i + count } else { i }));
158 }
159
160 pub fn adjust_for_remove(&self, start: usize, count: usize) {
163 if count == 0 {
164 return;
165 }
166 let end = start + count;
167 self.rekey(|i| {
168 if i < start {
169 Some(i)
170 } else if i >= end {
171 Some(i - count)
172 } else {
173 None
174 }
175 });
176 }
177
178 pub fn adjust_for_move(&self, from: usize, to: usize, count: usize) {
182 if from == to || count == 0 {
183 return;
184 }
185 self.rekey(|i| Some(crate::map_index_after_move(i, from, to, count)));
186 }
187
188 pub fn is_checked(&self, index: usize) -> bool {
190 self.inner
191 .borrow()
192 .per_index
193 .get(&index)
194 .map(|s| s.get())
195 .unwrap_or(false)
196 }
197
198 pub fn checked_indices(&self) -> Vec<usize> {
200 self.checked.get().into_iter().collect()
201 }
202
203 pub fn checked_count(&self) -> usize {
205 self.checked.get().len()
206 }
207
208 pub fn check(&self, index: usize) {
210 let sig = self.signal_for(index);
211 if !sig.get() {
212 sig.set(true);
213 }
214 }
215
216 pub fn uncheck(&self, index: usize) {
218 let sig = self.signal_for(index);
219 if sig.get() {
220 sig.set(false);
221 }
222 }
223
224 pub fn toggle(&self, index: usize) {
226 let sig = self.signal_for(index);
227 sig.set(!sig.get());
228 }
229
230 pub fn check_all(&self, count: usize) {
232 for i in 0..count {
233 self.check(i);
234 }
235 }
236
237 pub fn clear(&self) {
239 let keys: Vec<usize> = self.inner.borrow().per_index.keys().copied().collect();
242 for i in keys {
243 self.uncheck(i);
244 }
245 }
246}
247
248impl Default for CheckedModel {
249 fn default() -> Self {
250 Self::new()
251 }
252}
253
254impl Clone for CheckedModel {
255 fn clone(&self) -> Self {
256 Self {
257 checked: self.checked.clone(),
258 inner: self.inner.clone(),
259 }
260 }
261}
262
263impl std::fmt::Debug for CheckedModel {
264 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
265 f.debug_struct("CheckedModel")
266 .field("checked_count", &self.checked.get().len())
267 .finish()
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn signal_for_returns_same_root_per_index() {
277 let m = CheckedModel::new();
278 let a = m.signal_for(2);
279 let b = m.signal_for(2);
280 assert_eq!(a.get(), b.get());
281 a.set(true);
282 assert!(b.get(), "cached signal handles share the same root");
283 }
284
285 #[test]
286 fn writing_per_index_signal_updates_central() {
287 let m = CheckedModel::new();
288 let s = m.signal_for(7);
289 s.set(true);
290 assert_eq!(m.checked_indices(), vec![7]);
291 s.set(false);
292 assert_eq!(m.checked_indices(), Vec::<usize>::new());
293 }
294
295 #[test]
296 fn check_uncheck_toggle_round_trip() {
297 let m = CheckedModel::new();
298 assert!(!m.is_checked(3));
299 m.check(3);
300 assert!(m.is_checked(3));
301 m.toggle(3);
302 assert!(!m.is_checked(3));
303 m.toggle(3);
304 assert!(m.is_checked(3));
305 m.uncheck(3);
306 assert!(!m.is_checked(3));
307 }
308
309 #[test]
310 fn check_all_then_clear() {
311 let m = CheckedModel::new();
312 m.check_all(5);
313 assert_eq!(m.checked_indices(), vec![0, 1, 2, 3, 4]);
314 m.clear();
315 assert_eq!(m.checked_count(), 0);
316 }
317
318 #[test]
319 fn signal_updates_propagate() {
320 let m = CheckedModel::new();
321 let s = m.signal_for(7);
322 assert!(!s.get());
323 m.check(7);
324 assert!(s.get());
325 m.uncheck(7);
326 assert!(!s.get());
327 }
328
329 #[test]
330 fn unrelated_index_does_not_flip_signal() {
331 let m = CheckedModel::new();
332 let s = m.signal_for(1);
333 m.check(2);
334 assert!(!s.get());
335 }
336
337 #[test]
338 fn adjust_for_insert_shifts_checked_rows() {
339 let m = CheckedModel::new();
340 m.check(2);
341 m.check(4);
342 m.adjust_for_insert(3, 2);
343 assert_eq!(m.checked_indices(), vec![2, 6]);
345 assert!(m.is_checked(2));
346 assert!(m.is_checked(6));
347 assert!(!m.is_checked(4));
348 }
349
350 #[test]
351 fn adjust_for_remove_drops_in_range_and_shifts() {
352 let m = CheckedModel::new();
353 m.check(1);
354 m.check(3);
355 m.check(5);
356 m.adjust_for_remove(2, 2); assert_eq!(m.checked_indices(), vec![1, 3]);
359 assert!(m.is_checked(3), "row that shifted in is checked");
360 }
361
362 #[test]
363 fn adjust_for_move_follows_checked_row() {
364 let m = CheckedModel::new();
365 m.check(0); m.adjust_for_move(0, 2, 1); assert_eq!(m.checked_indices(), vec![2]);
368 assert!(m.is_checked(2));
369 }
370
371 #[test]
372 fn rekey_rewires_observer_so_later_clicks_target_the_new_index() {
373 let m = CheckedModel::new();
376 let s = m.signal_for(2);
377 s.set(true);
378 assert_eq!(m.checked_indices(), vec![2]);
379 m.adjust_for_insert(0, 1); assert_eq!(m.checked_indices(), vec![3]);
381 s.set(false);
383 assert_eq!(m.checked_indices(), Vec::<usize>::new());
384 s.set(true);
385 assert_eq!(m.checked_indices(), vec![3], "observer re-keyed to 3");
386 }
387
388 #[test]
389 fn adjust_is_noop_on_empty_and_zero_count() {
390 let m = CheckedModel::new();
391 m.check(1);
392 m.adjust_for_insert(0, 0);
393 m.adjust_for_remove(5, 0);
394 m.adjust_for_move(2, 2, 1);
395 assert_eq!(m.checked_indices(), vec![1]);
396 }
397}