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 <noreply@anthropic.com>
This commit is contained in:
alehelandClaude Opus 5.5 committed 2026-10-29 12:21:35 +01:00
1 parent 35d28a60cc
commit 7d72f21da3
6 files changed
+45 -12

No files matched your search

+4 -2
View File
@@ -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);
+6 -3
View File
@@ -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()));
+6 -3
View File
@@ -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)]));
+2 -1
View File
@@ -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));
+6 -3
View File
@@ -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| {
+21
View File
@@ -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]