fret_ui_headless/table/
column_pinning.rs1use 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#[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}