From 8333c371e110d4dc7f7fe5548868a7f765f67fa5 Mon Sep 17 00:00:00 2001 From: Piyush Goel Date: Thu, 29 Oct 2026 16:20:29 +0530 Subject: [PATCH] feat(ui): support Alt+1..9 shortcuts for Quick Access Toolbar items --- crates/ui-egui/src/menus.rs | 18 ++++++++++++++ crates/ui-egui/src/ribbon/qat.rs | 41 +++++++++++++++++++++++--------- crates/ui-egui/src/tests.rs | 21 ++++++++++++++++ 3 files changed, 69 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index f78e2526..fb4c50fc 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -72,6 +72,24 @@ pub fn ui_shortcut(app: &mut SlideApp, key: egui::Key, m: Mods) -> bool { } (F, true, false, false) => "app.find", (H, true, false, false) => "app.replace", + (Num1 | Num2 | Num3 | Num4 | Num5 | Num6 | Num7 | Num8 | Num9, false, false, true) => { + if app.session.mac_keys || app.backstage.is_some() || app.session.active().is_some_and(|d| d.selection.text.is_some()) { + return false; + } + let idx = match key { + Num1 => 0, + Num2 => 1, + Num3 => 2, + Num4 => 3, + Num5 => 4, + Num6 => 5, + Num7 => 6, + Num8 => 7, + Num9 => 8, + _ => return false, + }; + return crate::qat::run_index(app, idx); + } _ => return false, }; let _ = app.run(id, json!({})); diff --git a/crates/ui-egui/src/ribbon/qat.rs b/crates/ui-egui/src/ribbon/qat.rs index a0b77dbd..7edde77a 100644 --- a/crates/ui-egui/src/ribbon/qat.rs +++ b/crates/ui-egui/src/ribbon/qat.rs @@ -101,26 +101,45 @@ fn run(app: &mut SlideApp, q: &QatItem) { } } -fn tip(q: &QatItem) -> String { +/// Whether a Quick Access Toolbar item can be run right now. +pub fn is_enabled(app: &SlideApp, id: &str) -> bool { + match id { + "app.home" => true, + "file.save" | "show.fromStart" | "review.spelling" | "app.find" => app.session.active().is_some(), + id => crate::ribbon::enabled(app, id), + } +} + +/// Runs the Quick Access Toolbar item at `index` (0-based) if it exists and is enabled. Returns `true` if run. +pub fn run_index(app: &mut SlideApp, index: usize) -> bool { + let Some(q) = app.ui.qat.get(index).cloned() else { return false }; + if !is_enabled(app, &q.id) { + return false; + } + run(app, &q); + true +} + +fn tip(q: &QatItem, index: usize, mac: bool) -> String { let sc = deckcraft_engine::find_command(&q.id).and_then(|c| c.shortcut).or_else(|| crate::UI_COMMANDS.iter().find(|c| c.0 == q.id).and_then(|c| c.2)); - match sc { - Some(s) => format!("{} ({})", q.label, crate::ribbon::pretty_shortcut(s)), - None => q.label.clone(), + let qat_sc = if !mac && index < 9 { Some(format!("Alt+{}", index + 1)) } else { None }; + match (sc, qat_sc) { + (Some(s), Some(qsc)) => format!("{} ({}, {})", q.label, crate::ribbon::pretty_shortcut(s), qsc), + (Some(s), None) => format!("{} ({})", q.label, crate::ribbon::pretty_shortcut(s)), + (None, Some(qsc)) => format!("{} ({})", q.label, qsc), + (None, None) => q.label.clone(), } } /// Draw the toolbar and its Customize menu into a left-to-right row of the title bar. pub fn show(app: &mut SlideApp, ui: &mut Ui) { let items = app.ui.qat.clone(); - for q in &items { + let mac = app.session.mac_keys; + for (i, q) in items.iter().enumerate() { let on = q.id == "app.home" && app.backstage_open(); - let en = match q.id.as_str() { - "app.home" => true, - "file.save" | "show.fromStart" | "review.spelling" | "app.find" => app.session.active().is_some(), - id => crate::ribbon::enabled(app, id), - }; - let r = crate::widgets::icon_toggle(ui, q.icon(), &tip(q), en, on); + let en = is_enabled(app, &q.id); + let r = crate::widgets::icon_toggle(ui, q.icon(), &tip(q, i, mac), en, on); if r.clicked() { run(app, q); } diff --git a/crates/ui-egui/src/tests.rs b/crates/ui-egui/src/tests.rs index 0672d8c2..98f3b85c 100644 --- a/crates/ui-egui/src/tests.rs +++ b/crates/ui-egui/src/tests.rs @@ -123,6 +123,27 @@ fn quick_access_toolbar_adds_removes_resets_and_persists() { assert_eq!(app.ui.qat.len(), 6); app.run("app.qat", json!({"reset": true})).unwrap(); assert_eq!(ids(app), ["app.home", "file.save", "app.exportDialog", "edit.undo", "edit.redo", "show.fromStart"]); + // Alt+1 runs position 0 (app.home) when on Windows/Linux, opening Backstage. + app.session.mac_keys = false; + assert!(!app.backstage_open()); + assert!(crate::menus::ui_shortcut(app, egui::Key::Num1, deckcraft_engine::Mods { shift: false, alt: true, cmd: false })); + assert!(app.backstage_open()); + // Disabled while Backstage is open. + assert!(!crate::menus::ui_shortcut(app, egui::Key::Num1, deckcraft_engine::Mods { shift: false, alt: true, cmd: false })); + app.close_backstage(); + // Alt+9 is unassigned when there are only 6 items. + assert!(!crate::menus::ui_shortcut(app, egui::Key::Num9, deckcraft_engine::Mods { shift: false, alt: true, cmd: false })); + // Disabled on macOS so Option+digit types characters. + app.session.mac_keys = true; + assert!(!crate::menus::ui_shortcut(app, egui::Key::Num1, deckcraft_engine::Mods { shift: false, alt: true, cmd: false })); + app.session.mac_keys = false; + // Disabled when text is being edited. + let shape_id = deckcraft_model::ShapeId(app.session.active().unwrap().selection.shapes.first().map(|s| s.0).unwrap_or(0)); + app.session.active_mut().unwrap().selection.text = Some(deckcraft_engine::TextSel { shape: shape_id, ..Default::default() }); + assert!(!crate::menus::ui_shortcut(app, egui::Key::Num1, deckcraft_engine::Mods { shift: false, alt: true, cmd: false })); + app.session.active_mut().unwrap().selection.text = None; + // Disabled QAT items (e.g. edit.undo when stack is empty) cannot run. + assert!(!crate::qat::run_index(app, 3)); // An older settings file without the toolbar gets the defaults. let old: UiState = serde_json::from_value(json!({"tab": "Home"})).unwrap(); assert_eq!(old.qat.len(), 6);