UI.9: recover saved docking metadata and declare frontend MSRV
This commit is contained in:
parent
a77bf8bff5
commit
2bdb0ea01d
|
|
@ -7,7 +7,7 @@ default-members = ["crates/*", "apps/designcraft", "apps/designcraft-cli", "xtas
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.90"
|
rust-version = "1.95"
|
||||||
repository = "https://github.com/storytold/designcraft"
|
repository = "https://github.com/storytold/designcraft"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ name = "designcraft-web"
|
||||||
description = "DesignCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
description = "DesignCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "designcraft"
|
name = "designcraft"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
publish = false
|
publish = false
|
||||||
default-run = "designcraft"
|
default-run = "designcraft"
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ fn load_prefs(app: &mut DesignApp) {
|
||||||
}
|
}
|
||||||
if let Some(p) = prefs_path()
|
if let Some(p) = prefs_path()
|
||||||
&& let Ok(bytes) = std::fs::read(&p)
|
&& 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;
|
app.ui = ui;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
name = "designcraft-ui-egui"
|
name = "designcraft-ui-egui"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
description = "DesignCraft egui frontend (InDesign-style UI); swappable — nothing below depends on it"
|
description = "DesignCraft egui frontend (InDesign-style UI); swappable — nothing below depends on it"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ pub mod icons;
|
||||||
pub mod menus;
|
pub mod menus;
|
||||||
mod panel_docking;
|
mod panel_docking;
|
||||||
pub mod panels;
|
pub mod panels;
|
||||||
|
mod prefs;
|
||||||
pub mod render_worker;
|
pub mod render_worker;
|
||||||
mod rtl;
|
mod rtl;
|
||||||
pub mod story_editor;
|
pub mod story_editor;
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue