Skip to main content

orbital_base_components/collection/state/
cascade.rs

1use super::registry::CollectionRegistry;
2use super::selection::{CollectionSelectionMode, CollectionSelectionState};
3
4/// Checkbox tri-state for cascade selection.
5#[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}