diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 25398b45..b053892d 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -71,6 +71,22 @@ pub fn ui_shortcut(app: &mut SlideApp, key: egui::Key, m: Mods) -> bool { true } +/// The palette's entries for the lowercase query `q`. An engine command is hidden when a UI command has the same label: Open…, Save As…, +/// Export… and Audio/Video from File… need a `path` the palette can't give, so the one entry left opens the dialog instead of failing. +fn palette_items(session: &deckcraft_engine::Session, q: &str) -> Vec<(String, String, Option<&'static str>)> { + let mut items: Vec<(String, String, Option<&'static str>)> = session + .commands() + .into_iter() + .filter(|c| c.enabled && !crate::UI_COMMANDS.iter().any(|u| u.1 == c.label)) + .map(|c| (c.id.to_string(), c.label.to_string(), c.shortcut)) + .chain(crate::UI_COMMANDS.iter().map(|c| (c.0.to_string(), c.1.to_string(), c.2))) + .filter(|(id, label, _)| q.is_empty() || label.to_lowercase().contains(q) || id.to_lowercase().contains(q)) + .collect(); + items.sort_by_key(|(_, l, _)| !l.to_lowercase().starts_with(q)); + items.truncate(60); + items +} + /// ⇧⌘P: search every command by name and run it. pub fn palette(app: &mut SlideApp, ctx: &egui::Context) { let Some((mut query, mut sel)) = app.palette.take() else { return }; @@ -85,17 +101,7 @@ pub fn palette(app: &mut SlideApp, ctx: &egui::Context) { let r = ui.add(egui::TextEdit::singleline(&mut query).hint_text("Search commands…").desired_width(f32::INFINITY).font(theme::font(15.0))); r.request_focus(); let q = query.to_lowercase(); - let mut items: Vec<(String, String, Option<&'static str>)> = app - .session - .commands() - .into_iter() - .filter(|c| c.enabled) - .map(|c| (c.id.to_string(), c.label.to_string(), c.shortcut)) - .chain(crate::UI_COMMANDS.iter().map(|c| (c.0.to_string(), c.1.to_string(), c.2))) - .filter(|(id, label, _)| q.is_empty() || label.to_lowercase().contains(&q) || id.to_lowercase().contains(&q)) - .collect(); - items.sort_by_key(|(_, l, _)| !l.to_lowercase().starts_with(&q)); - items.truncate(60); + let items = palette_items(&app.session, &q); let (down, up, enter, esc) = ui.input(|i| { ( i.key_pressed(egui::Key::ArrowDown), @@ -265,3 +271,25 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<(&'static str, &'static str)>)> { ("Help", vec![("About DeckCraft", "app.about"), ("Command Palette", "app.palette")]), ] } + +#[cfg(test)] +mod tests { + use super::palette_items; + + /// #79: the palette listed two "Export…" entries and the first (`file.export`) failed with "missing `path`". + #[test] + fn palette_has_one_export_entry_that_opens_the_dialog() { + let session = deckcraft_engine::Session::with_new(); + for query in ["exp", "export"] { + let exports: Vec<_> = palette_items(&session, query).into_iter().filter(|(_, label, _)| label == "Export…").collect(); + assert_eq!(exports.len(), 1, "query {query}"); + assert_eq!(exports[0].0, "app.exportDialog"); + } + // Open…, Save As…, Audio/Video from File… and Notes had the same duplicates: every UI command is the only entry with its label. + for (id, label, _, _) in crate::UI_COMMANDS { + let same: Vec<_> = palette_items(&session, &label.to_lowercase()).into_iter().filter(|(_, l, _)| l == label).collect(); + assert_eq!(same.len(), 1, "{label}"); + assert_eq!(same[0].0, *id, "{label}"); + } + } +}