diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index b96364cb56..f843a2ca6c 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1444,7 +1444,8 @@ impl PageHits<'_> { }; // Added text and pictures, over the page's own paragraphs and under the objects selected // together. - let added = (editing_content && can_modify).then(|| crate::content_ui::hit(xf, info, page, added, view, p)).flatten(); + 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(); [link_hit(info, xf, page, p), comments::hit(pcx, view, p), pictures, paragraphs, added, objects, grips, fields] .into_iter() .flatten() @@ -1688,9 +1689,12 @@ fn page_overlays(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame<'_ } crate::image_pick::drop_stale(doc, view); crate::image_pick::paint(painter, &xf, i, info, view); - comments::page_after_text(resp, &pcx, view); - if tool == QuickTool::Redact && can_modify { - crate::redact_ui::after_text(resp, i, info, view); + // A cancelled press ends no selection: no markup or redaction is made from it. + if !view.press_cancelled { + comments::page_after_text(resp, &pcx, view); + if tool == QuickTool::Redact && can_modify { + crate::redact_ui::after_text(resp, i, info, view); + } } if input.area_tool { crate::redact_ui::paint(painter, i, view, pointer); @@ -1892,11 +1896,19 @@ fn link_hit(info: &DocInfo, xf: &PageXform, page: usize, p: Pos2) -> Option bool { match self { Layer::Link => matches!(tool, QuickTool::Select | QuickTool::Link), diff --git a/crates/ui-egui/src/tools/edit_text_ui.rs b/crates/ui-egui/src/tools/edit_text_ui.rs index 4ea80b1d6b..6ec31b0015 100644 --- a/crates/ui-egui/src/tools/edit_text_ui.rs +++ b/crates/ui-egui/src/tools/edit_text_ui.rs @@ -289,7 +289,8 @@ pub(crate) fn hit(xf: &PageXform, info: &DocInfo, page: usize, lines: &[pdfcraft return None; } let boxes: Vec = lines.iter().map(|l| paragraph_box(xf, info, page, l.rect)).collect(); - let (block, grip) = grab_at(&boxes, p, has_edges(info, page))?; + // Edges rewrap closed paragraphs only: around the open one lies its frame (`object_ui`). + let (block, grip) = grab_at(&boxes, p, has_edges(info, page) && active.is_none())?; let paragraph = |part| Owner::Paragraph { block, part }; Some(match (active, grip) { (None, Grip::Left) => Hit::new(paragraph(ParagraphPart::Left), Layer::Handle, egui::CursorIcon::ResizeHorizontal), diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index f247eb59b0..be5413b49c 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -4357,6 +4357,30 @@ fn an_open_paragraphs_frame_shows_move_wherever_a_press_takes_the_box() { assert!(h.state().views[0].line_editor.is_none(), "and a click there selects the box"); } +/// The open paragraph's frame takes hold of the box on its sides too: there the edges of a closed +/// paragraph would rewrap it, but around the open one lies its frame. +#[test] +fn an_open_paragraphs_frame_takes_the_box_beside_its_text() { + let mut h = harness(2, |_| {}); + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(2); + let id = h.state().views[0].id; + let before = h.state().session.get(id).unwrap().text_blocks(0)[0].clone(); + let r = h.state().views[0].page_screen_rect(0).expect("on screen"); + let k = r.width() / 200.0; + let screen = |x: f64, y: f64| egui::pos2(r.left() + x as f32 * k, r.top() + (300.0 - y as f32) * k); + let middle = (before.rect[1] + before.rect[3]) / 2.0; + click_at(&mut h, screen((before.rect[0] + before.rect[2]) / 2.0, middle)); + assert!(h.state().views[0].line_editor.is_some(), "a click on the text opens it for typing"); + // 6 px left of the text: 4 px past the frame drawn 2 px out. + let beside = screen(before.rect[0], middle) - egui::vec2(6.0, 0.0); + h.hover_at(beside); + h.run_steps(3); + assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::Move, "the frame offers to move the box there"); + click_at(&mut h, beside); + assert!(h.state().views[0].line_editor.is_none(), "and a click there selects the box"); +} + /// Contributor-original text, with an off-origin CropBox. Keep the moving paragraph last in /// source order so an ordinary hit picks it again when two independent boxes fully overlap. fn paragraph_drag_fixture(content: &str, rotation: i64, unit: i64) -> Vec { diff --git a/crates/ui-egui/tests/it/pointer.rs b/crates/ui-egui/tests/it/pointer.rs index 397d6316f8..0d058cb458 100644 --- a/crates/ui-egui/tests/it/pointer.rs +++ b/crates/ui-egui/tests/it/pointer.rs @@ -802,6 +802,57 @@ fn escape_cancels_a_signature_rectangle() { assert!(s.dialog.is_none(), "and no signing dialog opens: {:?}", s.dialog); } +/// A highlight dragged over the line of [`text_pdf`] at 150 %, from x 45 to x 150, with Escape +/// before the release when `escape`; the comments then on the page. +fn drag_a_highlight(escape: bool) -> usize { + let mut h = open(text_pdf(), "150", false); + h.state_mut().quick_tool = QuickTool::Comment(CommentTool::Highlight); + h.run_steps(4); + let (a, b) = (at(&h, 45.0, 303.0), at(&h, 150.0, 303.0)); + assert_eq!(wait_for_cursor(&mut h, a, |c| c == egui::CursorIcon::Text), egui::CursorIcon::Text, "the page's text has arrived"); + h.drag_at(a); + h.run_steps(1); + h.hover_at(b); + h.run_steps(2); + if escape { + h.key_press(egui::Key::Escape); + h.run_steps(1); + } + h.drop_at(b); + h.run_steps(4); + let s = h.state(); + s.session.get(s.views[0].id).unwrap().info.annotations.len() +} + +/// Escape mid-drag abandons a highlight being drawn over text: the release makes no highlight. +#[test] +fn escape_cancels_a_highlight_being_dragged_over_text() { + assert_eq!(drag_a_highlight(false), 1, "control: the drag highlights the text"); + assert_eq!(drag_a_highlight(true), 0, "with Escape before the release, no highlight is made"); +} + +/// A click on a comment, and in the frame of its release a press beside the page, then a drag +/// from there: the drag is not the comment's, so it does not move it. +#[test] +fn a_drag_from_beside_the_page_right_after_a_click_leaves_the_comment_alone() { + let mut h = open(comment_on_text_pdf(), "150", false); + let on_comment = at(&h, 120.0, 315.0); + let beside = page(&h).left_center() - egui::vec2(20.0, 0.0); + let before = square(&h).expect("the rectangle"); + h.hover_at(on_comment); + h.run_steps(1); + h.drag_at(on_comment); + h.run_steps(1); + let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + one_frame(&mut h, [button(on_comment, false), egui::Event::PointerMoved(beside), button(beside, true)]); + h.run_steps(1); + h.hover_at(beside + egui::vec2(0.0, 60.0)); + h.run_steps(2); + h.drop_at(beside + egui::vec2(0.0, 60.0)); + h.run_steps(4); + assert_eq!(square(&h), Some(before), "the comment stays where it was"); +} + /// Only the primary button draws a signature rectangle: a right-drag opens no signing dialog. #[test] fn a_right_drag_draws_no_signature_rectangle() {