From be019fd615ce58b4bf4a9219d452ad229189dbb3 Mon Sep 17 00:00:00 2001 From: Isaac Levine Date: Sun, 11 Oct 2026 22:53:29 -0400 Subject: [PATCH] Slides: Ctrl+A in the slide pane selects every slide (#188) The UI remembers whether the last press landed in the thumbnail pane (the sorter always counts) and passes scope slides to edit.selectAll in that case. --- crates/ui-egui/src/lib.rs | 51 ++++++++++++++++++++++++++++++++++++ crates/ui-egui/src/menus.rs | 5 ++++ crates/ui-egui/src/thumbs.rs | 6 ++++- 3 files changed, 61 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index bef64a4f..87b4a37d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -209,6 +209,8 @@ pub struct SlideApp { pub(crate) notes_buf: (u64, usize, String), /// Thumbnail pane drag (slide index, current drop index). pub(crate) thumb_drag: Option<(usize, usize)>, + /// The slide thumbnail pane was the last thing clicked, so ⌘A selects slides, not shapes. + pub(crate) slides_focused: bool, } impl SlideApp { @@ -244,6 +246,7 @@ impl SlideApp { quit_confirmed: false, notes_buf: (0, usize::MAX, String::new()), thumb_drag: None, + slides_focused: false, } } @@ -365,6 +368,12 @@ impl SlideApp { Ok(Value::Null) } + /// Whether ⌘A should act on the slide list: always in the sorter, in Normal view once the + /// thumbnail pane has been clicked. + pub(crate) fn slides_list_focused(&self) -> bool { + self.ui.view == ViewMode::Sorter || (self.ui.view == ViewMode::Normal && self.slides_focused) + } + pub fn start_show(&mut self, from: usize, reading: bool) { self.media.stop_owned(media::Owner::Editor); if let Some(d) = self.session.active() { @@ -938,4 +947,46 @@ mod tests { app.run("app.home", json!({})).unwrap(); assert!(!app.home_open() && app.home.is_none(), "with nothing open the start screen shows anyway"); } + + /// #188: Ctrl/Cmd+A with the slide thumbnail pane focused selects every slide; with the + /// canvas focused it still selects shapes. + #[test] + fn select_all_follows_the_focused_pane() { + let mut harness = egui_kittest::Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + SlideApp::new(Session::with_new(), Services::default()), + ); + harness.run_steps(2); + for _ in 0..2 { + harness.state_mut().run("slide.new", json!({})).unwrap(); + } + harness.state_mut().run("slide.go", json!({"index": 0})).unwrap(); + harness.run_steps(3); + let click = |h: &mut egui_kittest::Harness<'_, SlideApp>, pos: egui::Pos2| { + h.event(egui::Event::PointerMoved(pos)); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }); + } + h.run_steps(2); + }; + // Press in the thumbnail pane, then Cmd+A. + let w = harness.state().ui.thumbs_width; + click(&mut harness, egui::pos2(w / 2.0, 300.0)); + assert!(harness.state().slides_focused); + harness.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + harness.run_steps(2); + let st = harness.state().session.active().unwrap(); + assert_eq!(st.selection.slides.len(), 3, "every slide is selected"); + + // A press on the canvas moves focus away: Cmd+A selects shapes again. + click(&mut harness, egui::pos2(700.0, 400.0)); + assert!(!harness.state().slides_focused); + harness.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A); + harness.run_steps(2); + let st = harness.state().session.active().unwrap(); + assert!(!st.selection.shapes.is_empty(), "canvas select all picks the shapes"); + } } diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 9349dc36..2bb080c9 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -10,6 +10,11 @@ use crate::theme::{self, Tokens}; /// Shortcuts the UI handles itself (views, show, zoom, palette, file dialogs). True when consumed. pub fn ui_shortcut(app: &mut SlideApp, key: egui::Key, m: Mods) -> bool { use egui::Key::*; + if key == A && m.cmd && !m.shift && !m.alt && app.slides_list_focused() { + // The engine can't see which pane has focus, so the UI says so through the scope param. + let _ = app.run("edit.selectAll", json!({"scope": "slides"})); + return true; + } let id = match (key, m.cmd, m.shift, m.alt) { (F5, _, false, _) => { app.start_show(0, false); diff --git a/crates/ui-egui/src/thumbs.rs b/crates/ui-egui/src/thumbs.rs index ebdc81d0..1dc24d1d 100644 --- a/crates/ui-egui/src/thumbs.rs +++ b/crates/ui-egui/src/thumbs.rs @@ -11,7 +11,7 @@ use crate::theme::{self, Tokens}; pub fn show(app: &mut SlideApp, ui: &mut Ui) { let t = Tokens::get(ui.ctx()); let in_master = app.session.active().is_some_and(|d| d.selection.target != Target::Slides); - egui::Panel::left("thumbs") + let panel = egui::Panel::left("thumbs") .resizable(true) .default_size(app.ui.thumbs_width) .size_range(150.0..=420.0) @@ -28,6 +28,10 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) { ui.add_space(20.0); }); }); + // Focus follows the last press: inside the pane keeps it, anywhere else (canvas, ribbon) drops it. + if let Some(pos) = ui.input(|i| if i.pointer.primary_pressed() { i.pointer.interact_pos() } else { None }) { + app.slides_focused = panel.response.rect.contains(pos); + } } fn list(app: &mut SlideApp, ui: &mut Ui) {