diff --git a/crates/ui-egui/src/canvas/page_input.rs b/crates/ui-egui/src/canvas/page_input.rs index 5ca60f8684..5d6dcb8054 100644 --- a/crates/ui-egui/src/canvas/page_input.rs +++ b/crates/ui-egui/src/canvas/page_input.rs @@ -98,8 +98,7 @@ pub(super) fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut let at = hits.at(view, o); view.router.press(i, at, tool); } - // A right-click goes to what a press there would go to: the top hit under the pointer. - let context = pointer.filter(|p| resp.secondary_clicked() && xf.rect.contains(*p)).and_then(|p| top(hits.at(view, p), tool)).map(|h| h.owner); + let context = pointer.filter(|p| resp.secondary_clicked() && xf.rect.contains(*p)).and_then(|p| right_click(&hits, view, p)); if let QuickTool::Measure(measure_tool) = tool { crate::measure_ui::page_input(ui, resp, doc, view, &pcx, measure_tool, units); } @@ -303,6 +302,27 @@ fn link_of(owner: Option) -> Option { } } +/// A right-click at `p`: it goes to what lies on top there ([`PageHits::context_at`]), which the +/// page's menu then acts on. A comment there is selected, and none elsewhere; a Prepare field is +/// 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(); + if !editing_here && hits.field_tool.is_none() { + let comment = match context { + Some(Owner::Comment { index, .. }) => Some(index), + _ => None, + }; + comments::context(hits.pcx, p, comment, view); + } + if let Some(Owner::PrepareField { field, widget }) = context { + crate::prepare::context(hits.form, field, widget, view); + } + context +} + /// Edit text's paragraphs and pictures on page `i`, kept while the document is unchanged. fn edit_inventory( view: &mut DocView, @@ -348,12 +368,25 @@ struct PageHits<'a> { } impl PageHits<'_> { + /// The hits at `p`, bottom up ([`Self::hits`]). + fn at(&self, view: &DocView, p: Pos2) -> Vec { + self.hits(view, p, false) + } + + /// 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`]). + fn context_at(&self, view: &DocView, p: Pos2) -> Option { + top(self.hits(view, p, true), QuickTool::Select).map(|h| h.owner) + } + /// The hits at `p`, bottom up: links, comments; Edit text's pictures under its paragraphs (text /// on top of artwork takes the press), added content, the objects selected together and the /// open paragraph's frame, the selected picture's grips over them all; then form fields /// (Prepare's, being moved, over the comments; filled ones under added content and the /// comments that move). - fn at(&self, view: &DocView, p: Pos2) -> Vec { + /// For a right-click (`context`), comments by [`comments::context_hit`]. + fn hits(&self, view: &DocView, p: Pos2, context: bool) -> Vec { let PageHits { page, xf, @@ -415,7 +448,7 @@ impl PageHits<'_> { let picks_up = matches!(tool, QuickTool::Select | QuickTool::AddText | QuickTool::EditText); let added = (editing_content && can_modify && picks_up).then(|| crate::content_ui::hit(xf, info, page, added, view, p)).flatten(); let link = crate::link_ui::hit(xf, info, page, doc_links, tool, p); - let comment = comments::hit(pcx, view, p, link.is_some()); + let comment = if context { comments::context_hit(pcx, p) } else { comments::hit(pcx, view, p, link.is_some()) }; [link, comment, pictures, paragraphs, added, objects, grips, fields].into_iter().flatten().collect() } } diff --git a/crates/ui-egui/src/canvas/page_pointer/tests.rs b/crates/ui-egui/src/canvas/page_pointer/tests.rs index 4f9398be85..f9fc72bbcd 100644 --- a/crates/ui-egui/src/canvas/page_pointer/tests.rs +++ b/crates/ui-egui/src/canvas/page_pointer/tests.rs @@ -417,7 +417,11 @@ fn hit_test_calls(line: &str) -> Vec { /// The functions that call a hit test: (file, function, why). Only `PageHits::at` asks at the /// press point; the others ask where the pointer is. const HIT_TESTERS: [(&str, &str, &str); 4] = [ - ("canvas/page_input.rs", "at", "`PageHits::at`: every module's hits at a point, for the pointer's cursor and tip and, once, the press's owner"), + ( + "canvas/page_input.rs", + "hits", + "`PageHits::hits`: every module's hits at a point, for the pointer's cursor and tip, a right-click and, once, the press's owner", + ), ("canvas/page_input.rs", "page_overlays", "a link's click counts only released over the link its press was on"), ("tools/forms_ui.rs", "page_input", "the field's tooltip under the pointer, and a click counts only released over the widget its press was on"), ("tools/comments/input.rs", "grabs", "with no button down, whether a comment would take a press under the pointer, for hover painting"), diff --git a/crates/ui-egui/src/tools/comments.rs b/crates/ui-egui/src/tools/comments.rs index cb8bafe642..d0c46d0453 100644 --- a/crates/ui-egui/src/tools/comments.rs +++ b/crates/ui-egui/src/tools/comments.rs @@ -37,7 +37,7 @@ pub(crate) use geometry::{corner_ratio, page_rotation, resized}; pub use geometry::{drawn_text_box_rect, text_box_rect}; #[cfg(test)] use input::hover_tip; -pub(crate) use input::{describe, fill_grabs, hit, keys, nudge_keys, page_after_text, page_input}; +pub(crate) use input::{context, context_hit, describe, fill_grabs, hit, keys, nudge_keys, page_after_text, page_input}; pub use menu::{CanvasAction, fill_picker, recent_custom_colours, remember_custom_colour, status_badge, swatch_grid}; #[cfg(test)] use menu::{RECENT_CUSTOM, same_colour}; diff --git a/crates/ui-egui/src/tools/comments/input.rs b/crates/ui-egui/src/tools/comments/input.rs index 527df65d01..a12d971f5c 100644 --- a/crates/ui-egui/src/tools/comments/input.rs +++ b/crates/ui-egui/src/tools/comments/input.rs @@ -27,12 +27,6 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>, if cv.cancelled_drag { return true; } - if resp.secondary_clicked() - && let Some(p) = pointer.filter(|p| page_rect.contains(*p)) - { - cv.context_at = Some((cx.page, cx.to_user(p))); - cv.selected = cx.hit(p).map(|a| (cx.page, a.index)); - } let gestures = Gestures { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page }; match cx.tool { QuickTool::Comment(_) if draw_grab => select_input(ui, resp, cx, view, pointer, origin), @@ -106,6 +100,25 @@ pub(crate) fn hit(cx: &PageCx<'_>, view: &DocView, p: Pos2, on_link: bool) -> Op Some(Hit { declines_drag: markup, ..Hit::new(Owner::Comment { index: a.index, part: CommentPart::Body }, layer, icon) }) } +/// The comment a right-click at `p` is on: the topmost, with every tool, as a comment's menu shows +/// whatever tool is active. A highlight over a link is the highlight's here (a left click on it +/// follows the link, [`hit`]). `None` off the page or with comments hidden. +pub(crate) fn context_hit(cx: &PageCx<'_>, p: Pos2) -> Option { + if cx.hidden || !cx.xf.rect.contains(p) { + return None; + } + let a = cx.hit(p)?; + let layer = if a.fill_sign || !is_markup(&a.subtype) { Layer::Placed } else { Layer::Comment }; + Some(Hit::new(Owner::Comment { index: a.index, part: CommentPart::Body }, layer, egui::CursorIcon::Default)) +} + +/// A right-click at `p` on this page: the page's menu is anchored there (Copy Image), and the +/// comment it is on ([`context_hit`], when it wins there) is selected, or none. +pub(crate) fn context(cx: &PageCx<'_>, p: Pos2, index: Option, view: &mut DocView) { + view.comments.context_at = Some((cx.page, cx.to_user(p))); + view.comments.selected = index.map(|i| (cx.page, i)); +} + /// Whether the comments take the press on this page (or, with no button down, would take one /// under the pointer), so the tools beneath leave it: a live move, resize or reshape, or a press /// held by a comment that keeps its drag. diff --git a/crates/ui-egui/src/tools/prepare.rs b/crates/ui-egui/src/tools/prepare.rs index cc4b81441e..78568f46db 100644 --- a/crates/ui-egui/src/tools/prepare.rs +++ b/crates/ui-egui/src/tools/prepare.rs @@ -398,24 +398,6 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], tool: Opti return out; } - // Right-click a field: select it (the context menu acts on it). - if resp.secondary_clicked() - && let Some((f, wi)) = form.iter().find_map(|f| { - f.widgets.iter().enumerate().find(|(_, w)| w.page == Some(page) && screen_rect(xf, info, page, w.rect).contains(p)).map(|(i, _)| (f, i)) - }) - { - let key = (f.name.clone(), wi); - if let Some(i) = prep.also.iter().position(|k| *k == key) { - // Part of the selection: it becomes the anchor the others line up with. - prep.also.remove(i); - if let Some(old) = prep.selected.replace(key) { - prep.also.push(old); - } - } else if prep.selected.as_ref() != Some(&key) { - prep.selected = Some(key); - prep.also.clear(); - } - } // The Select tool: what the press was on ([`hit`]), a corner handle or a field; elsewhere a // drag draws a selection rectangle. A press held by something drawn over the fields (a // comment) is not Prepare's. @@ -469,6 +451,24 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, form: &[FormField], tool: Opti out } +/// 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. +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 let Some(i) = prep.also.iter().position(|k| *k == key) { + prep.also.remove(i); + if let Some(old) = prep.selected.replace(key) { + prep.also.push(old); + } + } else if prep.selected.as_ref() != Some(&key) { + prep.selected = Some(key); + prep.also.clear(); + } +} + /// Align, Center, Distribute and Set Fields to Same Size (Prepare a form, several fields /// selected). #[derive(Clone, Copy, Debug, PartialEq, Eq)] diff --git a/crates/ui-egui/tests/it/pointer.rs b/crates/ui-egui/tests/it/pointer.rs index 1ee9676ad2..c868dc8079 100644 --- a/crates/ui-egui/tests/it/pointer.rs +++ b/crates/ui-egui/tests/it/pointer.rs @@ -1089,3 +1089,43 @@ fn a_right_click_on_a_note_over_a_link_opens_the_notes_menu() { assert_eq!((v.links.context, v.comments.selected), (None, Some((0, 1))), "the note is selected, not the link"); assert!(h.query_by_label("Remove all links").is_none(), "the menu is the note's"); } + +/// [`link_pdf`], with a highlight drawn over the middle of its link. +fn highlight_over_link_pdf() -> Vec { + pdf(&[ + CATALOG, + 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 /Highlight /Rect [100 310 200 340] /QuadPoints [100 340 200 340 100 310 200 310] /C [1 1 0] /F 4 /T (Ana) >>", + ]) +} + +/// A click on a highlight over a link follows the link, but a right-click there is the +/// highlight's: it is selected and its menu opens. The link's menu used to open, with the +/// highlight selected underneath. +#[test] +fn a_right_click_on_a_highlight_over_a_link_opens_the_highlights_menu() { + let mut h = open(highlight_over_link_pdf(), "150", false); + 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.context, v.comments.selected), (None, Some((0, 1))), "the highlight is selected, not the link"); + assert!(h.query_by_label("Remove all links").is_none(), "the menu is the highlight's"); + h.key_press(egui::Key::Escape); + h.run_steps(3); + mouse_click(&mut h, on_both); + assert!(h.state().pending_link.is_some(), "a click there still follows the link"); +} + +/// A comment's menu opens on a right-click whatever the tool, though a press there is the tool's: +/// with the Rectangle tool, a right-click on a comment selects it. +#[test] +fn a_right_click_on_a_comment_with_a_drawing_tool_opens_its_menu() { + let mut h = open(comment_on_text_pdf(), "150", false); + h.state_mut().quick_tool = QuickTool::Comment(CommentTool::Rectangle); + h.run_steps(4); + let on_comment = at(&h, 100.0, 305.0); + right_click(&mut h, on_comment); + assert_eq!(h.state().views[0].comments.selected, Some((0, 0)), "the comment is selected for its menu"); +}