1#[derive(Debug, Clone, PartialEq)]
62#[cfg_attr(feature = "persist-table", derive(oxicode::Encode, oxicode::Decode))]
63pub struct TableState {
64 pub column_widths: Vec<f32>,
66 pub column_order: Vec<usize>,
69 pub sort_column: Option<usize>,
71 pub sort_ascending: bool,
74 pub column_filters: Vec<String>,
77 pub current_page: usize,
79 pub page_size: usize,
81 pub pinned_columns: usize,
83 pub zebra_striping: bool,
85}
86
87impl Default for TableState {
88 fn default() -> Self {
89 Self {
90 column_widths: Vec::new(),
91 column_order: Vec::new(),
92 sort_column: None,
93 sort_ascending: true,
94 column_filters: Vec::new(),
95 current_page: 0,
96 page_size: 50,
97 pinned_columns: 0,
98 zebra_striping: false,
99 }
100 }
101}
102
103impl TableState {
104 #[allow(clippy::too_many_arguments)]
110 pub fn from_table_fields(
111 column_widths: Vec<f32>,
112 column_order: Vec<usize>,
113 sort_column: Option<usize>,
114 sort_ascending: bool,
115 column_filters: Vec<String>,
116 current_page: usize,
117 page_size: usize,
118 pinned_columns: usize,
119 zebra_striping: bool,
120 ) -> Self {
121 Self {
122 column_widths,
123 column_order,
124 sort_column,
125 sort_ascending,
126 column_filters,
127 current_page,
128 page_size,
129 pinned_columns,
130 zebra_striping,
131 }
132 }
133
134 #[cfg(feature = "persist-table")]
142 pub fn encode_to_vec(&self) -> Result<Vec<u8>, String> {
143 oxicode::encode_to_vec(self).map_err(|e| e.to_string())
144 }
145
146 #[cfg(feature = "persist-table")]
154 pub fn decode_from_slice(bytes: &[u8]) -> Result<Self, String> {
155 let (state, _consumed) =
156 oxicode::decode_from_slice::<Self>(bytes).map_err(|e| e.to_string())?;
157 Ok(state)
158 }
159}
160
161#[derive(Debug, Clone, PartialEq, Default)]
168pub struct TableStateDiff {
169 pub column_widths: Option<Vec<f32>>,
171 pub column_order: Option<Vec<usize>>,
173 pub sort_column: Option<Option<usize>>,
175 pub sort_ascending: Option<bool>,
177 pub column_filters: Option<Vec<String>>,
179 pub current_page: Option<usize>,
181 pub page_size: Option<usize>,
183 pub pinned_columns: Option<usize>,
185 pub zebra_striping: Option<bool>,
187}
188
189pub fn diff(old: &TableState, new: &TableState) -> TableStateDiff {
193 TableStateDiff {
194 column_widths: if old.column_widths != new.column_widths {
195 Some(new.column_widths.clone())
196 } else {
197 None
198 },
199 column_order: if old.column_order != new.column_order {
200 Some(new.column_order.clone())
201 } else {
202 None
203 },
204 sort_column: if old.sort_column != new.sort_column {
205 Some(new.sort_column)
206 } else {
207 None
208 },
209 sort_ascending: if old.sort_ascending != new.sort_ascending {
210 Some(new.sort_ascending)
211 } else {
212 None
213 },
214 column_filters: if old.column_filters != new.column_filters {
215 Some(new.column_filters.clone())
216 } else {
217 None
218 },
219 current_page: if old.current_page != new.current_page {
220 Some(new.current_page)
221 } else {
222 None
223 },
224 page_size: if old.page_size != new.page_size {
225 Some(new.page_size)
226 } else {
227 None
228 },
229 pinned_columns: if old.pinned_columns != new.pinned_columns {
230 Some(new.pinned_columns)
231 } else {
232 None
233 },
234 zebra_striping: if old.zebra_striping != new.zebra_striping {
235 Some(new.zebra_striping)
236 } else {
237 None
238 },
239 }
240}
241
242pub fn apply_diff(state: &mut TableState, d: &TableStateDiff) {
246 if let Some(ref v) = d.column_widths {
247 state.column_widths = v.clone();
248 }
249 if let Some(ref v) = d.column_order {
250 state.column_order = v.clone();
251 }
252 if let Some(v) = d.sort_column {
253 state.sort_column = v;
254 }
255 if let Some(v) = d.sort_ascending {
256 state.sort_ascending = v;
257 }
258 if let Some(ref v) = d.column_filters {
259 state.column_filters = v.clone();
260 }
261 if let Some(v) = d.current_page {
262 state.current_page = v;
263 }
264 if let Some(v) = d.page_size {
265 state.page_size = v;
266 }
267 if let Some(v) = d.pinned_columns {
268 state.pinned_columns = v;
269 }
270 if let Some(v) = d.zebra_striping {
271 state.zebra_striping = v;
272 }
273}
274
275#[cfg(test)]
278mod tests {
279 use super::*;
280
281 fn sample_state() -> TableState {
282 TableState {
283 column_widths: vec![120.0, 80.0, 200.0],
284 column_order: vec![0, 2, 1],
285 sort_column: Some(0),
286 sort_ascending: true,
287 column_filters: vec!["".into(), "".into(), "Alice".into()],
288 current_page: 2,
289 page_size: 25,
290 pinned_columns: 1,
291 zebra_striping: true,
292 }
293 }
294
295 #[test]
296 fn default_state_has_sensible_values() {
297 let state = TableState::default();
298 assert!(state.column_widths.is_empty());
299 assert!(state.column_order.is_empty());
300 assert!(state.sort_column.is_none());
301 assert!(state.sort_ascending); assert_eq!(state.page_size, 50);
303 assert!(!state.zebra_striping);
304 }
305
306 #[test]
307 fn from_table_fields_round_trips() {
308 let state = TableState::from_table_fields(
309 vec![100.0, 200.0],
310 vec![1, 0],
311 Some(1),
312 false,
313 vec!["filter".into(), "".into()],
314 3,
315 10,
316 2,
317 true,
318 );
319 assert_eq!(state.column_widths, vec![100.0, 200.0]);
320 assert_eq!(state.column_order, vec![1, 0]);
321 assert_eq!(state.sort_column, Some(1));
322 assert!(!state.sort_ascending);
323 assert_eq!(state.column_filters[0], "filter");
324 assert_eq!(state.current_page, 3);
325 assert_eq!(state.page_size, 10);
326 assert_eq!(state.pinned_columns, 2);
327 assert!(state.zebra_striping);
328 }
329
330 #[test]
331 fn diff_identical_states_is_empty() {
332 let a = sample_state();
333 let b = a.clone();
334 let d = diff(&a, &b);
335 assert_eq!(d, TableStateDiff::default());
336 }
337
338 #[test]
339 fn diff_sort_column_changed() {
340 let a = sample_state();
341 let mut b = a.clone();
342 b.sort_column = Some(2);
343 let d = diff(&a, &b);
344 assert_eq!(d.sort_column, Some(Some(2)));
345 assert!(d.column_widths.is_none(), "column_widths must be unchanged");
346 }
347
348 #[test]
349 fn diff_column_widths_changed() {
350 let a = sample_state();
351 let mut b = a.clone();
352 b.column_widths = vec![150.0, 80.0, 200.0];
353 let d = diff(&a, &b);
354 assert_eq!(
355 d.column_widths.as_deref(),
356 Some(&[150.0_f32, 80.0, 200.0][..])
357 );
358 }
359
360 #[test]
361 fn apply_diff_modifies_state() {
362 let mut state = sample_state();
363 let d = TableStateDiff {
364 sort_column: Some(Some(2)),
365 sort_ascending: Some(false),
366 zebra_striping: Some(false),
367 ..Default::default()
368 };
369 apply_diff(&mut state, &d);
370 assert_eq!(state.sort_column, Some(2));
371 assert!(!state.sort_ascending);
372 assert!(!state.zebra_striping);
373 assert_eq!(state.column_order, vec![0, 2, 1]);
375 }
376
377 #[test]
378 fn apply_diff_none_fields_unchanged() {
379 let original = sample_state();
380 let mut state = original.clone();
381 apply_diff(&mut state, &TableStateDiff::default());
382 assert_eq!(state, original);
383 }
384
385 #[test]
386 fn diff_apply_roundtrip() {
387 let old = sample_state();
388 let mut new = old.clone();
389 new.sort_column = None;
390 new.page_size = 100;
391 new.zebra_striping = false;
392
393 let d = diff(&old, &new);
394 let mut reconstructed = old.clone();
395 apply_diff(&mut reconstructed, &d);
396 assert_eq!(reconstructed, new);
397 }
398
399 #[cfg(feature = "persist-table")]
400 #[test]
401 fn encode_decode_roundtrip() {
402 let state = sample_state();
403 let bytes = state.encode_to_vec().expect("encode must succeed");
404 assert!(!bytes.is_empty(), "encoded bytes must not be empty");
405 let decoded = TableState::decode_from_slice(&bytes).expect("decode must succeed");
406 assert_eq!(decoded, state);
407 }
408
409 #[cfg(feature = "persist-table")]
410 #[test]
411 fn encode_decode_default_state() {
412 let state = TableState::default();
413 let bytes = state.encode_to_vec().expect("encode");
414 let decoded = TableState::decode_from_slice(&bytes).expect("decode");
415 assert_eq!(decoded, state);
416 }
417
418 #[cfg(feature = "persist-table")]
419 #[test]
420 fn decode_invalid_bytes_returns_err() {
421 let result = TableState::decode_from_slice(&[0xFF, 0x00, 0xAB]);
422 assert!(result.is_err(), "invalid bytes must return Err");
423 }
424
425 #[cfg(feature = "persist-table")]
426 #[test]
427 fn encode_produces_non_trivial_bytes() {
428 let state = sample_state();
429 let bytes = state.encode_to_vec().expect("encode");
430 assert!(
432 bytes.len() >= 12,
433 "encoded bytes too short: {}",
434 bytes.len()
435 );
436 }
437
438 #[test]
439 fn diff_clear_sort_column() {
440 let mut a = sample_state();
441 a.sort_column = Some(0);
442 let mut b = a.clone();
443 b.sort_column = None;
444 let d = diff(&a, &b);
445 assert_eq!(
446 d.sort_column,
447 Some(None),
448 "clearing sort should produce Some(None)"
449 );
450 }
451
452 #[test]
453 fn diff_filter_change() {
454 let a = sample_state();
455 let mut b = a.clone();
456 b.column_filters = vec!["new".into(), "".into(), "Alice".into()];
457 let d = diff(&a, &b);
458 assert!(d.column_filters.is_some());
459 assert_eq!(d.column_filters.as_ref().unwrap()[0], "new");
460 }
461}