UI: a setup checklist at the top of the browser

Once a model has geometry or a mesh, the browser shows the analysis stages from the engine's
checklist, each ticked, next, dashed (optional) or open; a click starts the stage's command and a
blocked stage's hover says what it waits for. The × hides it and View › Setup checklist shows it
again; the choice is kept in the per-user prefs.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 21:46:33 -04:00
1 parent e223a6256a
commit 786121c21a
8 files changed
+251

No files matched your search

+59
View File
@@ -42,6 +42,8 @@ pub struct Services {
pub pick_save: Option<PickSave>,
/// The recent-files list's file (JSON); `None` keeps the list for this run only.
pub recent_path: Option<PathBuf>,
/// The user's preferences file (JSON, [`Prefs`]); `None` keeps them for this run only.
pub prefs_path: Option<PathBuf>,
}
const MODEL: Filter = Filter { name: "FieldCraft model", extensions: &["fieldcraft"] };
@@ -164,6 +166,47 @@ impl Recent {
}
}
/// Largest preferences file read.
const MAX_PREFS_BYTES: u64 = 64 * 1024;
/// What the window remembers for the user between runs.
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct Prefs {
/// The setup checklist at the top of the browser ([`crate::setup`]).
pub show_setup: bool,
}
impl Default for Prefs {
fn default() -> Self {
Prefs { show_setup: true }
}
}
impl Prefs {
/// The preferences kept in `file` (an unreadable or malformed file gives the defaults).
pub fn load(file: Option<&Path>) -> Prefs {
file.filter(|f| std::fs::metadata(f).is_ok_and(|m| m.is_file() && m.len() <= MAX_PREFS_BYTES))
.and_then(|f| std::fs::read(f).ok())
.and_then(|b| serde_json::from_slice(&b).ok())
.unwrap_or_default()
}
/// Write them to `file`; a failure is logged (the preferences stay for this run).
pub fn save(&self, file: Option<&Path>) {
let Some(file) = file else { return };
let write = || -> std::io::Result<()> {
if let Some(dir) = file.parent() {
std::fs::create_dir_all(dir)?;
}
std::fs::write(file, serde_json::to_vec_pretty(self).unwrap_or_default())
};
if let Err(e) = write() {
log::warn!("preferences {}: {e}", file.display());
}
}
}
/// A command running in the background on a copy of the session: a file opening, a solve, a
/// mesher started from its form.
pub struct Loading {
@@ -962,6 +1005,22 @@ mod tests {
dir
}
/// Hiding the setup checklist is remembered in the preferences file; a missing or
/// malformed file gives the defaults.
#[test]
fn hiding_the_setup_checklist_is_remembered() {
let file = temp("prefs").join("prefs.json");
assert!(Prefs::load(Some(&file)).show_setup);
let services = || Services { prefs_path: Some(file.clone()), ..Default::default() };
let mut app = ModelApp::new(Session::default()).with_services(services());
app.ui.show_setup = false;
app.save_prefs();
let again = ModelApp::new(Session::default()).with_services(services());
assert!(!again.ui.show_setup);
std::fs::write(&file, b"{nope").unwrap();
assert_eq!(Prefs::load(Some(&file)), Prefs::default());
}
/// A picker that answers `path` and records the filters it was offered.
fn answering(path: Option<String>, offered: Rc<RefCell<Vec<Vec<Filter>>>>) -> PickOpen {
Box::new(move |f: &[Filter]| {