Skip to main content

fret_ui_headless/table/
column_pinning.rs

1use std::collections::HashSet;
2
3use super::{ColumnDef, ColumnId};
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum ColumnPinPosition {
7    Left,
8    Right,
9}
10
11/// TanStack-compatible column pinning state.
12#[derive(Debug, Clone, PartialEq, Eq, Default)]
13pub struct ColumnPinningState {
14    pub left: Vec<ColumnId>,
15    pub right: Vec<ColumnId>,
16}
17
18pub fn is_column_pinned(
19    state: &ColumnPinningState,
20    column: &ColumnId,
21) -> Option<ColumnPinPosition> {
22    if state.left.iter().any(|c| c.as_ref() == column.as_ref()) {
23        return Some(ColumnPinPosition::Left);
24    }
25    if state.right.iter().any(|c| c.as_ref() == column.as_ref()) {
26        return Some(ColumnPinPosition::Right);
27    }
28    None
29}
30
31pub fn is_some_columns_pinned(
32    state: &ColumnPinningState,
33    position: Option<ColumnPinPosition>,
34) -> bool {
35    match position {
36        None => !(state.left.is_empty() && state.right.is_empty()),
37        Some(ColumnPinPosition::Left) => !state.left.is_empty(),
38        Some(ColumnPinPosition::Right) => !state.right.is_empty(),
39    }
40}
41
42pub fn pin_column(
43    state: &mut ColumnPinningState,
44    column: &ColumnId,
45    position: Option<ColumnPinPosition>,
46) {
47    pin_columns(state, position, [column.clone()]);
48}
49
50pub fn pin_columns(
51    state: &mut ColumnPinningState,
52    position: Option<ColumnPinPosition>,
53    columns: impl IntoIterator<Item = ColumnId>,
54) {
55    let mut ids: Vec<ColumnId> = Vec::new();
56    let mut id_set: HashSet<ColumnId> = HashSet::new();
57    for id in columns {
58        if id_set.insert(id.clone()) {
59            ids.push(id);
60        }
61    }
62
63    if id_set.is_empty() {
64        return;
65    }
66
67    state.left.retain(|c| !id_set.contains(c));
68    state.right.retain(|c| !id_set.contains(c));
69
70    match position {
71        None => {}
72        Some(ColumnPinPosition::Left) => state.left.extend(ids),
73        Some(ColumnPinPosition::Right) => state.right.extend(ids),
74    }
75}
76
77pub fn pinned_column(
78    state: &ColumnPinningState,
79    column: &ColumnId,
80    position: Option<ColumnPinPosition>,
81) -> ColumnPinningState {
82    let mut next = state.clone();
83    pin_column(&mut next, column, position);
84    next
85}
86
87pub fn pinned_columns(
88    state: &ColumnPinningState,
89    position: Option<ColumnPinPosition>,
90    columns: impl IntoIterator<Item = ColumnId>,
91) -> ColumnPinningState {
92    let mut next = state.clone();
93    pin_columns(&mut next, position, columns);
94    next
95}
96
97pub fn split_pinned_columns<'c, TData>(
98    columns: &[&'c ColumnDef<TData>],
99    pinning: &ColumnPinningState,
100) -> (
101    Vec<&'c ColumnDef<TData>>,
102    Vec<&'c ColumnDef<TData>>,
103    Vec<&'c ColumnDef<TData>>,
104) {
105    if columns.is_empty() {
106        return (Vec::new(), Vec::new(), Vec::new());
107    }
108
109    let by_id = columns
110        .iter()
111        .copied()
112        .map(|c| (c.id.as_ref(), c))
113        .collect::<std::collections::HashMap<_, _>>();
114
115    let mut out_left: Vec<&ColumnDef<TData>> = Vec::new();
116    let mut out_right: Vec<&ColumnDef<TData>> = Vec::new();
117
118    for id in &pinning.left {
119        if let Some(col) = by_id.get(id.as_ref()).copied() {
120            out_left.push(col);
121        }
122    }
123    for id in &pinning.right {
124        if let Some(col) = by_id.get(id.as_ref()).copied() {
125            out_right.push(col);
126        }
127    }
128
129    let pinned: HashSet<&str> = pinning
130        .left
131        .iter()
132        .chain(pinning.right.iter())
133        .map(|id| id.as_ref())
134        .collect();
135
136    let mut out_center: Vec<&ColumnDef<TData>> = Vec::new();
137    for col in columns {
138        if pinned.contains(col.id.as_ref()) {
139            continue;
140        }
141        out_center.push(col);
142    }
143
144    (out_left, out_center, out_right)
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn split_pinned_columns_respects_left_right_and_keeps_center_order() {
153        #[derive(Debug)]
154        struct Item;
155
156        let a = ColumnDef::<Item>::new("a");
157        let b = ColumnDef::<Item>::new("b");
158        let c = ColumnDef::<Item>::new("c");
159        let d = ColumnDef::<Item>::new("d");
160
161        let columns = vec![&a, &b, &c, &d];
162        let pinning = ColumnPinningState {
163            left: vec!["c".into()],
164            right: vec!["a".into()],
165        };
166
167        let (left, center, right) = split_pinned_columns(columns.as_slice(), &pinning);
168        assert_eq!(
169            left.iter().map(|c| c.id.as_ref()).collect::<Vec<_>>(),
170            vec!["c"]
171        );
172        assert_eq!(
173            center.iter().map(|c| c.id.as_ref()).collect::<Vec<_>>(),
174            vec!["b", "d"]
175        );
176        assert_eq!(
177            right.iter().map(|c| c.id.as_ref()).collect::<Vec<_>>(),
178            vec!["a"]
179        );
180    }
181
182    #[test]
183    fn pin_column_moves_between_sides_and_unpins() {
184        let mut state = ColumnPinningState {
185            left: vec!["a".into()],
186            right: vec!["b".into()],
187        };
188
189        pin_column(
190            &mut state,
191            &ColumnId::from("a"),
192            Some(ColumnPinPosition::Right),
193        );
194        assert!(state.left.is_empty());
195        assert_eq!(
196            state.right.iter().map(|c| c.as_ref()).collect::<Vec<_>>(),
197            vec!["b", "a"]
198        );
199        assert_eq!(
200            is_column_pinned(&state, &ColumnId::from("a")),
201            Some(ColumnPinPosition::Right)
202        );
203
204        pin_column(&mut state, &ColumnId::from("b"), None);
205        assert_eq!(
206            state.right.iter().map(|c| c.as_ref()).collect::<Vec<_>>(),
207            vec!["a"]
208        );
209        assert_eq!(is_column_pinned(&state, &ColumnId::from("b")), None);
210    }
211}