#[derive(Debug, Clone, PartialEq)]
#[cfg_attr(feature = "persist-table", derive(oxicode::Encode, oxicode::Decode))]
pub struct TableState {
pub column_widths: Vec<f32>,
pub column_order: Vec<usize>,
pub sort_column: Option<usize>,
pub sort_ascending: bool,
pub column_filters: Vec<String>,
pub current_page: usize,
pub page_size: usize,
pub pinned_columns: usize,
pub zebra_striping: bool,
}
impl Default for TableState {
fn default() -> Self {
Self {
column_widths: Vec::new(),
column_order: Vec::new(),
sort_column: None,
sort_ascending: true,
column_filters: Vec::new(),
current_page: 0,
page_size: 50,
pinned_columns: 0,
zebra_striping: false,
}
}
}
impl TableState {
#[allow(clippy::too_many_arguments)]
pub fn from_table_fields(
column_widths: Vec<f32>,
column_order: Vec<usize>,
sort_column: Option<usize>,
sort_ascending: bool,
column_filters: Vec<String>,
current_page: usize,
page_size: usize,
pinned_columns: usize,
zebra_striping: bool,
) -> Self {
Self {
column_widths,
column_order,
sort_column,
sort_ascending,
column_filters,
current_page,
page_size,
pinned_columns,
zebra_striping,
}
}
#[cfg(feature = "persist-table")]
pub fn encode_to_vec(&self) -> Result<Vec<u8>, String> {
oxicode::encode_to_vec(self).map_err(|e| e.to_string())
}
#[cfg(feature = "persist-table")]
pub fn decode_from_slice(bytes: &[u8]) -> Result<Self, String> {
let (state, _consumed) =
oxicode::decode_from_slice::<Self>(bytes).map_err(|e| e.to_string())?;
Ok(state)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct TableStateDiff {
pub column_widths: Option<Vec<f32>>,
pub column_order: Option<Vec<usize>>,
pub sort_column: Option<Option<usize>>,
pub sort_ascending: Option<bool>,
pub column_filters: Option<Vec<String>>,
pub current_page: Option<usize>,
pub page_size: Option<usize>,
pub pinned_columns: Option<usize>,
pub zebra_striping: Option<bool>,
}
pub fn diff(old: &TableState, new: &TableState) -> TableStateDiff {
TableStateDiff {
column_widths: if old.column_widths != new.column_widths {
Some(new.column_widths.clone())
} else {
None
},
column_order: if old.column_order != new.column_order {
Some(new.column_order.clone())
} else {
None
},
sort_column: if old.sort_column != new.sort_column {
Some(new.sort_column)
} else {
None
},
sort_ascending: if old.sort_ascending != new.sort_ascending {
Some(new.sort_ascending)
} else {
None
},
column_filters: if old.column_filters != new.column_filters {
Some(new.column_filters.clone())
} else {
None
},
current_page: if old.current_page != new.current_page {
Some(new.current_page)
} else {
None
},
page_size: if old.page_size != new.page_size {
Some(new.page_size)
} else {
None
},
pinned_columns: if old.pinned_columns != new.pinned_columns {
Some(new.pinned_columns)
} else {
None
},
zebra_striping: if old.zebra_striping != new.zebra_striping {
Some(new.zebra_striping)
} else {
None
},
}
}
pub fn apply_diff(state: &mut TableState, d: &TableStateDiff) {
if let Some(ref v) = d.column_widths {
state.column_widths = v.clone();
}
if let Some(ref v) = d.column_order {
state.column_order = v.clone();
}
if let Some(v) = d.sort_column {
state.sort_column = v;
}
if let Some(v) = d.sort_ascending {
state.sort_ascending = v;
}
if let Some(ref v) = d.column_filters {
state.column_filters = v.clone();
}
if let Some(v) = d.current_page {
state.current_page = v;
}
if let Some(v) = d.page_size {
state.page_size = v;
}
if let Some(v) = d.pinned_columns {
state.pinned_columns = v;
}
if let Some(v) = d.zebra_striping {
state.zebra_striping = v;
}
}
#[cfg(test)]
mod tests {
use super::*;
fn sample_state() -> TableState {
TableState {
column_widths: vec![120.0, 80.0, 200.0],
column_order: vec![0, 2, 1],
sort_column: Some(0),
sort_ascending: true,
column_filters: vec!["".into(), "".into(), "Alice".into()],
current_page: 2,
page_size: 25,
pinned_columns: 1,
zebra_striping: true,
}
}
#[test]
fn default_state_has_sensible_values() {
let state = TableState::default();
assert!(state.column_widths.is_empty());
assert!(state.column_order.is_empty());
assert!(state.sort_column.is_none());
assert!(state.sort_ascending); assert_eq!(state.page_size, 50);
assert!(!state.zebra_striping);
}
#[test]
fn from_table_fields_round_trips() {
let state = TableState::from_table_fields(
vec![100.0, 200.0],
vec![1, 0],
Some(1),
false,
vec!["filter".into(), "".into()],
3,
10,
2,
true,
);
assert_eq!(state.column_widths, vec![100.0, 200.0]);
assert_eq!(state.column_order, vec![1, 0]);
assert_eq!(state.sort_column, Some(1));
assert!(!state.sort_ascending);
assert_eq!(state.column_filters[0], "filter");
assert_eq!(state.current_page, 3);
assert_eq!(state.page_size, 10);
assert_eq!(state.pinned_columns, 2);
assert!(state.zebra_striping);
}
#[test]
fn diff_identical_states_is_empty() {
let a = sample_state();
let b = a.clone();
let d = diff(&a, &b);
assert_eq!(d, TableStateDiff::default());
}
#[test]
fn diff_sort_column_changed() {
let a = sample_state();
let mut b = a.clone();
b.sort_column = Some(2);
let d = diff(&a, &b);
assert_eq!(d.sort_column, Some(Some(2)));
assert!(d.column_widths.is_none(), "column_widths must be unchanged");
}
#[test]
fn diff_column_widths_changed() {
let a = sample_state();
let mut b = a.clone();
b.column_widths = vec![150.0, 80.0, 200.0];
let d = diff(&a, &b);
assert_eq!(
d.column_widths.as_deref(),
Some(&[150.0_f32, 80.0, 200.0][..])
);
}
#[test]
fn apply_diff_modifies_state() {
let mut state = sample_state();
let d = TableStateDiff {
sort_column: Some(Some(2)),
sort_ascending: Some(false),
zebra_striping: Some(false),
..Default::default()
};
apply_diff(&mut state, &d);
assert_eq!(state.sort_column, Some(2));
assert!(!state.sort_ascending);
assert!(!state.zebra_striping);
assert_eq!(state.column_order, vec![0, 2, 1]);
}
#[test]
fn apply_diff_none_fields_unchanged() {
let original = sample_state();
let mut state = original.clone();
apply_diff(&mut state, &TableStateDiff::default());
assert_eq!(state, original);
}
#[test]
fn diff_apply_roundtrip() {
let old = sample_state();
let mut new = old.clone();
new.sort_column = None;
new.page_size = 100;
new.zebra_striping = false;
let d = diff(&old, &new);
let mut reconstructed = old.clone();
apply_diff(&mut reconstructed, &d);
assert_eq!(reconstructed, new);
}
#[cfg(feature = "persist-table")]
#[test]
fn encode_decode_roundtrip() {
let state = sample_state();
let bytes = state.encode_to_vec().expect("encode must succeed");
assert!(!bytes.is_empty(), "encoded bytes must not be empty");
let decoded = TableState::decode_from_slice(&bytes).expect("decode must succeed");
assert_eq!(decoded, state);
}
#[cfg(feature = "persist-table")]
#[test]
fn encode_decode_default_state() {
let state = TableState::default();
let bytes = state.encode_to_vec().expect("encode");
let decoded = TableState::decode_from_slice(&bytes).expect("decode");
assert_eq!(decoded, state);
}
#[cfg(feature = "persist-table")]
#[test]
fn decode_invalid_bytes_returns_err() {
let result = TableState::decode_from_slice(&[0xFF, 0x00, 0xAB]);
assert!(result.is_err(), "invalid bytes must return Err");
}
#[cfg(feature = "persist-table")]
#[test]
fn encode_produces_non_trivial_bytes() {
let state = sample_state();
let bytes = state.encode_to_vec().expect("encode");
assert!(
bytes.len() >= 12,
"encoded bytes too short: {}",
bytes.len()
);
}
#[test]
fn diff_clear_sort_column() {
let mut a = sample_state();
a.sort_column = Some(0);
let mut b = a.clone();
b.sort_column = None;
let d = diff(&a, &b);
assert_eq!(
d.sort_column,
Some(None),
"clearing sort should produce Some(None)"
);
}
#[test]
fn diff_filter_change() {
let a = sample_state();
let mut b = a.clone();
b.column_filters = vec!["new".into(), "".into(), "Alice".into()];
let d = diff(&a, &b);
assert!(d.column_filters.is_some());
assert_eq!(d.column_filters.as_ref().unwrap()[0], "new");
}
}