diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index bf0468d08..9b9559916 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -8,6 +8,43 @@ use crate::CadApp; /// The top-level menus, in order. pub const MENUS: &[&str] = &["File", "Edit", "View", "Insert", "Format", "Tools", "Draw", "Dimension", "Modify", "Window", "Help"]; +/// The reference menu tree (feature names, four spaces per level): the order every menu lists +/// its items in. The same file is `cargo xtask parity`'s oracle. +const REFERENCE_TREE: &str = include_str!("../../../xtask/data/menu-catalog.txt"); + +/// A menu of the reference tree: its label and its items, in order. +struct Reference { + label: &'static str, + children: Vec, +} + +/// The reference tree's top-level menus. +fn reference() -> &'static [Reference] { + static TREE: std::sync::OnceLock> = std::sync::OnceLock::new(); + TREE.get_or_init(|| { + // The menus being read, outermost first; a line closes every menu deeper than its indent. + let mut open = vec![Reference { label: "", children: Vec::new() }]; + let close = |open: &mut Vec| { + if let Some(done) = open.pop() + && let Some(parent) = open.last_mut() + { + parent.children.push(done); + } + }; + for line in REFERENCE_TREE.lines().filter(|l| !l.trim().is_empty()) { + let depth = (line.len() - line.trim_start_matches(' ').len()) / 4; + while open.len() > depth + 1 { + close(&mut open); + } + open.push(Reference { label: line.trim(), children: Vec::new() }); + } + while open.len() > 1 { + close(&mut open); + } + open.pop().map(|root| root.children).unwrap_or_default() + }) +} + /// File ▸ Open Recent: one `ui.recent.` item per recent file, then Clear Recent Files. pub const OPEN_RECENT: &str = "Open Recent"; const RECENT_ITEM: &str = "ui.recent."; @@ -993,6 +1030,70 @@ mod tests { } } +#[cfg(test)] +mod reference_order_tests { + use cadcraft_engine::Session; + + use super::{Entry, Reference, reference, tree}; + use crate::{CadApp, Services}; + + fn label(e: &Entry) -> &str { + match e { + Entry::Item { label, .. } | Entry::Sub { label, .. } => label, + } + } + + /// Every item the reference lists comes in the reference's order, at every level, and the + /// items the reference lacks follow them. + fn check(path: &str, entries: &[Entry], reference: &[Reference], failures: &mut Vec) { + let known = |e: &Entry| reference.iter().any(|r| r.label == label(e)); + let listed: Vec<&str> = entries.iter().filter(|e| known(e)).map(label).collect(); + let expected: Vec<&str> = reference.iter().map(|r| r.label).filter(|l| listed.contains(l)).collect(); + if listed != expected { + failures.push(format!("{path}: {listed:?} should be {expected:?}")); + } + if let (Some(unlisted), Some(last)) = (entries.iter().position(|e| !known(e)), entries.iter().rposition(known)) + && unlisted < last + { + failures.push(format!("{path}: {} comes before the reference's items", entries.get(unlisted).map_or("?", label))); + } + for e in entries { + if let Entry::Sub { label, children } = e + && let Some(r) = reference.iter().find(|r| r.label == *label) + { + check(&format!("{path} > {label}"), children, &r.children, failures); + } + } + } + + #[test] + fn menus_follow_the_reference_tree() { + let app = CadApp::new(Session::new(), Services::default()); + let mut failures = Vec::new(); + for (menu, entries) in tree(&app) { + let r = reference().iter().find(|r| r.label == menu).unwrap_or_else(|| panic!("{menu} is not in the reference tree")); + check(&menu, &entries, &r.children, &mut failures); + } + assert!(failures.is_empty(), "{}", failures.join("\n")); + } + + #[test] + fn reference_tree_is_nested_by_indentation() { + assert_eq!(reference().iter().map(|r| r.label).collect::>(), super::MENUS); + let draw = reference().iter().find(|r| r.label == "Draw").unwrap(); + let circle = draw.children.iter().find(|r| r.label == "Circle").unwrap(); + assert_eq!(circle.children.first().map(|r| r.label), Some("Center, Radius")); + } + + #[test] + fn open_recent_stays_after_open() { + let app = CadApp::new(Session::new(), Services::default()); + let file = tree(&app).into_iter().find(|(m, _)| m == "File").map(|(_, e)| e).unwrap_or_default(); + let open = file.iter().position(|e| matches!(e, Entry::Item { id, .. } if id == "open")).unwrap(); + assert_eq!(file.get(open + 1).map(label), Some(super::OPEN_RECENT)); + } +} + #[cfg(test)] mod shortcut_label_tests { use super::shortcut_label;