use serde::{Deserialize, Serialize};
use teksilo_settings::Versioned;
use crate::splitter::SplitterState;
use super::geometry::CornerOwners;
use super::model::TabPresentation;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DockTabState {
pub id: u64,
#[serde(default)]
pub splitter: SplitterState,
#[serde(default)]
pub panes: Vec<u64>,
#[serde(default)]
pub hidden: bool,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DockSideState {
pub presentation: TabPresentation,
pub size_px: f32,
pub visible: bool,
#[serde(default)]
pub selected_tab: usize,
#[serde(default)]
pub tabs: Vec<DockTabState>,
#[serde(default)]
pub rail_size: usize,
#[serde(default)]
pub tab_display: usize,
}
impl Default for DockSideState {
fn default() -> Self {
Self {
presentation: TabPresentation::Strip,
size_px: 240.0,
visible: false,
selected_tab: 0,
tabs: Vec::new(),
rail_size: 0,
tab_display: 0,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct DockLayoutState {
#[serde(default = "default_version")]
pub version: u32,
#[serde(default)]
pub leading: DockSideState,
#[serde(default)]
pub trailing: DockSideState,
#[serde(default)]
pub top: DockSideState,
#[serde(default)]
pub bottom: DockSideState,
#[serde(default)]
pub corners: CornerOwners,
}
fn default_version() -> u32 {
DockLayoutState::CURRENT_VERSION
}
impl Default for DockLayoutState {
fn default() -> Self {
Self {
version: DockLayoutState::CURRENT_VERSION,
leading: DockSideState::default(),
trailing: DockSideState::default(),
top: DockSideState::default(),
bottom: DockSideState::default(),
corners: CornerOwners::default(),
}
}
}
impl Versioned for DockLayoutState {
const CURRENT_VERSION: u32 = 1;
fn version(&self) -> u32 {
self.version
}
fn set_version(&mut self, v: u32) {
self.version = v;
}
}
#[cfg(test)]
mod tests {
use super::*;
use teksilo_settings::{MigrationError, Migrator};
fn sample_state() -> DockLayoutState {
DockLayoutState {
version: DockLayoutState::CURRENT_VERSION,
leading: DockSideState {
presentation: TabPresentation::Rail,
size_px: 280.0,
visible: true,
selected_tab: 1,
tabs: vec![
DockTabState {
id: 1,
splitter: SplitterState::default(),
panes: vec![10],
hidden: false,
},
DockTabState {
id: 2,
splitter: SplitterState::default(),
panes: vec![20, 21],
hidden: true,
},
],
rail_size: 2,
tab_display: 2,
},
trailing: DockSideState::default(),
top: DockSideState::default(),
bottom: DockSideState::default(),
corners: CornerOwners::default(),
}
}
#[test]
fn toml_round_trips_through_the_migrator() {
let state = sample_state();
let value = toml::Value::try_from(&state).expect("serialize");
let restored = Migrator::<DockLayoutState>::new()
.run(value)
.expect("migrate");
assert_eq!(restored, state);
}
#[test]
fn missing_version_field_loads_as_current() {
let state = sample_state();
let mut value = toml::Value::try_from(&state).unwrap();
value.as_table_mut().unwrap().remove("version");
let restored = Migrator::<DockLayoutState>::new().run(value).unwrap();
assert_eq!(restored.version, DockLayoutState::CURRENT_VERSION);
assert_eq!(restored.leading.tabs.len(), 2, "payload survives");
}
#[test]
fn newer_than_current_is_refused() {
let mut value = toml::Value::try_from(sample_state()).unwrap();
value
.as_table_mut()
.unwrap()
.insert("version".into(), toml::Value::Integer(99));
let err = Migrator::<DockLayoutState>::new().run(value).unwrap_err();
assert!(
matches!(
err,
MigrationError::NewerThanCurrent {
on_disk: 99,
current: 1
}
),
"expected NewerThanCurrent, got {err:?}"
);
}
#[test]
fn partial_legacy_toml_fills_additive_defaults() {
let src = r#"
[leading]
presentation = "Rail"
size_px = 300.0
visible = true
"#;
let value: toml::Value = toml::from_str(src).expect("parse");
let restored = Migrator::<DockLayoutState>::new().run(value).unwrap();
assert_eq!(restored.version, DockLayoutState::CURRENT_VERSION);
assert!(restored.leading.visible);
assert_eq!(restored.leading.size_px, 300.0);
assert!(restored.leading.tabs.is_empty(), "tabs default to empty");
assert_eq!(
restored.trailing,
DockSideState::default(),
"absent sides default"
);
}
#[test]
fn migration_step_promotes_a_v0_payload() {
let mut value = toml::Value::try_from(sample_state()).unwrap();
{
let t = value.as_table_mut().unwrap();
t.insert("version".into(), toml::Value::Integer(0));
let leading = t.get_mut("leading").unwrap().as_table_mut().unwrap();
let size = leading.remove("size_px").unwrap();
leading.insert("size".into(), size);
}
let migrator = Migrator::<DockLayoutState>::new().step(0, |mut v| {
if let Some(leading) = v.get_mut("leading").and_then(|l| l.as_table_mut())
&& let Some(size) = leading.remove("size")
{
leading.insert("size_px".into(), size);
}
Ok(v)
});
let restored = migrator.run(value).expect("migrate v0→v1");
assert_eq!(restored.version, 1);
assert_eq!(restored.leading.size_px, 280.0);
}
}