forked from 1b-agent/fieldcraft-upstream
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 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
f5a84f5f74
commit
012119e2c2
8 files changed
+3
-239
No files matched your search
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"))),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]| {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
});
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user