diff --git a/crates/ui-egui/src/canvas/page_input.rs b/crates/ui-egui/src/canvas/page_input.rs index a2f71d97c8..7f5f9dd154 100644 --- a/crates/ui-egui/src/canvas/page_input.rs +++ b/crates/ui-egui/src/canvas/page_input.rs @@ -316,9 +316,13 @@ fn tool_input( /// selected; links take theirs from what this returns. fn right_click(hits: &PageHits<'_>, view: &mut DocView, p: Pos2) -> Option { let context = hits.context_at(view, p); - // Edit text's own paragraphs and pictures keep the right-click as they take it today, and a - // Prepare field tool the whole page. - let editing_here = matches!(hits.tool, QuickTool::AddText | QuickTool::EditText) && top(hits.at(view, p), hits.tool).is_some(); + // Add text takes every right-click on a page it can change, Edit text those on its own + // paragraphs and pictures, and a Prepare field tool the whole page, as before. + let editing_here = match hits.tool { + QuickTool::AddText => hits.can_modify || top(hits.at(view, p), hits.tool).is_some(), + QuickTool::EditText => top(hits.at(view, p), hits.tool).is_some(), + _ => false, + }; if !editing_here && hits.field_tool.is_none() { let comment = match context { Some(Owner::Comment { index, .. }) => Some(index), @@ -385,8 +389,16 @@ impl PageHits<'_> { /// What a right-click at `p` is on: what lies on top there, as for a press, but with every /// tool reaching comments and links, as a comment's menu shows whatever the tool. A highlight /// over a link is the highlight's ([`comments::context_hit`]). + /// The Link tool edits links, so its right-click on one is the link's whatever lies over it, as + /// its press there is. fn context_at(&self, view: &DocView, p: Pos2) -> Option { - top(self.hits(view, p, true), QuickTool::Select).map(|h| h.owner) + let hits = self.hits(view, p, true); + if self.tool == QuickTool::Link + && let Some(link) = hits.iter().rev().find(|h| matches!(h.owner, Owner::Link { .. })) + { + return Some(link.owner); + } + top(hits, QuickTool::Select).map(|h| h.owner) } /// The hits at `p`, bottom up: links, comments; Edit text's pictures under its paragraphs (text diff --git a/crates/ui-egui/src/tools/prepare.rs b/crates/ui-egui/src/tools/prepare.rs index ffd716c056..9c659352f9 100644 --- a/crates/ui-egui/src/tools/prepare.rs +++ b/crates/ui-egui/src/tools/prepare.rs @@ -462,11 +462,15 @@ pub(crate) fn page_input( /// A right-click on widget `widget` of field `field` (the top hit there, [`hit`]): the field is /// selected, so the menu acts on it. Part of a selection of several, it becomes the anchor the -/// others line up with. +/// others line up with. Not while a field is dragged or a rectangle drawn: the release acts on +/// the selection the drag began with. pub(crate) fn context(form: &[FormField], field: usize, widget: usize, view: &mut DocView) { let Some(f) = form.get(field) else { return }; let key = (f.name.clone(), widget); let prep = &mut view.prepare; + if prep.grab.is_some() || prep.ignore_release { + return; + } if let Some(i) = prep.also.iter().position(|k| *k == key) { prep.also.remove(i); if let Some(old) = prep.selected.replace(key) { diff --git a/crates/ui-egui/tests/it/pointer.rs b/crates/ui-egui/tests/it/pointer.rs index 6975a12a04..306f98accc 100644 --- a/crates/ui-egui/tests/it/pointer.rs +++ b/crates/ui-egui/tests/it/pointer.rs @@ -1226,3 +1226,61 @@ fn a_fields_tooltip_shows_only_where_the_field_takes_the_press() { rest_at(&mut h, on_field); assert!(h.query_all_by_label("Your name").next().is_some(), "the field's tooltip where it takes the press"); } + +/// With the Link tool a right-click on a link is the link's, as its press there is, though a note +/// lies over it: the Link tool edits links and reaches no comment. +#[test] +fn a_right_click_with_the_link_tool_on_a_note_over_a_link_selects_the_link() { + let mut h = open(note_over_link_pdf(), "150", false); + link_tool(&mut h); + let on_both = at(&h, 150.0, 325.0); + right_click(&mut h, on_both); + let v = &h.state().views[0]; + assert_eq!((v.links.selected, v.links.context, v.comments.selected), (Some((0, 0)), Some((0, 0)), None), "the link, not the note"); +} + +/// With Add text a right-click leaves comments alone, as before: Add text takes the page's +/// right-clicks. +#[test] +fn a_right_click_with_add_text_selects_no_comment() { + let mut h = open(comment_on_text_pdf(), "150", false); + h.state_mut().quick_tool = QuickTool::AddText; + h.run_steps(4); + // Above the line of text the comment covers, where no paragraph is. + let on_comment = at(&h, 100.0, 317.0); + right_click(&mut h, on_comment); + assert_eq!(h.state().views[0].comments.selected, None, "the comment under the pointer is not selected"); +} + +/// With the Select tool a right-click finds a link only where a click follows it: a link that +/// does nothing has no menu. The Link tool's own test used to give it one. +#[test] +fn a_right_click_with_the_select_tool_on_a_link_with_no_action_opens_no_link_menu() { + let mut h = open(dead_link_pdf(), "150", false); + let dead = at(&h, 25.0, 325.0); + right_click(&mut h, dead); + assert_eq!(h.state().views[0].links.context, None); + assert!(h.query_by_label("Remove all links").is_none(), "the page's menu has no link items"); +} + +/// [`link_pdf`], with a check box over the middle of its link. +fn field_over_link_pdf() -> Vec { + pdf(&[ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [5 0 R] /DA (/ZaDb 0 Tf 0 g) /DR << /Font << /ZaDb 6 0 R >> >> >> >>", + PAGES, + "<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >>", + "<< /Type /Annot /Subtype /Link /Rect [50 300 250 350] /Border [0 0 0] /A << /S /URI /URI (https://example.org/) >> >>", + "<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [140 315 160 335] /P 3 0 R /F 4 /MK << /CA (4) >> /DA (/ZaDb 11 Tf 0 g) >>", + "<< /Type /Font /Subtype /Type1 /BaseFont /ZapfDingbats >>", + ]) +} + +/// A right-click on a form field over a link is the field's, as a press there is: no link menu. +#[test] +fn a_right_click_on_a_field_over_a_link_opens_no_link_menu() { + let mut h = open(field_over_link_pdf(), "150", false); + let on_field = at(&h, 150.0, 325.0); + right_click(&mut h, on_field); + assert_eq!(h.state().views[0].links.context, None, "the field is on top"); + assert!(h.query_by_label("Remove all links").is_none()); +} diff --git a/crates/ui-egui/tests/it/prepare.rs b/crates/ui-egui/tests/it/prepare.rs index 82e33b29de..281e3d9e18 100644 --- a/crates/ui-egui/tests/it/prepare.rs +++ b/crates/ui-egui/tests/it/prepare.rs @@ -1562,3 +1562,40 @@ fn button_export_properties_apply_cancel_and_undo() { assert_eq!(f.export_of(0), Some(original.as_str())); } } + +/// The screen rectangle of widget 0 of field `name` on the first view. +fn field_rect(h: &Harness<'static, PdfCraftApp>, name: &str) -> egui::Rect { + let s = h.state(); + let doc = s.session.get(s.views[0].id).unwrap(); + let f = doc.form.iter().find(|f| f.name == name).unwrap(); + pdfcraft_ui_egui::forms_ui::field_screen_rect(&s.views[0], &doc.info, f, 0).expect("on screen") +} + +/// A right-click while a field is dragged leaves the dragged field selected, so the drag moves it +/// and not the field right-clicked. The right-click used to select the other field, and the release +/// moved that one. +#[test] +fn a_right_click_during_a_field_drag_moves_the_dragged_field() { + let mut h = harness(); + h.state_mut().execute("form.prepare"); + h.run_steps(3); + let (name, city) = (field_rect(&h, "name"), field_rect(&h, "city")); + let a = name.center(); + h.hover_at(a); + h.run_steps(1); + h.drag_at(a); + h.run_steps(1); + h.hover_at(a + egui::vec2(0.0, 30.0)); + h.run_steps(1); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos: city.center(), button: egui::PointerButton::Secondary, pressed, modifiers: Default::default() }); + } + h.run_steps(1); + let end = a + egui::vec2(0.0, 60.0); + h.hover_at(end); + h.run_steps(1); + h.drop_at(end); + h.run_steps(3); + assert_eq!(field_rect(&h, "city"), city, "the field right-clicked stays where it was"); + assert_ne!(field_rect(&h, "name"), name, "the dragged field moved"); +}