//! Delete (the Delete and Backspace keys, and every menu's Delete item): whatever is selected in //! the viewport or the browser goes in one undoable step through `selection.delete`. When it //! takes other features with it or makes some fail, a confirmation lists them first. use egui::{RichText, vec2}; use serde_json::{Value, json}; use crate::SolveApp; use crate::theme::Tokens; /// A delete waiting for confirmation: its parameters and the dry run's report. #[derive(Clone, Debug)] pub struct Pending { pub params: Value, pub report: Value, } /// What Delete acts on now: the selection (bodies not locked), plus the components picked in /// the browser (their occurrences). pub fn selection_params(app: &SolveApp) -> Value { let items: Vec<&solvecraft_engine::Sel> = app.session.selection.iter().filter(|s| !matches!(s, solvecraft_engine::Sel::Body { name } if app.ui.locked_bodies.contains(name))).collect(); let occurrences: Vec = app.tree.picked_components.iter().filter_map(|c| app.session.doc.occurrence_of(*c).map(|o| o.id)).collect(); json!({ "items": items, "occurrences": occurrences, "canvases": app.tree.picked_canvases }) } fn is_empty(p: &Value) -> bool { ["items", "occurrences", "components", "canvases"].iter().all(|k| p.get(*k).and_then(Value::as_array).is_none_or(|a| a.is_empty())) } /// Delete what is selected (the Delete key). A selected sketch dimension or constraint glyph /// goes first, as before. pub fn delete_selection(app: &mut SolveApp) { if crate::sketch3d::delete_selected(app) || crate::sketch_tools::delete_glyph(app) { return; } request(app, selection_params(app)); } /// Delete `params` (as `selection.delete` takes them): at once, or after a confirmation when /// other features go with it or would fail. pub fn request(app: &mut SolveApp, params: Value) { if is_empty(¶ms) { return; } let mut dry = params.clone(); dry["dry_run"] = json!(true); let report = match app.session.execute("selection.delete", &dry) { Ok(r) => r, Err(e) => { app.set_status(e.to_string().replace("selection.delete: ", ""), true); return; } }; let asked = params.get("items").and_then(Value::as_array).map_or(0, |a| a.iter().filter(|i| i["type"] == "feature" || i["type"] == "plane").count()); let extra = report["deleted"].as_array().map_or(0, Vec::len) > asked; let fails = report["would_fail"].as_array().is_some_and(|a| !a.is_empty()); if extra || fails { app.menu.confirm = Some(Pending { params, report }); } else { run(app, ¶ms); } } fn run(app: &mut SolveApp, params: &Value) { match app.run("selection.delete", params.clone()) { Ok(r) => { app.tree.picked_components.clear(); app.tree.picked_canvases.clear(); if let Some(s) = r["skipped"].as_array().and_then(|a| a.first()).and_then(Value::as_str) { app.set_status(format!("deleted; {s}"), false); } } Err(e) => app.set_status(e.replace("selection.delete: ", ""), true), } } /// Answer the confirmation (also `ui.confirm`): delete, or keep everything. pub fn confirm(app: &mut SolveApp, accept: bool) -> bool { let Some(p) = app.menu.confirm.take() else { return false }; if accept { run(app, &p.params); } true } /// The confirmation window. pub fn show(app: &mut SolveApp, ctx: &egui::Context) { let Some(p) = app.menu.confirm.clone() else { return }; let t = Tokens::get(); let list = |k: &str| -> Vec { p.report[k].as_array().map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_string)).collect()).unwrap_or_default() }; let (deleted, fails) = (list("deleted"), list("would_fail")); let mut answer: Option = None; crate::frame::window(ctx, "Delete", crate::frame::Width::Normal) .id(egui::Id::new("sc_delete_confirm")) .collapsible(false) .resizable(false) .anchor(egui::Align2::CENTER_CENTER, vec2(0.0, -60.0)) .show(ctx, |ui| { ui.set_max_width(360.0); if !deleted.is_empty() { ui.label("These features will be deleted:"); for d in deleted.iter().take(20) { ui.label(RichText::new(format!(" • {d}")).color(t.text)); } if deleted.len() > 20 { ui.label(format!(" … and {} more", deleted.len() - 20)); } } if !fails.is_empty() { ui.add_space(6.0); ui.label(RichText::new("These features will fail:").color(t.warning)); for f in fails.iter().take(20) { ui.label(RichText::new(format!(" • {f}")).color(t.warning)); } } ui.add_space(8.0); ui.horizontal(|ui| { if ui.button("Delete").clicked() { answer = Some(true); } if ui.button("Cancel").clicked() { answer = Some(false); } }); if ui.input(|i| i.key_pressed(egui::Key::Escape)) { answer = Some(false); } }); if let Some(a) = answer { confirm(app, a); } }