From 172952f6e098b77ec4337909b6666a960e2ef02f Mon Sep 17 00:00:00 2001 From: Zetenypapp Date: Sun, 18 Oct 2026 18:00:41 +0200 Subject: [PATCH] Find: "Find what:" has the keyboard when the dialog opens (#951) Ctrl+F and typing searches without clicking the field first, and the field keeps the keyboard after Enter finds a match. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/ui-egui/src/dialogs.rs | 13 +++++++- crates/ui-egui/src/find_dialog_tests.rs | 43 +++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 2 ++ 4 files changed, 58 insertions(+), 1 deletion(-) create mode 100644 crates/ui-egui/src/find_dialog_tests.rs diff --git a/ROADMAP.md b/ROADMAP.md index 933bc7d1..6f85b959 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-18 | Find (Ctrl+F): "Find what:" has the keyboard as soon as the dialog opens, and keeps it after Enter finds a match, so the search can be typed and changed without clicking the field (#951) | | 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort | | 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape | | 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) | diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 2393bd61..94b3f8d4 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -3196,7 +3196,18 @@ fn tabs_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { /// Home › Find / Replace: step through matches (slide text, table cells, notes) or replace them. fn find_dialog(app: &mut SlideApp, ui: &mut Ui, d: &mut Dialog) -> bool { let replace_mode = d.get("replace", "0") == "1"; - field(ui, d, "Find what:", "text", ""); + // "Find what:" has the keyboard when the dialog opens, so Ctrl+F and typing finds (#951). + let mut what = d.get("text", ""); + ui.horizontal(|ui| { + ui.add_sized(vec2(120.0, 20.0), egui::Label::new("Find what:")); + let r = ui.add(egui::TextEdit::singleline(&mut what).desired_width(180.0)); + // Enter finds the next match and leaves the keyboard here, so the search can be changed. + if !d.fields.contains_key("focused") || (r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))) { + r.request_focus(); + d.fields.insert("focused".into(), "1".into()); + } + }); + d.fields.insert("text".into(), what); if replace_mode { field(ui, d, "Replace with:", "with", ""); } diff --git a/crates/ui-egui/src/find_dialog_tests.rs b/crates/ui-egui/src/find_dialog_tests.rs new file mode 100644 index 00000000..25e71a28 --- /dev/null +++ b/crates/ui-egui/src/find_dialog_tests.rs @@ -0,0 +1,43 @@ +//! Home › Find (Ctrl+F): "Find what:" has the keyboard as soon as the dialog opens (#951). + +use deckcraft_engine::Session; +use egui_kittest::Harness; +use serde_json::json; + +use crate::{Services, SlideApp}; + +#[test] +fn ctrl_f_puts_the_caret_in_find_what() { + let mut s = Session::with_new(); + s.execute("slide.new", &json!({"layout": "blank"})).unwrap(); + s.execute("insert.textBox", &json!({"rect": [40, 40, 400, 80], "text": "Hello there"})).unwrap(); + s.execute("text.exit", &json!({})).unwrap(); + s.doc_mut().unwrap().selection.shapes.clear(); + let app = SlideApp::new(s, Services::default()); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state( + |ui, app: &mut SlideApp| { + app.logic(ui.ctx()); + app.ui(ui); + }, + app, + ); + h.run_steps(3); + h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::F); + h.run_steps(3); + assert_eq!(h.state().dialog.as_ref().map(|d| d.id.as_str()), Some("find")); + // Typing right away goes into the field, without clicking it first. + h.event(egui::Event::Text("there".into())); + h.run_steps(3); + let typed = h.state().dialog.as_ref().and_then(|d| d.fields.get("text").cloned()); + assert_eq!(typed.as_deref(), Some("there")); + // Enter finds it. + h.key_press(egui::Key::Enter); + h.run_steps(3); + let status = h.state().dialog.as_ref().and_then(|d| d.fields.get("status").cloned()); + assert!(status.as_deref().is_some_and(|m| m.starts_with("Match 1 of 1")), "Enter didn't find the text: {status:?}"); + // The keyboard stays in the field afterwards: more typing adds to it. + h.event(egui::Event::Text("!".into())); + h.run_steps(3); + let typed = h.state().dialog.as_ref().and_then(|d| d.fields.get("text").cloned()); + assert_eq!(typed.as_deref(), Some("there!")); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index eef9b1b3..6a64527b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2052,6 +2052,8 @@ mod compare_tests; #[cfg(test)] mod eyedropper_tests; #[cfg(test)] +mod find_dialog_tests; +#[cfg(test)] mod huge_deck_tests; #[cfg(test)] mod ime_tests;