From 80e290acd177e072ee72148e94fbdbdd86422378 Mon Sep 17 00:00:00 2001 From: alehel Date: Thu, 22 Oct 2026 13:21:48 +0200 Subject: [PATCH] fix(ui): a tap on a focused check box or radio button keeps its focus A click on a check box or radio button focuses it, so Tab, Space and the arrows reach it (#2223). The page hands the click to the field, and the field overlay toggles the box in the same frame unless the click landed elsewhere. That check read hover_pos, which is None on the frame egui reports a tap: egui-winit sends PointerGone with every touch release, and winit reports Windows pen input as touch. So a finger or pen tap counted as a click away. The focus closed and the toggle was dropped: a tap left a check box unticked and unfocused, and a tap on a radio button selected nothing. The check now uses the overlay's clicked_outside, which reads interact_pos, as the other field editors do. Tests: touch_tap_ticks_a_check_box runs again. tests/it/editor_taps.rs taps a focused check box and the other button of a focused radio group, each with a mouse click as the control. Each checks that the focus stays and the tap toggles, and for the radio group that the arrows still move within it. With the fix reverted, the three tap tests fail and the controls pass. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/forms_ui.rs | 3 +- crates/ui-egui/tests/it/editor_taps.rs | 73 +++++++++++++++++++++++++- crates/ui-egui/tests/it/pointer.rs | 1 - 3 files changed, 73 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/forms_ui.rs b/crates/ui-egui/src/forms_ui.rs index 601061af0..8caa44ced 100644 --- a/crates/ui-egui/src/forms_ui.rs +++ b/crates/ui-egui/src/forms_ui.rs @@ -873,8 +873,7 @@ pub(crate) fn overlay( None } }); - let clicked_away = ctx.input(|i| i.pointer.any_click() && !i.pointer.hover_pos().is_some_and(|p| rect.contains(p))); - if esc || clicked_away { + if esc || clicked_outside() { close = true; } else if tab { next = Some(!shift); diff --git a/crates/ui-egui/tests/it/editor_taps.rs b/crates/ui-egui/tests/it/editor_taps.rs index 68d0a7893..2f3569661 100644 --- a/crates/ui-egui/tests/it/editor_taps.rs +++ b/crates/ui-egui/tests/it/editor_taps.rs @@ -86,10 +86,15 @@ fn mouse_click(h: &mut Harness<'_, PdfCraftApp>, p: egui::Pos2) { /// Where field `name`'s widget is on screen. fn field(h: &Harness<'_, PdfCraftApp>, name: &str) -> egui::Rect { + widget(h, name, 0) +} + +/// Where widget `w` of field `name` is on screen. +fn widget(h: &Harness<'_, PdfCraftApp>, name: &str, w: usize) -> egui::Rect { let s = h.state(); let doc = s.session.get(s.views[0].id).unwrap(); let f = doc.form.iter().find(|f| f.name == name).unwrap(); - pdfcraft_ui_egui::forms_ui::field_screen_rect(&s.views[0], &doc.info, f, 0).expect("on screen") + pdfcraft_ui_egui::forms_ui::field_screen_rect(&s.views[0], &doc.info, f, w).expect("on screen") } fn value(h: &Harness<'_, PdfCraftApp>, name: &str) -> Vec { @@ -228,6 +233,72 @@ fn touch_tap_on_a_date_fields_calendar_keeps_it_open() { assert_eq!(value(&h, "due"), [picked], "and a tap on a day picks it"); } +/// A 300×400 page with a check box `agree` and a pair of radio buttons `pick`, A and then B to +/// its right, each 20 pt square. A click focuses them, so the keys reach them (#2223). +fn toggles_pdf() -> Vec { + pdf(&[ + "<< /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R] >> >>", + PAGES, + "<< /Type /Page /Parent 2 0 R /Annots [4 0 R 6 0 R 7 0 R] >>", + "<< /Type /Annot /Subtype /Widget /FT /Btn /T (agree) /V /Off /AS /Off /Rect [40 300 60 320] /P 3 0 R /F 4 /AP << /N << /Yes 8 0 R /Off 8 0 R >> >> >>", + "<< /FT /Btn /T (pick) /Ff 32768 /V /Off /Kids [6 0 R 7 0 R] >>", + "<< /Type /Annot /Subtype /Widget /Parent 5 0 R /AS /Off /Rect [40 240 60 260] /P 3 0 R /F 4 /AP << /N << /A 8 0 R /Off 8 0 R >> >> >>", + "<< /Type /Annot /Subtype /Widget /Parent 5 0 R /AS /Off /Rect [100 240 120 260] /P 3 0 R /F 4 /AP << /N << /B 8 0 R /Off 8 0 R >> >> >>", + "<< /Type /XObject /Subtype /Form /BBox [0 0 20 20] /Length 0 >>\nstream\n\nendstream", + ]) +} + +#[test] +fn mouse_click_on_a_focused_check_box_keeps_its_focus() { + // Control for the tap below. + let mut h = open(toggles_pdf()); + let p = field(&h, "agree").center(); + mouse_click(&mut h, p); + assert_eq!((value(&h, "agree"), focus(&h).as_deref()), (vec!["Yes".to_string()], Some("agree")), "a click ticks and focuses the box"); + mouse_click(&mut h, p); + assert_eq!(focus(&h).as_deref(), Some("agree"), "a click on the focused box keeps its focus"); + assert!(value(&h, "agree").is_empty(), "and clears it"); +} + +#[test] +fn touch_tap_on_a_focused_check_box_keeps_its_focus() { + let mut h = open(toggles_pdf()); + let p = field(&h, "agree").center(); + mouse_click(&mut h, p); + assert_eq!(focus(&h).as_deref(), Some("agree"), "a click focuses the box"); + touch_tap(&mut h, p); + assert_eq!(focus(&h).as_deref(), Some("agree"), "a tap on the focused box keeps its focus, as a click does"); + assert!(value(&h, "agree").is_empty(), "and clears it"); +} + +#[test] +fn mouse_click_on_a_radio_button_keeps_the_group_focused() { + // Control for the tap below. + let mut h = open(toggles_pdf()); + let (a, b) = (widget(&h, "pick", 0).center(), widget(&h, "pick", 1).center()); + mouse_click(&mut h, a); + assert_eq!((value(&h, "pick"), focus(&h).as_deref()), (vec!["A".to_string()], Some("pick")), "a click selects A and focuses the group"); + mouse_click(&mut h, b); + assert_eq!((value(&h, "pick"), focus(&h).as_deref()), (vec!["B".to_string()], Some("pick")), "a click on B selects it in the focused group"); + h.key_press(egui::Key::ArrowLeft); + h.run_steps(3); + assert_eq!(value(&h, "pick"), ["A"], "so the arrows move within the group"); +} + +#[test] +fn touch_tap_on_a_radio_button_keeps_the_group_focused() { + let mut h = open(toggles_pdf()); + let (a, b) = (widget(&h, "pick", 0).center(), widget(&h, "pick", 1).center()); + mouse_click(&mut h, a); + assert_eq!(focus(&h).as_deref(), Some("pick"), "a click focuses the group"); + touch_tap(&mut h, b); + assert_eq!(focus(&h).as_deref(), Some("pick"), "a tap on B keeps the group focused, as a click does"); + assert_eq!(value(&h, "pick"), ["B"], "and selects B"); + h.key_press(egui::Key::ArrowLeft); + h.run_steps(3); + assert_eq!(value(&h, "pick"), ["A"], "so the arrows move within the group"); +} + // --------------------------------------------------------------------------------------------- // Edit text: a click on the Format text panel keeps the paragraph open. These open the Line // spacing list: a control that changes the paragraph on the click gives it the keyboard back in diff --git a/crates/ui-egui/tests/it/pointer.rs b/crates/ui-egui/tests/it/pointer.rs index 73af2373c..9562353ac 100644 --- a/crates/ui-egui/tests/it/pointer.rs +++ b/crates/ui-egui/tests/it/pointer.rs @@ -482,7 +482,6 @@ fn mouse_click_ticks_a_check_box() { } #[test] -#[ignore = "the check box focus (#2223) counts a tap as a click away: fixed by a later commit"] fn touch_tap_ticks_a_check_box() { let mut h = open(checkbox_pdf(), "150", false); let p = at(&h, 110.0, 210.0);