From 0fba511e9778cdee725e7e78102cf2793022db6b Mon Sep 17 00:00:00 2001 From: FredApps <153173854+FredApps@users.noreply.github.com> Date: Wed, 11 Nov 2026 13:11:32 +0100 Subject: [PATCH] forms: a push button with only a mouse-down action takes the press Widget.mouse_down records an /AA /D action, so a push button acting only on mouse down (the LaTeX animate package's controls) is pressable and its press runs the action as Mouse Down through the widget trigger dispatch, instead of saying it has no action. Co-authored-by: Peter Yatsyshin --- crates/engine/src/js.rs | 1 + crates/forms/src/author/lifecycle.rs | 1 + crates/forms/src/lib.rs | 3 +++ crates/forms/src/read.rs | 1 + crates/ui-egui/src/canvas/sign_tab_tests.rs | 1 + crates/ui-egui/src/tools/forms_ui.rs | 8 +++++-- crates/ui-egui/tests/it/field_triggers.rs | 24 ++++++++++++++++++--- 7 files changed, 34 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/js.rs b/crates/engine/src/js.rs index a6fbea8abe..348052aff4 100644 --- a/crates/engine/src/js.rs +++ b/crates/engine/src/js.rs @@ -2025,6 +2025,7 @@ mod widget_budget_review_tests { rotation: 0, caption: None, mouse_up_script: None, + mouse_down: false, list: None, round: false, }; diff --git a/crates/forms/src/author/lifecycle.rs b/crates/forms/src/author/lifecycle.rs index 50d552ccdb..c079b40f13 100644 --- a/crates/forms/src/author/lifecycle.rs +++ b/crates/forms/src/author/lifecycle.rs @@ -173,6 +173,7 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel rotation: 0, caption: None, mouse_up_script: None, + mouse_down: false, list: None, round: true, }; diff --git a/crates/forms/src/lib.rs b/crates/forms/src/lib.rs index 96d6ea2a6e..0c71c4077f 100644 --- a/crates/forms/src/lib.rs +++ b/crates/forms/src/lib.rs @@ -140,6 +140,9 @@ pub struct Widget { pub caption: Option, /// Text widget JavaScript to run on a mouse-up click. pub mouse_up_script: Option, + /// The widget has a mouse-down action (`/AA /D`): a press runs it (the LaTeX `animate` + /// package's controls act on mouse down), so a push button with only that still takes a press. + pub mouse_down: bool, /// List boxes: where the options are drawn, so a click can pick the one under the pointer. pub list: Option, /// Drawn round: a radio button, or a check box laid out for a round XFA check button diff --git a/crates/forms/src/read.rs b/crates/forms/src/read.rs index 537237254a..49d7e91562 100644 --- a/crates/forms/src/read.rs +++ b/crates/forms/src/read.rs @@ -733,6 +733,7 @@ fn walk( } else { None }, + mouse_down: wd.get(b"AA").map(|a| doc.resolve(a)).as_deref().and_then(Object::as_dict).is_some_and(|aa| aa.contains(b"D")), list: (kind == FieldKind::List).then(|| appearance::list_rows_of(doc, inh.da.as_deref().unwrap_or("/Helv 0 Tf 0 g"), Some(d), wd)), round: kind == FieldKind::Radio || (kind == FieldKind::CheckBox && appearance::is_round(doc, wd)), }) diff --git a/crates/ui-egui/src/canvas/sign_tab_tests.rs b/crates/ui-egui/src/canvas/sign_tab_tests.rs index c2d5c20748..79789eddcf 100644 --- a/crates/ui-egui/src/canvas/sign_tab_tests.rs +++ b/crates/ui-egui/src/canvas/sign_tab_tests.rs @@ -58,6 +58,7 @@ fn field(name: &str, page: usize, rect: [f64; 4]) -> FormField { tooltip: None, widgets: vec![FormWidget { mouse_up_script: None, + mouse_down: false, obj: pdfcraft_cos::ObjRef::new(4, 0), page: Some(page), rect, diff --git a/crates/ui-egui/src/tools/forms_ui.rs b/crates/ui-egui/src/tools/forms_ui.rs index 94aafda3f7..6a5abcecf2 100644 --- a/crates/ui-egui/src/tools/forms_ui.rs +++ b/crates/ui-egui/src/tools/forms_ui.rs @@ -432,11 +432,13 @@ pub(crate) fn tab_under<'a>(tabs: &[(&'a str, SignTab)], p: egui::Pos2) -> Optio tabs.iter().rev().find(|(_, tab)| tab.rect.contains(p)).map(|(name, _)| *name) } -/// Whether `w`, a widget of `f`, takes a press: fillable, a push button with an action, or an +/// Whether `w`, a widget of `f`, takes a press: fillable, a push button with an action (a click +/// or mouse-down one), or an /// empty signature field (signed by clicking it, Use a certificate). A radio button of a group can /// be read-only on its own. fn usable(f: &FormField, w: &FormWidget, allowed: bool) -> bool { - let pressable = f.kind == FormFieldKind::PushButton && !f.read_only() && f.button.is_some(); + // A button acting only on mouse down (`/AA /D`) takes the press too, which runs it (#4751). + let pressable = f.kind == FormFieldKind::PushButton && !f.read_only() && (f.button.is_some() || w.mouse_down); allowed && !w.read_only && (fillable(f) || pressable || f.kind == FormFieldKind::Signature) } @@ -551,6 +553,8 @@ pub(crate) fn page_input( _ if f.read_only() || w.read_only => view.forms.notice = Some(FormNotice::ReadOnly(f.name.clone())), FormFieldKind::PushButton => match &f.button { Some(a) => view.forms.button = Some((f.name.clone(), pdfcraft_engine::js::field_object_id(f.obj), a.clone())), + // Its press already ran its mouse-down action. + None if w.mouse_down => {} None => view.forms.notice = Some(FormNotice::NoAction(f.name.clone())), }, FormFieldKind::Text if let Some(script) = &w.mouse_up_script => { diff --git a/crates/ui-egui/tests/it/field_triggers.rs b/crates/ui-egui/tests/it/field_triggers.rs index f648b7a445..d57397e72e 100644 --- a/crates/ui-egui/tests/it/field_triggers.rs +++ b/crates/ui-egui/tests/it/field_triggers.rs @@ -1,6 +1,7 @@ //! A form field widget's pointer scripts in the app (#269, ported from #344): the pointer //! moving onto and off a widget runs its Mouse Enter and Mouse Exit JavaScript, and a click on a -//! check box runs its Mouse Up JavaScript after the click stored the new value (#247). +//! check box runs its Mouse Up JavaScript after the click stored the new value (#247). A push +//! button with only a mouse-down action runs it when pressed (#4751). use egui_kittest::Harness; use pdfcraft_ui_egui::PdfCraftApp; @@ -9,15 +10,16 @@ use pdfcraft_ui_egui::PdfCraftApp; /// `strike` (a check box) shows or hides `note` from its Mouse Up script by its own new value. fn form() -> Vec { let objs = [ - "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R 6 0 R 8 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 7 0 R >> >> >> >>", + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R 6 0 R 8 0 R 10 0 R] /DA (/Helv 0 Tf 0 g) /DR << /Font << /Helv 7 0 R >> >> >> >>", "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >>", - "<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R 8 0 R] >>", + "<< /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R 6 0 R 8 0 R 10 0 R] >>", "<< /Type /Annot /Subtype /Widget /FT /Tx /T (log) /V () /Rect [20 350 280 370] /P 3 0 R /F 4 >>", "<< /Type /Annot /Subtype /Widget /FT /Tx /T (hot) /V () /Rect [20 250 280 290] /P 3 0 R /F 4 /AA << /E << /S /JavaScript /JS (getField\\('log'\\).value += 'in;';) >> /X << /S /JavaScript /JS (getField\\('log'\\).value += 'out;';) >> >> >>", "<< /Type /Annot /Subtype /Widget /FT /Btn /T (strike) /V /Off /AS /Off /Rect [20 300 50 330] /P 3 0 R /F 4 /MK << /CA (4) >> /AP << /N << /Yes 9 0 R /Off 9 0 R >> >> /A << /S /JavaScript /JS (getField\\('note'\\).display = event.target.value == 'Off' ? display.hidden : display.visible; getField\\('log'\\).value += 'up:' + event.target.value + ';';) >> >>", "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>", "<< /Type /Annot /Subtype /Widget /FT /Tx /T (note) /V (struck) /Rect [60 300 280 330] /P 3 0 R /F 6 >>", "<< /Type /XObject /Subtype /Form /BBox [0 0 30 30] /Length 0 >>\nstream\n\nendstream", + "<< /Type /Annot /Subtype /Widget /FT /Btn /Ff 65536 /T (step) /Rect [20 200 120 230] /P 3 0 R /F 4 /MK << /CA (Step) >> /AA << /D << /S /JavaScript /JS (getField\\('log'\\).value += event.name + ';';) >> >> >>", ]; let mut out = b"%PDF-1.7\n".to_vec(); let mut offsets = Vec::new(); @@ -102,3 +104,19 @@ fn a_check_box_mouse_up_script_sees_the_value_its_click_stored() { crate::support::wait_until(&mut h, "unchecked and its Mouse Up ran", |app| field(app, "log").value.join(",") == "up:Yes;up:Off;"); assert!(field(h.state(), "note").widgets[0].hidden, "hidden again"); } + +/// A push button with only a mouse-down action (`/AA /D`, as the LaTeX `animate` package's +/// controls have) takes the press and runs it as Mouse Down, once per click (#4751). +#[test] +fn a_push_button_with_only_a_mouse_down_action_runs_it_on_press() { + let mut h = harness(); + let step = centre(&h, "step"); + h.hover_at(step); + h.run_steps(1); + h.drag_at(step); + h.run_steps(1); + h.drop_at(step); + crate::support::wait_until(&mut h, "Mouse Down ran", |app| field(app, "log").value.join(",") == "Mouse Down;"); + h.run_steps(4); + assert_eq!(log(&h), "Mouse Down;", "the click runs nothing more"); +}