UI.9: recover saved docking metadata and declare frontend MSRV

This commit is contained in:
skylar taylor-barrick committed 2026-10-10 18:52:20 -06:00
1 parent a77bf8bff5
commit 2bdb0ea01d
7 files changed
+122 -2

No files matched your search

+1 -1
View File
@@ -7,7 +7,7 @@ default-members = ["crates/*", "apps/designcraft", "apps/designcraft-cli", "xtas
version = "0.6.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.90"
rust-version = "1.95"
repository = "https://github.com/storytold/designcraft"
[workspace.dependencies]
+1
View File
@@ -3,6 +3,7 @@ name = "designcraft-web"
description = "DesignCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish = false
+1
View File
@@ -2,6 +2,7 @@
name = "designcraft"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
publish = false
default-run = "designcraft"
+1 -1
View File
@@ -83,7 +83,7 @@ fn load_prefs(app: &mut DesignApp) {
}
if let Some(p) = prefs_path()
&& let Ok(bytes) = std::fs::read(&p)
&& let Ok(ui) = serde_json::from_slice::<designcraft_ui_egui::UiState>(&bytes)
&& let Ok(ui) = designcraft_ui_egui::UiState::from_preferences_json(&bytes)
{
app.ui = ui;
}
+1
View File
@@ -2,6 +2,7 @@
name = "designcraft-ui-egui"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
description = "DesignCraft egui frontend (InDesign-style UI); swappable — nothing below depends on it"
+1
View File
@@ -18,6 +18,7 @@ pub mod icons;
pub mod menus;
mod panel_docking;
pub mod panels;
mod prefs;
pub mod render_worker;
mod rtl;
pub mod story_editor;
+116
View File
@@ -0,0 +1,116 @@
//! Compatibility recovery at the saved-preferences boundary. Command deserialization stays strict.
use craft_ui::docking::{Layout, Location};
use serde_json::Value;
use crate::UiState;
impl UiState {
/// Read saved preferences, discarding only unreadable optional docking metadata.
/// A layout from a newer craft-ui version must not reset unrelated settings or named workspaces.
pub fn from_preferences_json(bytes: &[u8]) -> serde_json::Result<Self> {
let mut saved: Value = serde_json::from_slice(bytes)?;
recover_docking(&mut saved);
if let Some(workspaces) = saved.get_mut("customWorkspaces").and_then(Value::as_array_mut) {
for workspace in workspaces {
recover_docking(workspace);
}
}
serde_json::from_value(saved)
}
}
fn recover_docking(saved: &mut Value) {
let Some(fields) = saved.as_object_mut() else { return };
if fields.get("docking").is_some_and(|value| serde_json::from_value::<Option<Layout<String>>>(value.clone()).is_err()) {
fields.remove("docking");
}
if let Some(hidden) = fields.get_mut("dockingHidden") {
if let Some(entries) = hidden.as_object_mut() {
entries.retain(|_, value| serde_json::from_value::<Location<String>>(value.clone()).is_ok());
} else {
fields.remove("dockingHidden");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{DesignApp, SavedWorkspace};
use serde_json::json;
fn populated() -> UiState {
let mut app = DesignApp::new(designcraft_engine::Session::new(), Default::default());
app.run("window.panel.move", json!({"panel":"swatches", "anchor":"layers", "zone":"top"})).unwrap();
app.run("window.panel.close", json!({"panel":"properties"})).unwrap();
app.ui.ui_scale = 1.5;
app.ui.language = "ja".into();
app.ui.custom_workspaces = vec![
SavedWorkspace {
name: "Print".into(),
docking: app.ui.docking.clone(),
docking_hidden: app.ui.docking_hidden.clone(),
..Default::default()
},
SavedWorkspace { name: "Proof".into(), docking: app.ui.docking.clone(), ..Default::default() },
];
app.ui
}
#[test]
fn saved_preferences_preserve_valid_layouts_and_hidden_origins() {
let original = serde_json::to_value(populated()).unwrap();
let loaded = UiState::from_preferences_json(&serde_json::to_vec(&original).unwrap()).unwrap();
assert_eq!(serde_json::to_value(loaded).unwrap(), original);
}
#[test]
fn future_layouts_recover_independently_without_resetting_preferences_or_workspaces() {
let original = populated();
for field in ["docking", "dockingHidden"] {
for invalid in [json!({"root":{"FutureLayout":{}}}), json!(false)] {
let mut saved = serde_json::to_value(&original).unwrap();
saved[field] = invalid.clone();
saved["customWorkspaces"][0][field] = invalid;
assert!(serde_json::from_value::<UiState>(saved.clone()).is_err(), "command parsing remains strict");
let loaded = UiState::from_preferences_json(&serde_json::to_vec(&saved).unwrap()).unwrap();
assert_eq!(loaded.ui_scale, 1.5);
assert_eq!(loaded.language, "ja");
assert_eq!(loaded.custom_workspaces.len(), 2);
assert_eq!(loaded.custom_workspaces[0].name, "Print");
assert_eq!(loaded.custom_workspaces[1].name, "Proof");
assert_eq!(loaded.custom_workspaces[1].docking, original.custom_workspaces[1].docking);
if field == "docking" {
assert!(loaded.docking.is_none());
assert!(loaded.custom_workspaces[0].docking.is_none());
assert_eq!(loaded.docking_hidden, original.docking_hidden);
assert_eq!(loaded.custom_workspaces[0].docking_hidden, original.custom_workspaces[0].docking_hidden);
} else {
assert!(loaded.docking_hidden.is_empty());
assert!(loaded.custom_workspaces[0].docking_hidden.is_empty());
assert_eq!(loaded.docking, original.docking);
assert_eq!(loaded.custom_workspaces[0].docking, original.custom_workspaces[0].docking);
}
}
}
}
#[test]
fn unreadable_hidden_origin_does_not_discard_other_origins() {
let original = populated();
let mut saved = serde_json::to_value(&original).unwrap();
saved["dockingHidden"]["futurePanel"] = json!({"FutureLocation":{}});
saved["customWorkspaces"][0]["dockingHidden"]["futurePanel"] = json!(false);
let loaded = UiState::from_preferences_json(&serde_json::to_vec(&saved).unwrap()).unwrap();
assert!(!original.docking_hidden.is_empty());
assert_eq!(loaded.docking_hidden, original.docking_hidden);
assert_eq!(loaded.custom_workspaces[0].docking_hidden, original.custom_workspaces[0].docking_hidden);
}
#[test]
fn unrelated_malformed_preferences_are_still_rejected() {
assert!(UiState::from_preferences_json(br#"{"uiScale":"invalid","docking":false}"#).is_err());
assert!(UiState::from_preferences_json(br#"{"customWorkspaces":false}"#).is_err());
}
}