Remove the SETUP card: the browser tree shows the progress

The setup checklist card (#265), its show_setup view setting, the View
menu checkbox and the preferences file that only remembered it go; the
tree's marks and ghost rows replace them. Session::progress and
model.check's progress stay for agents. An old prefs.json is no longer
read.

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 22:49:07 -04:00
1 parent f5a84f5f74
commit 012119e2c2
8 files changed
+3 -239

No files matched your search

-1
View File
@@ -188,7 +188,6 @@ fn services(inbox: Inbox, ctx: egui::Context) -> Services {
pick_save: Some(Box::new(|name: &str, _: &[Filter]| Some(page_path(name)))),
// The recent list holds the page's files, which go with the page.
recent_path: None,
prefs_path: None,
}
}
-1
View File
@@ -95,7 +95,6 @@ fn services() -> Services {
})),
// Beside the recovery folder in the per-user data folder.
recent_path: fieldcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("recent.json"))),
prefs_path: fieldcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("prefs.json"))),
}
}
-1
View File
@@ -525,7 +525,6 @@ pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) {
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(6, 6)))
.show(ui, |ui| {
app.place("browser", ui.max_rect());
crate::setup::card(app, ui);
ui.label(RichText::new("BROWSER").font(crate::theme::label_font(ui.ctx(), 10.5)).color(t.text_dim));
ui.add_space(4.0);
egui::ScrollArea::vertical().id_salt("fc_browser_scroll").auto_shrink([false, false]).show(ui, |ui| {
-24
View File
@@ -597,30 +597,6 @@ mod tests {
}
}
/// The setup checklist shows once there is something to set up, and a click on its next
/// stage opens that stage's form (#74).
#[test]
fn a_setup_stage_click_starts_its_command() {
let mut app = ModelApp::new(Session::default());
let ctx = egui::Context::default();
frame(&mut app, &ctx);
assert!(!app.layout.contains_key("setup"), "an empty model shows the Start card instead");
let cube = [[0, 0, 0], [1, 0, 0], [1, 1, 0], [0, 1, 0], [0, 0, 1], [1, 0, 1], [1, 1, 1], [0, 1, 1]];
app.run("mesh.block", json!({"body": "B", "kind": "hex8", "corners": cube, "divisions": [1, 1, 1]})).unwrap();
frame(&mut app, &ctx);
let at = app.layout[crate::setup::region(fieldcraft_engine::progress::Stage::Material)].center();
call_in(&mut app, &ctx, "ui.click", json!({"x": at.x, "y": at.y}));
for _ in 0..4 {
frame(&mut app, &ctx);
}
assert_eq!(app.dialog.as_ref().map(|d| d.command), Some("model.material.create"));
// Hidden from the View menu (or `ui.set`), it stays hidden.
assert_eq!(call_in(&mut app, &ctx, "ui.set", json!({"show_setup": false}))["ok"], true);
app.layout.clear();
frame(&mut app, &ctx);
assert!(!app.layout.contains_key("setup"));
}
/// A meshed model's Materials node shows a highlighted ghost row; a click on it opens the
/// material form, and `ui.inspect` reports each node's state.
#[test]
-59
View File
@@ -42,8 +42,6 @@ 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"] };
@@ -166,47 +164,6 @@ 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 {
@@ -1069,22 +1026,6 @@ 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]| {
+3 -20
View File
@@ -42,7 +42,6 @@ pub mod results;
pub mod results_panel;
pub mod scene;
pub mod selection;
pub mod setup;
pub mod theme;
pub mod timeline;
pub mod titlebar;
@@ -88,8 +87,6 @@ pub struct UiState {
pub show_tree: bool,
/// The journal timeline (bottom).
pub show_timeline: bool,
/// The setup checklist at the top of the browser (remembered per user).
pub show_setup: bool,
/// The workspace shown, and its toolbar tab.
pub workspace: Workspace,
pub tab: Tab,
@@ -113,7 +110,6 @@ impl Default for UiState {
dark: true,
show_tree: true,
show_timeline: true,
show_setup: true,
workspace: Workspace::default(),
tab: Tab::default(),
palette: false,
@@ -356,8 +352,7 @@ pub struct ModelApp {
/// Recently opened and saved files.
pub recent: files::Recent,
/// The user's preferences as last saved.
prefs: files::Prefs,
/// The setup checklist and what it was worked out for.
/// The setup progress the browser marks, and what it was worked out for.
setup: (Option<(u64, u64, bool, bool)>, std::sync::Arc<Vec<fieldcraft_engine::progress::StageState>>),
/// The file opening in the background, and files waiting to open after it.
pub loading: Option<files::Loading>,
@@ -436,7 +431,6 @@ impl ModelApp {
quit_requested: false,
services: files::Services::default(),
recent: files::Recent::default(),
prefs: files::Prefs::default(),
setup: (None, std::sync::Arc::default()),
loading: None,
queued: std::collections::VecDeque::new(),
@@ -469,8 +463,6 @@ impl ModelApp {
/// Use the host's services (file pickers, the recent-files file) and load the recent list.
pub fn with_services(mut self, services: files::Services) -> Self {
self.recent = files::Recent::load(services.recent_path.clone());
self.prefs = files::Prefs::load(services.prefs_path.as_deref());
self.ui.show_setup = self.prefs.show_setup;
self.services = services;
self
}
@@ -1327,7 +1319,6 @@ impl ModelApp {
browser::browser(self, ui);
}
context::panel(self, ui);
self.save_prefs();
egui::CentralPanel::default().frame(egui::Frame::NONE).show(ui, |ui| viewport::show(self, ui));
palette::popup(self, ui.ctx());
files::show(self, ui.ctx());
@@ -1336,8 +1327,8 @@ impl ModelApp {
}
}
/// The setup checklist ([`fieldcraft_engine::progress`]), worked out again only when the model
/// or its results change.
/// The setup progress ([`fieldcraft_engine::progress`]) the browser marks, worked out again
/// only when the model or its results change.
pub fn setup_stages(&mut self) -> std::sync::Arc<Vec<fieldcraft_engine::progress::StageState>> {
use fieldcraft_engine::requirement::Requirement;
let key = (self.revision, self.session.revision(), Requirement::Result.met(&self.session), Requirement::ResultsShown.met(&self.session));
@@ -1347,14 +1338,6 @@ impl ModelApp {
self.setup.1.clone()
}
/// Save the preferences the window changed (a View menu toggle, `ui.set`).
pub(crate) fn save_prefs(&mut self) {
if self.ui.show_setup != self.prefs.show_setup {
self.prefs.show_setup = self.ui.show_setup;
self.prefs.save(self.services.prefs_path.as_deref());
}
}
/// Remember where a region of the window was drawn (`ui.inspect`'s `layout`).
pub fn place(&mut self, region: &'static str, rect: egui::Rect) {
self.layout.insert(region, rect);
-132
View File
@@ -1,132 +0,0 @@
//! The setup checklist at the top of the browser: the analysis stages from geometry to results
//! ([`fieldcraft_engine::progress`]), each ticked when done, the next one highlighted, and
//! optional ones dashed. A click on a stage starts its command (as its toolbar button would);
//! a blocked stage's hover says what it waits for. It shows once the model has geometry or a
//! mesh (before that the viewport's Start card is the entry), and the × hides it until the
//! View menu shows it again; the choice is remembered per user.
use egui::{FontId, Pos2, Rect, Sense, Stroke, pos2, vec2};
use fieldcraft_engine::progress::{Stage, StageState, Status};
use crate::ModelApp;
use crate::theme::Tokens;
const ROW_H: f32 = 20.0;
/// Where a stage's row was drawn (`ui.inspect`'s `layout`).
pub fn region(stage: Stage) -> &'static str {
match stage {
Stage::Geometry => "setup/geometry",
Stage::Prepare => "setup/prepare",
Stage::Mesh => "setup/mesh",
Stage::Material => "setup/material",
Stage::Section => "setup/section",
Stage::Sets => "setup/sets",
Stage::BoundaryConditions => "setup/boundary_conditions",
Stage::Loads => "setup/loads",
Stage::Step => "setup/step",
Stage::Solve => "setup/solve",
Stage::Results => "setup/results",
}
}
pub fn card(app: &mut ModelApp, ui: &mut egui::Ui) {
if !app.ui.show_setup {
return;
}
let stages = app.setup_stages();
if stages.first().is_none_or(|g| g.status != Status::Done) {
return;
}
let t = Tokens::get(app.ui.dark);
let mut start = None;
let frame = egui::Frame::NONE.fill(t.dialog_bg).stroke(Stroke::new(1.0, t.border)).corner_radius(4.0).inner_margin(egui::Margin::same(6));
let r = frame.show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
ui.horizontal(|ui| {
ui.label(egui::RichText::new("SETUP").font(crate::theme::label_font(ui.ctx(), 10.5)).color(t.text_dim));
let done = stages.iter().filter(|s| matches!(s.status, Status::Done)).count();
ui.label(egui::RichText::new(format!("{done} of {}", stages.len())).size(10.5).color(t.text_dim));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let close = ui.add(egui::Button::new(egui::RichText::new("×").size(13.0).color(t.text_dim)).frame(false));
if close.on_hover_text("Hide the setup checklist (View › Setup checklist shows it again)").clicked() {
app.ui.show_setup = false;
}
});
});
ui.add_space(2.0);
for s in stages.iter() {
if row(app, ui, &t, s) {
start = Some(s.command);
}
}
});
app.place("setup", r.response.rect);
ui.add_space(6.0);
if let Some(command) = start {
// A command that cannot run yet says why in the status bar.
let _ = app.start(command);
}
}
/// One stage; true when clicked.
fn row(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, s: &StageState) -> bool {
let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), ROW_H), Sense::click());
app.place(region(s.stage), r);
let next = s.status == Status::Next;
if next {
ui.painter().rect_filled(r, 3.0, t.accent_soft);
} else if resp.hovered() {
ui.painter().rect_filled(r, 3.0, t.hover);
}
let mark = Rect::from_center_size(pos2(r.left() + 10.0, r.center().y), vec2(10.0, 10.0));
paint_mark(ui.painter(), mark, s.status, t);
let colour = match s.status {
Status::Done | Status::Next => t.text,
Status::Todo | Status::Blocked | Status::Optional => t.text_dim,
};
let x = r.left() + 22.0;
ui.painter().with_clip_rect(r).text(pos2(x, r.center().y), egui::Align2::LEFT_CENTER, s.label, FontId::proportional(12.5), colour);
let note = match s.status {
Status::Next => Some("next"),
Status::Optional => Some("optional"),
_ => None,
};
if let Some(n) = note {
let c = if next { t.accent } else { t.text_dim };
ui.painter().text(pos2(r.right() - 4.0, r.center().y), egui::Align2::RIGHT_CENTER, n, FontId::proportional(11.0), c);
}
let tip = match &s.detail {
Some(why) => format!("{}\n\n{why}", s.place),
None => s.place.clone(),
};
let resp = resp.on_hover_text(tip);
resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, s.label));
resp.clicked()
}
/// ✓ done, → next, – optional, an open circle for the rest (struck through when blocked).
fn paint_mark(p: &egui::Painter, r: Rect, status: Status, t: &Tokens) {
let at = |x: f32, y: f32| Pos2::new(r.left() + x * r.width(), r.top() + y * r.height());
match status {
Status::Done => {
p.line_segment([at(0.05, 0.55), at(0.4, 0.88)], Stroke::new(1.8, t.ok));
p.line_segment([at(0.4, 0.88), at(0.95, 0.15)], Stroke::new(1.8, t.ok));
}
Status::Next => {
p.line_segment([at(0.0, 0.5), at(0.95, 0.5)], Stroke::new(1.6, t.accent));
p.line_segment([at(0.55, 0.15), at(0.95, 0.5)], Stroke::new(1.6, t.accent));
p.line_segment([at(0.55, 0.85), at(0.95, 0.5)], Stroke::new(1.6, t.accent));
}
Status::Optional => {
p.line_segment([at(0.15, 0.5), at(0.85, 0.5)], Stroke::new(1.4, t.text_dim));
}
Status::Todo => {
p.circle_stroke(r.center(), r.width() * 0.4, Stroke::new(1.2, t.text_dim));
}
Status::Blocked => {
p.circle_stroke(r.center(), r.width() * 0.4, Stroke::new(1.2, t.text_dim));
p.line_segment([at(0.2, 0.8), at(0.8, 0.2)], Stroke::new(1.2, t.text_dim));
}
}
}
-1
View File
@@ -559,7 +559,6 @@ fn nav_bar(app: &mut ModelApp, ui: &mut egui::Ui, rect: Rect, t: &Tokens) {
ui.checkbox(&mut app.ui.show_interactions, "Interactions");
ui.separator();
ui.checkbox(&mut app.ui.show_tree, "Browser");
ui.checkbox(&mut app.ui.show_setup, "Setup checklist");
ui.checkbox(&mut app.ui.show_timeline, "Timeline");
});
}