Skip to main content

fret_ui_headless/table/
row_expanding.rs

1use std::collections::HashSet;
2use std::iter::FromIterator;
3
4use super::{RowIndex, RowKey, RowModel};
5
6/// TanStack-compatible expanded state.
7///
8/// In TanStack Table v8, the expanded state is `true | Record<RowId, boolean>`.
9/// We model this as:
10/// - `All`: every row is considered expanded (`expanded === true`).
11/// - `Keys`: a set of explicitly expanded rows.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum ExpandingState {
14    All,
15    Keys(HashSet<RowKey>),
16}
17
18impl Default for ExpandingState {
19    fn default() -> Self {
20        Self::Keys(HashSet::new())
21    }
22}
23
24impl FromIterator<RowKey> for ExpandingState {
25    fn from_iter<T: IntoIterator<Item = RowKey>>(iter: T) -> Self {
26        Self::Keys(HashSet::from_iter(iter))
27    }
28}
29
30pub fn is_row_expanded(row_key: RowKey, expanded: &ExpandingState) -> bool {
31    match expanded {
32        ExpandingState::All => true,
33        ExpandingState::Keys(keys) => keys.contains(&row_key),
34    }
35}
36
37pub fn is_some_rows_expanded(expanded: &ExpandingState) -> bool {
38    match expanded {
39        ExpandingState::All => true,
40        ExpandingState::Keys(keys) => !keys.is_empty(),
41    }
42}
43
44pub fn set_all_rows_expanded(expanded: &mut ExpandingState, expanded_value: bool) {
45    if expanded_value {
46        *expanded = ExpandingState::All;
47    } else {
48        *expanded = ExpandingState::default();
49    }
50}
51
52pub fn toggle_all_rows_expanded(expanded: &mut ExpandingState, expanded_value: Option<bool>) {
53    let next = expanded_value.unwrap_or(!matches!(expanded, ExpandingState::All));
54    set_all_rows_expanded(expanded, next);
55}
56
57pub fn toggle_row_expanded<'a, TData>(
58    expanded: &mut ExpandingState,
59    row_model: &RowModel<'a, TData>,
60    row_key: RowKey,
61    expanded_value: Option<bool>,
62) {
63    let exists = is_row_expanded(row_key, expanded);
64    let expanded_value = expanded_value.unwrap_or(!exists);
65
66    match expanded {
67        ExpandingState::All => {
68            if expanded_value {
69                return;
70            }
71
72            // Convert `All` -> `Keys(all_row_ids)` and then remove the row.
73            let mut keys: HashSet<RowKey> = row_model.rows_by_key().keys().copied().collect();
74            keys.remove(&row_key);
75            *expanded = ExpandingState::Keys(keys);
76        }
77        ExpandingState::Keys(keys) => {
78            if expanded_value {
79                keys.insert(row_key);
80            } else {
81                keys.remove(&row_key);
82            }
83        }
84    }
85}
86
87pub fn row_can_expand<TData>(row_model: &RowModel<'_, TData>, row: RowIndex) -> bool {
88    row_model.row(row).is_some_and(|r| !r.sub_rows.is_empty())
89}
90
91pub fn row_is_all_parents_expanded<TData>(
92    row_model: &RowModel<'_, TData>,
93    expanded: &ExpandingState,
94    row: RowIndex,
95) -> bool {
96    let mut current = row_model.row(row);
97    while let Some(r) = current {
98        let Some(parent) = r.parent else {
99            return true;
100        };
101        let Some(parent_row) = row_model.row(parent) else {
102            return true;
103        };
104        if !is_row_expanded(parent_row.key, expanded) {
105            return false;
106        }
107        current = Some(parent_row);
108    }
109    true
110}
111
112pub fn expanded_depth<TData>(row_model: &RowModel<'_, TData>, expanded: &ExpandingState) -> u16 {
113    match expanded {
114        ExpandingState::All => row_model
115            .arena()
116            .iter()
117            .map(|r| r.depth.saturating_add(1))
118            .max()
119            .unwrap_or(0),
120        ExpandingState::Keys(keys) => keys
121            .iter()
122            .filter_map(|k| row_model.row_by_key(*k).and_then(|i| row_model.row(i)))
123            .map(|r| r.depth.saturating_add(1))
124            .max()
125            .unwrap_or(0),
126    }
127}
128
129/// TanStack-aligned "expanded row model": a `RowModel` whose `flat_rows` contain only the
130/// visible rows under the current expansion state.
131///
132/// Notes:
133/// - `arena` and `rows_by_key` are preserved (like TanStack's `rowsById`) so callers can still
134///   resolve row metadata for collapsed descendants.
135/// - `root_rows` are not changed by expansion; only the flattened visible order is.
136pub fn expand_row_model<'a, TData>(
137    row_model: &RowModel<'a, TData>,
138    expanded: &ExpandingState,
139    mut row_is_expanded: impl FnMut(RowKey, &TData) -> bool,
140) -> RowModel<'a, TData> {
141    if row_model.root_rows().is_empty() {
142        return row_model.clone();
143    }
144    if !is_some_rows_expanded(expanded) {
145        return row_model.clone();
146    }
147
148    let mut out = row_model.clone();
149    out.root_rows.clear();
150
151    fn push_visible<TData>(
152        source: &RowModel<'_, TData>,
153        row_is_expanded: &mut impl FnMut(RowKey, &TData) -> bool,
154        out: &mut Vec<RowIndex>,
155        row: RowIndex,
156    ) {
157        out.push(row);
158        let Some(r) = source.row(row) else {
159            return;
160        };
161        if r.sub_rows.is_empty() {
162            return;
163        }
164        if !row_is_expanded(r.key, r.original) {
165            return;
166        }
167        for &child in &r.sub_rows {
168            push_visible(source, row_is_expanded, out, child);
169        }
170    }
171
172    for &root in row_model.root_rows() {
173        push_visible(row_model, &mut row_is_expanded, &mut out.root_rows, root);
174    }
175
176    out
177}
178
179#[cfg(test)]
180mod tests {
181    use super::*;
182    use crate::table::Table;
183
184    #[derive(Debug, Clone)]
185    struct Node {
186        id: u64,
187        children: Vec<Node>,
188    }
189
190    #[test]
191    fn expanded_depth_tracks_max_depth_plus_one() {
192        let data = vec![Node {
193            id: 1,
194            children: vec![Node {
195                id: 10,
196                children: vec![Node {
197                    id: 100,
198                    children: Vec::new(),
199                }],
200            }],
201        }];
202
203        let table = Table::builder(&data)
204            .get_row_key(|n, _i, _p| RowKey(n.id))
205            .get_sub_rows(|n, _i| Some(n.children.as_slice()))
206            .build();
207        let core = table.core_row_model();
208
209        let expanded = ExpandingState::from_iter([RowKey(1), RowKey(10)]);
210        assert_eq!(expanded_depth(core, &expanded), 2);
211    }
212
213    #[test]
214    fn toggle_all_sets_all_variant() {
215        let mut expanded = ExpandingState::default();
216        toggle_all_rows_expanded(&mut expanded, Some(true));
217        assert!(matches!(expanded, ExpandingState::All));
218        toggle_all_rows_expanded(&mut expanded, Some(false));
219        assert!(matches!(expanded, ExpandingState::Keys(_)));
220    }
221}