From 7d72f21da366fbb09fa9d02830b2c43d5c60f4fa Mon Sep 17 00:00:00 2001 From: alehel Date: Thu, 29 Oct 2026 12:21:35 +0100 Subject: [PATCH] fix(ui): the area and placement tools act only on their own presses, of the primary button The Sign area, marquee zoom and snapshot, crop and redaction boxes, and the stamps, act where nothing drawn over the page holds the press (the page router has no owner for it), rather than wherever the press lands on the page. Fill & Sign already asks the router (fill_grabs). Their drags start on the primary button and end with its release, however egui reported it: its drag stop, or the button found up. They started on any button, so a right-drag drew a signature rectangle and opened the signing dialog (and drew a crop or a marquee), and the end waited for a pointer position, so a drag whose release came without one never ended. New test a_right_drag_draws_no_signature_rectangle, seen failing with the drag started on any button. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/canvas.rs | 6 ++++-- crates/ui-egui/src/canvas/crop.rs | 9 ++++++--- crates/ui-egui/src/canvas/zoom_snap.rs | 9 ++++++--- crates/ui-egui/src/tools/redact_ui.rs | 3 ++- crates/ui-egui/src/tools/sign_ui.rs | 9 ++++++--- crates/ui-egui/tests/it/pointer.rs | 21 +++++++++++++++++++++ 6 files changed, 45 insertions(+), 12 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index d67b18b802..f2b5720190 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1145,7 +1145,8 @@ fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame< && let Some(p) = ptr.within(xf.rect) { view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Crosshair); - if resp.clicked() { + // The tool's own click: nothing drawn over the page here holds the press. + if resp.clicked() && view.router.owner(i).is_none() { // Centred on the click, upright as the page is shown. let (vx, vy) = xf.screen_to_view(p); let (w, h) = kind.size(); @@ -1169,7 +1170,8 @@ fn page_tool_input(ui: &mut egui::Ui, view: &mut DocView, frame: &mut PageFrame< && let Some(p) = ptr.within(xf.rect) { view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Crosshair); - if resp.clicked() { + // The tool's own click: nothing drawn over the page here holds the press. + if resp.clicked() && view.router.owner(i).is_none() { // Centred on the click at its natural size (the engine sizes it). let (vx, vy) = xf.screen_to_view(p); let u = info.pages[i].view_to_user(vx, vy); diff --git a/crates/ui-egui/src/canvas/crop.rs b/crates/ui-egui/src/canvas/crop.rs index dd88db1a19..bdd1e9e135 100644 --- a/crates/ui-egui/src/canvas/crop.rs +++ b/crates/ui-egui/src/canvas/crop.rs @@ -38,15 +38,18 @@ pub(crate) fn page_input(ui: &egui::Ui, input: CropPageInput<'_>) -> bool { if !allowed { return false; } - if resp.drag_started() + // The tool's own press, of the primary button: nothing drawn over the page here holds it. + if resp.drag_started_by(egui::PointerButton::Primary) + && view.router.owner(page).is_none() && let Some(o) = origin.filter(|o| xf.rect.contains(*o)) { view.crop_drag = Some((page, o)); } if let Some((p, start)) = view.crop_drag && p == page - && let Some(end) = pointer { + // Where the pointer is, or where it went down when it is off the window. + let end = pointer.unwrap_or(start); let end = Pos2::new(end.x.clamp(xf.rect.left(), xf.rect.right()), end.y.clamp(xf.rect.top(), xf.rect.bottom())); let r = Rect::from_two_pos(start, end); // Dim what will be cut away. @@ -60,7 +63,7 @@ pub(crate) fn page_input(ui: &egui::Ui, input: CropPageInput<'_>) -> bool { painter.rect_filled(band, CornerRadius::ZERO, Color32::from_black_alpha(70)); } painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(1.0, Color32::from_rgb(0x14, 0x73, 0xE6)), egui::StrokeKind::Middle); - if resp.drag_stopped() { + if resp.drag_stopped() || !ptr.primary_down { view.crop_drag = None; if r.width() >= 4.0 && r.height() >= 4.0 { let (a, b) = (to_user(xf, info, page, r.left_top()), to_user(xf, info, page, r.right_bottom())); diff --git a/crates/ui-egui/src/canvas/zoom_snap.rs b/crates/ui-egui/src/canvas/zoom_snap.rs index 8e530ac58e..5a0dcf6733 100644 --- a/crates/ui-egui/src/canvas/zoom_snap.rs +++ b/crates/ui-egui/src/canvas/zoom_snap.rs @@ -19,19 +19,22 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, xf: &PageXform, p if pointer.is_some_and(|p| xf.rect.contains(p)) { view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Crosshair); } - if resp.drag_started() + // The tool's own press, of the primary button: nothing drawn over the page here holds it. + if resp.drag_started_by(egui::PointerButton::Primary) + && view.router.owner(page).is_none() && let Some(o) = origin.filter(|o| xf.rect.contains(*o)) { view.marquee = Some((page, o)); } if let Some((p, start)) = view.marquee && p == page - && let Some(end) = pointer { + // Where the pointer is, or where it went down when it is off the window. + let end = pointer.unwrap_or(start); let end = Pos2::new(end.x.clamp(xf.rect.left(), xf.rect.right()), end.y.clamp(xf.rect.top(), xf.rect.bottom())); let r = Rect::from_two_pos(start, end); ui.painter().rect_stroke(r, CornerRadius::ZERO, Stroke::new(1.0, Color32::from_rgb(0x14, 0x73, 0xE6)), egui::StrokeKind::Middle); - if resp.drag_stopped() { + if resp.drag_stopped() || !ptr.primary_down { view.marquee = None; let (a, b) = (xf.screen_to_view(r.left_top()), xf.screen_to_view(r.right_bottom())); view.marquee_done = Some((page, r, [a.0.min(b.0), a.1.min(b.1), a.0.max(b.0), a.1.max(b.1)])); diff --git a/crates/ui-egui/src/tools/redact_ui.rs b/crates/ui-egui/src/tools/redact_ui.rs index e5c2ac06e5..96b544b4d0 100644 --- a/crates/ui-egui/src/tools/redact_ui.rs +++ b/crates/ui-egui/src/tools/redact_ui.rs @@ -256,7 +256,8 @@ pub(crate) fn page_input(cx: PageToolContext<'_>, over_text: impl Fn(Pos2) -> bo if !over_text(p) { view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Crosshair); } - if resp.drag_started() { + // The tool's own press, of the primary button: nothing drawn over the page here holds it. + if resp.drag_started_by(egui::PointerButton::Primary) && view.router.owner(page).is_none() { let origin = ptr.origin.unwrap_or(p); if !over_text(origin) { view.redact_drag = Some((page, origin)); diff --git a/crates/ui-egui/src/tools/sign_ui.rs b/crates/ui-egui/src/tools/sign_ui.rs index 921f9a738d..b00d07279a 100644 --- a/crates/ui-egui/src/tools/sign_ui.rs +++ b/crates/ui-egui/src/tools/sign_ui.rs @@ -176,20 +176,23 @@ pub(crate) fn page_input( if pointer.is_some_and(|p| xf.rect.contains(p)) { view.cursor.claim(Layer::ToolArea, egui::CursorIcon::Crosshair); } - if resp.drag_started() + // The tool's own press, of the primary button: nothing drawn over the page here holds it. + if resp.drag_started_by(egui::PointerButton::Primary) + && view.router.owner(page).is_none() && let Some(o) = origin.filter(|o| xf.rect.contains(*o)) { view.sign.drag = Some((page, o)); } if let Some((p, start)) = view.sign.drag && p == page - && let Some(end) = pointer { + // Where the pointer is, or where it went down when it is off the window. + let end = pointer.unwrap_or(start); let end = Pos2::new(end.x.clamp(xf.rect.left(), xf.rect.right()), end.y.clamp(xf.rect.top(), xf.rect.bottom())); let r = Rect::from_two_pos(start, end); ui.painter().rect_filled(r, CornerRadius::ZERO, Color32::from_rgba_unmultiplied(0x14, 0x73, 0xE6, 28)); ui.painter().rect_stroke(r, CornerRadius::ZERO, Stroke::new(1.0, Color32::from_rgb(0x14, 0x73, 0xE6)), egui::StrokeKind::Middle); - if resp.drag_stopped() { + if resp.drag_stopped() || !ptr.primary_down { view.sign.drag = None; if r.width() >= 8.0 && r.height() >= 8.0 { let user = |q: Pos2| { diff --git a/crates/ui-egui/tests/it/pointer.rs b/crates/ui-egui/tests/it/pointer.rs index fbd3d720e4..4a28943b11 100644 --- a/crates/ui-egui/tests/it/pointer.rs +++ b/crates/ui-egui/tests/it/pointer.rs @@ -744,6 +744,27 @@ fn escape_cancels_a_signature_rectangle() { assert!(s.dialog.is_none(), "and no signing dialog opens: {:?}", s.dialog); } +/// Only the primary button draws a signature rectangle: a right-drag opens no signing dialog. +#[test] +fn a_right_drag_draws_no_signature_rectangle() { + let mut h = open(link_pdf(), "150", false); + h.state_mut().quick_tool = QuickTool::SignArea { certify: false }; + h.run_steps(4); + let (a, b) = (at(&h, 60.0, 200.0), at(&h, 200.0, 120.0)); + let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Secondary, pressed, modifiers: egui::Modifiers::NONE }; + h.hover_at(a); + h.run_steps(1); + h.event(button(a, true)); + h.run_steps(1); + h.hover_at(b); + h.run_steps(2); + h.event(button(b, false)); + h.run_steps(4); + let s = h.state(); + assert_eq!(s.quick_tool, QuickTool::SignArea { certify: false }, "the tool stays: no rectangle was drawn"); + assert!(s.dialog.is_none(), "and no signing dialog opens: {:?}", s.dialog); +} + /// Escape with the button down drops the press: its release places no stamp, and the next click /// places one. #[test]