diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index ca6cbba33..dd71ceb6b 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -269,6 +269,8 @@ pub enum Dialog { Link { url: String, text: String, + /// Focus and select the address when the dialog first opens. + focus: bool, /// "Place in This Document" instead of "Existing File or Web Page". place: bool, /// The document's places (`link.targets`), read when that tab is first shown. @@ -1008,7 +1010,9 @@ impl Dialog { valign: sp.valign, } } - "link" => Dialog::Link { url: "https://".into(), text: app.session.selected_text(), place: false, targets: None, pick: None }, + "link" => { + Dialog::Link { url: "https://".into(), text: app.session.selected_text(), place: false, targets: None, pick: None, focus: true } + } "bookmark" => Dialog::Bookmark { name: bookmark_at_caret(&app.session), by_location: false, hidden: false, deleted: false }, "wordCount" => Dialog::WordCount { stats: app.session.run("review.wordCount", &json!({})).unwrap_or_default() }, "zoom" => Dialog::Zoom { percent: (app.session.view.zoom * 100.0).round() }, @@ -2011,7 +2015,7 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { } ok || cancel } - Dialog::Link { url, text, place, targets, pick } => { + Dialog::Link { url, text, place, targets, pick, focus } => { // Word's "Link to:" choices; the places are read once, when that tab is first shown. ui.horizontal(|ui| { ui.label(tl!("Link to:")); @@ -2027,7 +2031,13 @@ fn body(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool { ui.end_row(); if !*place { ui.label(tl!("Address:")); - ui.text_edit_singleline(url); + let mut out = egui::TextEdit::singleline(url).id(egui::Id::new("link_address")).show(ui); + if std::mem::take(focus) { + out.response.request_focus(); + let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(url.chars().count())); + out.state.cursor.set_char_range(Some(all)); + out.state.store(ui.ctx(), out.response.id); + } ui.end_row(); } }); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 9e0f72cbb..42eafa268 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2981,6 +2981,45 @@ mod tests { assert!(a.session.doc.plain_text(wordcraft_doc::StoryRef::Body).is_empty(), "nothing was typed into the document"); } + /// #1610: opening a hyperlink must direct typing and paste into its address. + #[test] + fn hyperlink_shortcut_focuses_address_and_preserves_document() { + let ctx = egui::Context::default(); + let mut a = app(); + a.run("text.insert", json!({"text": "Keep this text"})).unwrap(); + a.run("select.text", json!({"text": "Keep this text"})).unwrap(); + let frame = |a: &mut WordApp, events: Vec| { + ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| { + a.logic(ui.ctx()); + a.ui(ui); + }) + .drop_without_applying_deltas(); + }; + for _ in 0..3 { + frame(&mut a, Vec::new()); + } + let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }; + for (event, expected) in [ + (egui::Event::Paste("https://example.com".into()), "https://example.com"), + (egui::Event::Text("https://example.org".into()), "https://example.org"), + ] { + frame(&mut a, vec![key(egui::Key::K, egui::Modifiers::COMMAND)]); + for _ in 0..2 { + frame(&mut a, Vec::new()); + } + assert!(ctx.memory(|m| m.has_focus(egui::Id::new("link_address"))), "Address has focus"); + frame(&mut a, vec![event]); + let Some(dialogs::Dialog::Link { url, text, .. }) = &a.dialog else { panic!("no Link dialog") }; + assert_eq!(url, expected, "input replaces the initial address"); + assert_eq!(text, "Keep this text"); + assert_eq!(a.session.doc.plain_text(wordcraft_doc::StoryRef::Body), "Keep this text"); + frame(&mut a, vec![key(egui::Key::Escape, egui::Modifiers::NONE)]); + frame(&mut a, Vec::new()); + assert!(a.dialog.is_none(), "Cancel closes the dialog"); + assert_eq!(a.session.selected_text(), "Keep this text", "Cancel preserves selection"); + } + } + /// Issue #139: Ctrl+wheel over the page didn't zoom. #[test] fn ctrl_wheel_over_canvas_zooms() {