Workspaces: saved workspaces last between sessions (#320)
Window > Workspace > Save as New Workspace and Save Changes to this Workspace kept the layout only in memory (`UiState::saved_workspaces` / `saved_floating`): nothing wrote them, so they were gone after a restart. They are now written to `workspaces.json` in the settings store (the config folder on desktop, OPFS on the web) when a workspace is saved, renamed or deleted, and read back when the app starts; a file that doesn't read is ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01QiAJeFttZp83Fh7YuzAR5f
This commit is contained in:
parent
da06c42de1
commit
e96638093c
|
|
@ -295,6 +295,17 @@ pub struct EffectcraftApp {
|
|||
pub(crate) template_list: Option<(Vec<String>, Vec<effectcraft_engine::templates::TemplateInfo>)>,
|
||||
}
|
||||
|
||||
/// The saved workspaces in the settings store ([`effectcraft_engine::config::ConfigStore`]).
|
||||
pub const WORKSPACES_FILE: &str = "workspaces.json";
|
||||
|
||||
#[derive(serde::Serialize, serde::Deserialize)]
|
||||
struct SavedWorkspaces {
|
||||
#[serde(default)]
|
||||
workspaces: std::collections::BTreeMap<String, dock::DockNode>,
|
||||
#[serde(default)]
|
||||
floating: std::collections::BTreeMap<String, Vec<dock::Floating>>,
|
||||
}
|
||||
|
||||
impl EffectcraftApp {
|
||||
pub fn new(session: Session) -> Self {
|
||||
EffectcraftApp {
|
||||
|
|
@ -353,6 +364,34 @@ impl EffectcraftApp {
|
|||
template_list: None,
|
||||
}
|
||||
.with_ui_commands()
|
||||
.with_saved_workspaces()
|
||||
}
|
||||
|
||||
/// The workspaces saved in an earlier session (Window ▸ Workspace ▸ Save as New Workspace,
|
||||
/// Save Changes to this Workspace), from the settings store. A file that doesn't read is
|
||||
/// ignored.
|
||||
fn with_saved_workspaces(mut self) -> Self {
|
||||
let text = self.session.config.as_ref().and_then(|c| c.read(WORKSPACES_FILE));
|
||||
match text.map(|t| serde_json::from_str::<SavedWorkspaces>(&t)) {
|
||||
Some(Ok(w)) => {
|
||||
self.ui.saved_workspaces = w.workspaces;
|
||||
self.ui.saved_floating = w.floating;
|
||||
}
|
||||
Some(Err(e)) => log::warn!("{WORKSPACES_FILE}: {e}"),
|
||||
None => {}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Keep the saved workspaces for the next session (see [`EffectcraftApp::new`]): After
|
||||
/// Effects' saved workspaces last until they are deleted (#320).
|
||||
pub(crate) fn store_saved_workspaces(&self) {
|
||||
let Some(c) = &self.session.config else { return };
|
||||
let w = SavedWorkspaces { workspaces: self.ui.saved_workspaces.clone(), floating: self.ui.saved_floating.clone() };
|
||||
let written = serde_json::to_string(&w).map_err(std::io::Error::other).and_then(|t| c.write(WORKSPACES_FILE, &t));
|
||||
if let Err(e) = written {
|
||||
log::warn!("{WORKSPACES_FILE}: {e}");
|
||||
}
|
||||
}
|
||||
|
||||
/// Offer the frontend's commands (tools, timeline navigation…) for keyboard shortcuts.
|
||||
|
|
|
|||
|
|
@ -943,6 +943,7 @@ pub fn frontend(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, p: Valu
|
|||
let name = app.ui.workspace.clone();
|
||||
app.ui.saved_workspaces.insert(name.clone(), app.ui.dock.clone());
|
||||
app.ui.saved_floating.insert(name.clone(), app.ui.floating.clone());
|
||||
app.store_saved_workspaces();
|
||||
json!({"workspace": name})
|
||||
}
|
||||
"window.saveWorkspaceAs" => {
|
||||
|
|
@ -959,6 +960,7 @@ pub fn frontend(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, p: Valu
|
|||
app.ui.saved_workspaces.insert(name.clone(), app.ui.dock.clone());
|
||||
app.ui.saved_floating.insert(name.clone(), app.ui.floating.clone());
|
||||
app.ui.workspace = name.clone();
|
||||
app.store_saved_workspaces();
|
||||
json!({"workspace": name})
|
||||
}
|
||||
"window.editWorkspaces" => {
|
||||
|
|
@ -983,6 +985,7 @@ pub fn frontend(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, p: Valu
|
|||
if app.ui.saved_workspaces.remove(&name).is_none() && !builtin {
|
||||
return Err(format!("no workspace `{name}`"));
|
||||
}
|
||||
app.store_saved_workspaces();
|
||||
if app.ui.workspace == name {
|
||||
app.set_workspace("Default");
|
||||
}
|
||||
|
|
@ -1000,6 +1003,7 @@ pub fn frontend(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, p: Valu
|
|||
if app.ui.workspace == name {
|
||||
app.ui.workspace = new.to_string();
|
||||
}
|
||||
app.store_saved_workspaces();
|
||||
return Ok(json!({"renamed": new}));
|
||||
}
|
||||
json!({"workspaces": app.workspace_names()})
|
||||
|
|
|
|||
|
|
@ -117,6 +117,42 @@ fn hover(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
|
|||
h.run_steps(3);
|
||||
}
|
||||
|
||||
/// Issue #320: saved workspaces last between sessions (they were only kept in memory): a new
|
||||
/// app on the same settings store lists them and brings their layout back; renamed and deleted
|
||||
/// ones stay that way.
|
||||
#[test]
|
||||
fn saved_workspaces_persist_across_sessions() {
|
||||
use effectcraft_engine::config::{ConfigStore, MemoryConfig};
|
||||
use effectcraft_ui_egui::menus::invoke;
|
||||
let store = std::sync::Arc::new(MemoryConfig::default());
|
||||
let launch = || {
|
||||
let s = Session { config: Some(store.clone()), ..Default::default() };
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.run_steps(2);
|
||||
h
|
||||
};
|
||||
let mut h = launch();
|
||||
let ctx = h.ctx.clone();
|
||||
invoke(h.state_mut(), &ctx, "window.workspace", json!({"name": "Minimal"})).unwrap();
|
||||
invoke(h.state_mut(), &ctx, "window.saveWorkspaceAs", json!({"name": "My Layout"})).unwrap();
|
||||
invoke(h.state_mut(), &ctx, "window.saveWorkspaceAs", json!({"name": "Scratch"})).unwrap();
|
||||
let saved = h.state().ui.dock.clone();
|
||||
drop(h);
|
||||
let mut h = launch();
|
||||
assert_eq!(h.state().saved_workspace_names(), ["My Layout", "Scratch"]);
|
||||
assert_eq!(h.state().ui.workspace, "Default");
|
||||
let ctx = h.ctx.clone();
|
||||
invoke(h.state_mut(), &ctx, "window.workspace", json!({"name": "My Layout"})).unwrap();
|
||||
assert_eq!(h.state().ui.dock, saved);
|
||||
invoke(h.state_mut(), &ctx, "window.editWorkspaces", json!({"name": "Scratch", "delete": true})).unwrap();
|
||||
invoke(h.state_mut(), &ctx, "window.editWorkspaces", json!({"name": "My Layout", "rename": "Edit Bay"})).unwrap();
|
||||
drop(h);
|
||||
assert_eq!(launch().state().saved_workspace_names(), ["Edit Bay"]);
|
||||
// A damaged file is ignored.
|
||||
store.write(effectcraft_ui_egui::WORKSPACES_FILE, "{not json").unwrap();
|
||||
assert!(launch().state().saved_workspace_names().is_empty());
|
||||
}
|
||||
|
||||
/// Issue #191: a workspace saved with Save as New Workspace is listed in Window ▸ Workspace
|
||||
/// (below the built-ins, where the submenu used to be cut off) and choosing it there brings its
|
||||
/// layout back.
|
||||
|
|
|
|||
Loading…
Reference in New Issue