Skip to main content

fret_ui_headless/table/
headers.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use super::ColumnDef;
5
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub struct HeaderGroupSnapshot {
8    pub depth: usize,
9    pub id: Arc<str>,
10    pub headers: Vec<HeaderSnapshot>,
11}
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct HeaderSnapshot {
15    pub id: Arc<str>,
16    pub column_id: Arc<str>,
17    pub depth: usize,
18    pub index: usize,
19    pub is_placeholder: bool,
20    pub placeholder_id: Option<Arc<str>>,
21    pub col_span: usize,
22    pub row_span: usize,
23    pub sub_header_ids: Vec<Arc<str>>,
24}
25
26#[derive(Debug, Clone)]
27struct ColumnMeta {
28    depth: usize,
29    parent: Option<Arc<str>>,
30    children: Vec<Arc<str>>,
31}
32
33#[derive(Debug, Clone)]
34struct HeaderNode {
35    id: Arc<str>,
36    column_id: Arc<str>,
37    depth: usize,
38    index: usize,
39    is_placeholder: bool,
40    placeholder_id: Option<Arc<str>>,
41    col_span: usize,
42    row_span: usize,
43    sub_headers: Vec<usize>,
44}
45
46fn build_column_meta<TData>(
47    columns: &[ColumnDef<TData>],
48    depth: usize,
49    parent: Option<Arc<str>>,
50    out: &mut HashMap<Arc<str>, ColumnMeta>,
51) {
52    for col in columns {
53        let id = col.id.clone();
54        let children: Vec<Arc<str>> = col.columns.iter().map(|c| c.id.clone()).collect();
55        out.insert(
56            id.clone(),
57            ColumnMeta {
58                depth,
59                parent: parent.clone(),
60                children,
61            },
62        );
63        if !col.columns.is_empty() {
64            build_column_meta(&col.columns, depth + 1, Some(id), out);
65        }
66    }
67}
68
69fn column_is_visible(
70    id: &str,
71    meta: &HashMap<Arc<str>, ColumnMeta>,
72    leaf_visible: &dyn Fn(&str) -> bool,
73    cache: &mut HashMap<Arc<str>, bool>,
74) -> bool {
75    if let Some(&v) = cache.get(id) {
76        return v;
77    }
78
79    let Some(m) = meta.get(id) else {
80        let v = leaf_visible(id);
81        cache.insert(Arc::<str>::from(id), v);
82        return v;
83    };
84
85    let v = if m.children.is_empty() {
86        leaf_visible(id)
87    } else {
88        m.children
89            .iter()
90            .any(|child| column_is_visible(child.as_ref(), meta, leaf_visible, cache))
91    };
92
93    cache.insert(Arc::<str>::from(id), v);
94    v
95}
96
97fn find_max_depth<TData>(
98    columns: &[ColumnDef<TData>],
99    depth_1_based: usize,
100    meta: &HashMap<Arc<str>, ColumnMeta>,
101    leaf_visible: &dyn Fn(&str) -> bool,
102    visible_cache: &mut HashMap<Arc<str>, bool>,
103    max_depth: &mut usize,
104) {
105    *max_depth = (*max_depth).max(depth_1_based);
106    for col in columns {
107        if !column_is_visible(col.id.as_ref(), meta, leaf_visible, visible_cache) {
108            continue;
109        }
110        if !col.columns.is_empty() {
111            find_max_depth(
112                &col.columns,
113                depth_1_based + 1,
114                meta,
115                leaf_visible,
116                visible_cache,
117                max_depth,
118            );
119        }
120    }
121}
122
123fn header_group_id(header_family: Option<&str>, depth: usize) -> Arc<str> {
124    if let Some(family) = header_family {
125        Arc::<str>::from(format!("{}_{}", family, depth))
126    } else {
127        Arc::<str>::from(depth.to_string())
128    }
129}
130
131fn header_id(
132    header_family: Option<&str>,
133    depth: usize,
134    column_id: &str,
135    child_header_id: &str,
136) -> Arc<str> {
137    if let Some(family) = header_family {
138        Arc::<str>::from(format!(
139            "{}_{}_{}_{}",
140            family, depth, column_id, child_header_id
141        ))
142    } else {
143        Arc::<str>::from(format!("{}_{}_{}", depth, column_id, child_header_id))
144    }
145}
146
147fn create_header_group(
148    headers_to_group: Vec<usize>,
149    depth: usize,
150    meta: &HashMap<Arc<str>, ColumnMeta>,
151    _leaf_visible: &dyn Fn(&str) -> bool,
152    _visible_cache: &mut HashMap<Arc<str>, bool>,
153    header_family: Option<&str>,
154    header_groups: &mut Vec<(usize, Arc<str>, Vec<usize>)>,
155    arena: &mut Vec<HeaderNode>,
156) {
157    let group_id = header_group_id(header_family, depth);
158    let mut pending_parent_headers: Vec<usize> = Vec::new();
159
160    for &header_to_group_idx in &headers_to_group {
161        let column_id = arena[header_to_group_idx].column_id.clone();
162        let Some(col_meta) = meta.get(column_id.as_ref()) else {
163            continue;
164        };
165
166        let is_leaf_header = col_meta.depth == depth;
167
168        let mut parent_column_id: Arc<str> = column_id.clone();
169        let mut is_placeholder = false;
170
171        if is_leaf_header {
172            if let Some(parent) = col_meta.parent.clone() {
173                parent_column_id = parent;
174            } else {
175                is_placeholder = true;
176            }
177        } else {
178            is_placeholder = true;
179        }
180
181        if let Some(&latest_idx) = pending_parent_headers.last()
182            && arena[latest_idx].column_id.as_ref() == parent_column_id.as_ref()
183        {
184            arena[latest_idx].sub_headers.push(header_to_group_idx);
185            continue;
186        }
187
188        let placeholder_id = if is_placeholder {
189            let count = pending_parent_headers
190                .iter()
191                .filter(|&&idx| arena[idx].column_id.as_ref() == parent_column_id.as_ref())
192                .count();
193            Some(Arc::<str>::from(count.to_string()))
194        } else {
195            None
196        };
197
198        let child_header_id = arena[header_to_group_idx].id.clone();
199
200        let header_idx = arena.len();
201        arena.push(HeaderNode {
202            id: header_id(
203                header_family,
204                depth,
205                parent_column_id.as_ref(),
206                child_header_id.as_ref(),
207            ),
208            column_id: parent_column_id,
209            depth,
210            index: pending_parent_headers.len(),
211            is_placeholder,
212            placeholder_id,
213            col_span: 0,
214            row_span: 0,
215            sub_headers: vec![header_to_group_idx],
216        });
217        pending_parent_headers.push(header_idx);
218    }
219
220    header_groups.push((depth, group_id, headers_to_group));
221
222    if depth > 0 {
223        create_header_group(
224            pending_parent_headers,
225            depth - 1,
226            meta,
227            _leaf_visible,
228            _visible_cache,
229            header_family,
230            header_groups,
231            arena,
232        );
233    }
234}
235
236fn recurse_headers_for_spans(
237    headers: &[usize],
238    meta: &HashMap<Arc<str>, ColumnMeta>,
239    leaf_visible: &dyn Fn(&str) -> bool,
240    visible_cache: &mut HashMap<Arc<str>, bool>,
241    arena: &mut [HeaderNode],
242) -> Vec<(usize, usize)> {
243    let mut out = Vec::new();
244    for &idx in headers {
245        if !column_is_visible(
246            arena[idx].column_id.as_ref(),
247            meta,
248            leaf_visible,
249            visible_cache,
250        ) {
251            continue;
252        }
253
254        let mut col_span = 0usize;
255        let mut row_span = 0usize;
256        let mut child_row_spans = vec![0usize];
257
258        let sub_headers = arena[idx].sub_headers.clone();
259        if !sub_headers.is_empty() {
260            child_row_spans.clear();
261            for (child_col_span, child_row_span) in
262                recurse_headers_for_spans(&sub_headers, meta, leaf_visible, visible_cache, arena)
263            {
264                col_span += child_col_span;
265                child_row_spans.push(child_row_span);
266            }
267        } else {
268            col_span = 1;
269        }
270
271        let min_child_row_span = child_row_spans.into_iter().min().unwrap_or(0);
272        row_span += min_child_row_span;
273
274        arena[idx].col_span = col_span;
275        arena[idx].row_span = row_span;
276
277        out.push((col_span, row_span));
278    }
279    out
280}
281
282fn snapshot_group(headers: &[usize], arena: &[HeaderNode]) -> Vec<HeaderSnapshot> {
283    headers
284        .iter()
285        .copied()
286        .map(|idx| HeaderSnapshot {
287            id: arena[idx].id.clone(),
288            column_id: arena[idx].column_id.clone(),
289            depth: arena[idx].depth,
290            index: arena[idx].index,
291            is_placeholder: arena[idx].is_placeholder,
292            placeholder_id: arena[idx].placeholder_id.clone(),
293            col_span: arena[idx].col_span,
294            row_span: arena[idx].row_span,
295            sub_header_ids: arena[idx]
296                .sub_headers
297                .iter()
298                .map(|&child| arena[child].id.clone())
299                .collect(),
300        })
301        .collect()
302}
303
304/// TanStack-aligned header-group builder (`buildHeaderGroups`).
305///
306/// - `all_columns`: the full column def tree (group columns + leaf columns).
307/// - `columns_to_group`: ordered leaf column ids (after pinning re-order, if any).
308/// - `leaf_visible`: leaf-level visibility predicate (group visibility is derived from children).
309pub fn build_header_groups<TData>(
310    all_columns: &[ColumnDef<TData>],
311    columns_to_group: &[Arc<str>],
312    leaf_visible: &dyn Fn(&str) -> bool,
313    header_family: Option<&str>,
314) -> Vec<HeaderGroupSnapshot> {
315    let mut meta: HashMap<Arc<str>, ColumnMeta> = HashMap::new();
316    build_column_meta(all_columns, 0, None, &mut meta);
317
318    let mut visible_cache: HashMap<Arc<str>, bool> = HashMap::new();
319
320    let mut max_depth = 0usize;
321    find_max_depth(
322        all_columns,
323        1,
324        &meta,
325        leaf_visible,
326        &mut visible_cache,
327        &mut max_depth,
328    );
329
330    if max_depth == 0 {
331        return Vec::new();
332    }
333
334    let mut arena: Vec<HeaderNode> = Vec::new();
335    let bottom_headers: Vec<usize> = columns_to_group
336        .iter()
337        .enumerate()
338        .map(|(index, col_id)| {
339            let idx = arena.len();
340            arena.push(HeaderNode {
341                id: col_id.clone(),
342                column_id: col_id.clone(),
343                depth: max_depth,
344                index,
345                is_placeholder: false,
346                placeholder_id: None,
347                col_span: 0,
348                row_span: 0,
349                sub_headers: Vec::new(),
350            });
351            idx
352        })
353        .collect();
354
355    let mut header_groups: Vec<(usize, Arc<str>, Vec<usize>)> = Vec::new();
356    create_header_group(
357        bottom_headers,
358        max_depth.saturating_sub(1),
359        &meta,
360        leaf_visible,
361        &mut visible_cache,
362        header_family,
363        &mut header_groups,
364        &mut arena,
365    );
366
367    header_groups.reverse();
368
369    if let Some((_, _, headers)) = header_groups.first() {
370        recurse_headers_for_spans(
371            headers,
372            &meta,
373            leaf_visible,
374            &mut visible_cache,
375            arena.as_mut_slice(),
376        );
377    }
378
379    header_groups
380        .into_iter()
381        .map(|(depth, id, headers)| HeaderGroupSnapshot {
382            depth,
383            id,
384            headers: snapshot_group(&headers, &arena),
385        })
386        .collect()
387}