orbital_base_components/collection/state/
cascade.rs1use super::registry::CollectionRegistry;
2use super::selection::{CollectionSelectionMode, CollectionSelectionState};
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum CollectionCheckboxState {
7 Unchecked,
8 Checked,
9 Indeterminate,
10}
11
12pub fn checkbox_state(
13 registry: &CollectionRegistry,
14 selection: &CollectionSelectionState,
15 item_id: &str,
16) -> CollectionCheckboxState {
17 if selection.is_selected(item_id) {
18 return CollectionCheckboxState::Checked;
19 }
20
21 let descendants = registry.descendant_ids(item_id);
22 if descendants.is_empty() {
23 return CollectionCheckboxState::Unchecked;
24 }
25
26 let selected_count = descendants
27 .iter()
28 .filter(|id| selection.is_selected(id))
29 .count();
30
31 if selected_count == 0 {
32 CollectionCheckboxState::Unchecked
33 } else {
34 CollectionCheckboxState::Indeterminate
35 }
36}
37
38pub fn apply_cascade(
39 registry: &CollectionRegistry,
40 selection: &CollectionSelectionState,
41 item_id: &str,
42 selected: bool,
43) {
44 if selection.mode != CollectionSelectionMode::Checkbox || !selection.cascade {
45 return;
46 }
47
48 let descendants = registry.descendant_ids(item_id);
49 selection.selected_items.update(|set| {
50 if selected {
51 set.insert(item_id.to_string());
52 for id in &descendants {
53 set.insert(id.clone());
54 }
55 } else {
56 set.remove(item_id);
57 for id in &descendants {
58 set.remove(id);
59 }
60 }
61 });
62
63 update_ancestors(registry, selection, item_id);
64}
65
66fn update_ancestors(
67 registry: &CollectionRegistry,
68 selection: &CollectionSelectionState,
69 item_id: &str,
70) {
71 for ancestor_id in registry.ancestor_ids(item_id) {
72 let children = registry.ordered_children_ids(&ancestor_id);
73 if children.is_empty() {
74 continue;
75 }
76 let all_selected = children.iter().all(|id| selection.is_selected(id));
77 selection.selected_items.update(|set| {
78 if all_selected {
79 set.insert(ancestor_id.clone());
80 } else {
81 set.remove(&ancestor_id);
82 }
83 });
84 }
85}
86
87pub fn shift_range_in_visible(
88 visible: &[String],
89 anchor: &str,
90 item_id: &str,
91) -> Option<Vec<String>> {
92 let start = visible.iter().position(|id| id == anchor)?;
93 let end = visible.iter().position(|id| id == item_id)?;
94 let (from, to) = if start <= end {
95 (start, end)
96 } else {
97 (end, start)
98 };
99 Some(visible[from..=to].to_vec())
100}
101
102pub fn typeahead_match(
103 registry: &CollectionRegistry,
104 visible: &[String],
105 prefix: &str,
106 start_after: Option<&str>,
107) -> Option<String> {
108 let lower = prefix.to_lowercase();
109 let start_index = start_after
110 .and_then(|id| visible.iter().position(|v| v == id))
111 .map(|i| i + 1)
112 .unwrap_or(0);
113
114 for id in visible
115 .iter()
116 .skip(start_index)
117 .chain(visible.iter().take(start_index))
118 {
119 if registry
120 .get_entry(id)
121 .is_some_and(|entry| entry.label.to_lowercase().starts_with(&lower))
122 {
123 return Some(id.clone());
124 }
125 }
126 None
127}