diff --git a/apps/fieldcraft-web/src/web.rs b/apps/fieldcraft-web/src/web.rs index 8a061fd..4e816a2 100644 --- a/apps/fieldcraft-web/src/web.rs +++ b/apps/fieldcraft-web/src/web.rs @@ -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, } } diff --git a/apps/fieldcraft/src/main.rs b/apps/fieldcraft/src/main.rs index 9d1d8af..74e2f60 100644 --- a/apps/fieldcraft/src/main.rs +++ b/apps/fieldcraft/src/main.rs @@ -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"))), } } diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index a1cebd5..bb6ca3a 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -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| { diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index 4aba809..bf2ab00 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -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] diff --git a/crates/ui-egui/src/files.rs b/crates/ui-egui/src/files.rs index e11250d..071ae8a 100644 --- a/crates/ui-egui/src/files.rs +++ b/crates/ui-egui/src/files.rs @@ -42,8 +42,6 @@ pub struct Services { pub pick_save: Option, /// The recent-files list's file (JSON); `None` keeps the list for this run only. pub recent_path: Option, - /// The user's preferences file (JSON, [`Prefs`]); `None` keeps them for this run only. - pub prefs_path: Option, } 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, offered: Rc>>>) -> PickOpen { Box::new(move |f: &[Filter]| { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e76940f..67707b0 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -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>), /// The file opening in the background, and files waiting to open after it. pub loading: Option, @@ -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> { 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); diff --git a/crates/ui-egui/src/setup.rs b/crates/ui-egui/src/setup.rs deleted file mode 100644 index a596eba..0000000 --- a/crates/ui-egui/src/setup.rs +++ /dev/null @@ -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)); - } - } -} diff --git a/crates/ui-egui/src/viewport.rs b/crates/ui-egui/src/viewport.rs index 660477d..e8b8635 100644 --- a/crates/ui-egui/src/viewport.rs +++ b/crates/ui-egui/src/viewport.rs @@ -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"); }); }