M6: a round XFA check box's field highlight is round, as a radio button's is (#4512)

This commit is contained in:
Eric Kaden
2026-10-25 10:00:27 -04:00
committed by GitHub
parent 32f51c25b8
commit 2b8de22b64
6 changed files with 58 additions and 8 deletions
+1 -1
View File
@@ -480,7 +480,7 @@ fn frame(doc: &Document, wd: &Dict, w: f64, h: f64) -> (String, f64) {
/// Whether `wd` is a check box or radio button PdfCraft laid out for a round XFA check button
/// (`/PCXfaRound true`, written by the XFA layout).
fn is_round(doc: &Document, wd: &Dict) -> bool {
pub(crate) fn is_round(doc: &Document, wd: &Dict) -> bool {
matches!(wd.get(b"PCXfaRound").map(|o| doc.resolve(o)).as_deref(), Some(Object::Bool(true)))
}
+1
View File
@@ -963,6 +963,7 @@ pub fn add_field(doc: &mut Document, page: usize, rect: [f64; 4], kind: &NewFiel
rotation: 0,
caption: None,
list: None,
round: true,
};
let ap = appearance::check_box_states(doc, &w, FieldKind::Radio, export, "/ZaDb 0 Tf 0 g");
doc.update_dict(widget, |d| d.set(b"AP".to_vec(), Object::Dict(ap)))?;
+4
View File
@@ -118,6 +118,9 @@ pub struct Widget {
pub caption: Option<String>,
/// List boxes: where the options are drawn, so a click can pick the one under the pointer.
pub list: Option<appearance::ListRows>,
/// Drawn round: a radio button, or a check box laid out for a round XFA check button
/// (`/PCXfaRound true`).
pub round: bool,
}
/// A terminal form field.
@@ -882,6 +885,7 @@ fn walk(
})
.flatten(),
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)),
})
})
.collect();
+4 -5
View File
@@ -3860,20 +3860,19 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
}
// Form-field highlight (Acrobat's "Highlight existing fields"); required fields get a
// red border. Radio buttons are round, and so is theirs (as in Acrobat). Only what can
// be filled is marked: a read-only widget (its field's, or a radio button's own) is
// left as it is drawn.
// red border. Radio buttons and round XFA check boxes are round, and so is theirs (as
// in Acrobat). Only what can be filled is marked: a read-only widget (its field's, or a
// radio button's own) is left as it is drawn.
if view.highlight_fields {
for f in form.iter() {
let required = f.has(pdfcraft_engine::field_flags::REQUIRED);
let round = f.kind == pdfcraft_engine::FormFieldKind::Radio;
for w in f.widgets.iter().filter(|w| w.page == Some(i) && !w.hidden && !w.read_only) {
let r = w.rect;
let sr = xf.user_rect(info, i, [r[0] as f32, r[1] as f32, r[2] as f32, r[3] as f32]);
let tint = Color32::from_rgba_unmultiplied(0x6E, 0x8E, 0xF5, 48);
let (width, color) =
if required { (2.0, Color32::from_rgb(0xE3, 0x22, 0x22)) } else { (1.0, Color32::from_rgb(0x6E, 0x8E, 0xF5)) };
if round {
if w.round {
let radius = sr.width().min(sr.height()) / 2.0;
painter.circle_filled(sr.center(), radius, tint);
painter.circle_stroke(sr.center(), radius - width / 2.0, Stroke::new(width, color));
+46
View File
@@ -1468,6 +1468,52 @@ fn required_radio_buttons_get_a_round_red_border() {
assert!(!stroke(at(50.3, 339.7)), "the widget's corner, outside the circle, is not: {:?}", at(50.3, 339.7));
}
/// Two required check boxes: the first laid out for a round XFA check button (`/PCXfaRound`), the
/// second an ordinary square one.
const ROUND_CHECKS: &[u8] = b"%PDF-1.7
1 0 obj << /Type /Catalog /Pages 2 0 R /AcroForm << /Fields [4 0 R 5 0 R] >> >> endobj
2 0 obj << /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 300 400] >> endobj
3 0 obj << /Type /Page /Parent 2 0 R /Annots [4 0 R 5 0 R] >> endobj
4 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /Ff 2 /T (round) /V /Off /AS /Off /Rect [50 300 90 340] /PCXfaRound true /P 3 0 R >> endobj
5 0 obj << /Type /Annot /Subtype /Widget /FT /Btn /Ff 2 /T (square) /V /Off /AS /Off /Rect [150 300 190 340] /P 3 0 R >> endobj
trailer << /Root 1 0 R >>
%%EOF";
/// With fields highlighted, a round XFA check box gets a round border, as a radio button does,
/// while an ordinary check box keeps a square one.
#[test]
fn round_check_boxes_get_a_round_highlight() {
let _gpu = gpu();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
let mut app = PdfCraftApp::new();
app.set_option("language", "en").unwrap();
app.open_bytes("checks.pdf", None, ROUND_CHECKS.to_vec()).expect("opens");
app.set_option("left", "closed").unwrap();
app.set_option("panel", "none").unwrap();
app.set_option("fields", "on").unwrap();
app
});
crate::support::settle(&mut h);
let r = rect(&h, 0).expect("page 1");
let ppp = h.ctx.pixels_per_point();
let img = h.render().expect("renders");
let at = |x: f32, y: f32| {
let p = egui::pos2(r.min.x + r.width() * (x / 300.0), r.min.y + r.height() * (1.0 - y / 400.0));
*img.get_pixel((p.x * ppp) as u32, (p.y * ppp) as u32)
};
// The red of the required border, or its antialiased edge over the field tint.
let stroke = |p: image::Rgba<u8>| p[0] > 180 && p[2] < 140 && i16::from(p[0]) - i16::from(p[2]) > 80;
assert!([0.05_f32, 0.2].into_iter().any(|dx| stroke(at(50.0 + dx, 320.0))), "the round box's left edge is red: {:?}", at(50.2, 320.0));
// Near the top of the left edge: outside the circle, on a square's border.
assert!(!stroke(at(50.25, 339.0)), "the round box's corner, outside the circle, is not: {:?}", at(50.25, 339.0));
assert!(
[0.1_f32, 0.25, 0.4].into_iter().any(|dx| stroke(at(150.0 + dx, 339.0))),
"the square box's is: {:?} {:?}",
at(150.1, 339.0),
at(150.25, 339.0)
);
}
/// Acrobat's Highlight existing fields is a preference, not a per-document choice: turned off
/// once, the next document (and the next session) opens with fields plain. New settings start
/// with it on.
+2 -2
View File
@@ -4463,8 +4463,8 @@ milestone = "M3"
status = "shipped"
commands = []
tools = []
evidence = ["crates/ui-egui/tests/it/view.rs::highlight_fields_tints_the_field_area", "crates/ui-egui/tests/it/view.rs::required_radio_buttons_get_a_round_red_border", "crates/ui-egui/tests/it/view.rs::field_highlighting_is_on_by_default_and_remembered", "crates/ui-egui/tests/it/view.rs::stored_field_highlight_choice_wins_over_the_default", "crates/ui-egui/tests/it/view.rs::highlight_fields_leaves_read_only_fields_alone", "crates/ui-egui/tests/it/editing.rs::a_secured_form_shows_its_form_notice_and_its_security"]
notes = "only fields that can be filled are marked (a read-only field is left as drawn, so a form's locked boxes don't look fillable); offered on secured forms too; on by default for new settings (a stored choice is kept) and remembered for the next document and session; toggled from the form bar and from Preferences > Appearance, where it sets the default for documents opened afterwards; required fields get a red border (round for radio buttons); the tint is drawn over the field, where Acrobat paints the highlight colour behind it"
evidence = ["crates/ui-egui/tests/it/view.rs::highlight_fields_tints_the_field_area", "crates/ui-egui/tests/it/view.rs::required_radio_buttons_get_a_round_red_border", "crates/ui-egui/tests/it/view.rs::field_highlighting_is_on_by_default_and_remembered", "crates/ui-egui/tests/it/view.rs::stored_field_highlight_choice_wins_over_the_default", "crates/ui-egui/tests/it/view.rs::highlight_fields_leaves_read_only_fields_alone", "crates/ui-egui/tests/it/editing.rs::a_secured_form_shows_its_form_notice_and_its_security", "crates/ui-egui/tests/it/view.rs::round_check_boxes_get_a_round_highlight"]
notes = "only fields that can be filled are marked (a read-only field is left as drawn, so a form's locked boxes don't look fillable); offered on secured forms too; on by default for new settings (a stored choice is kept) and remembered for the next document and session; toggled from the form bar and from Preferences > Appearance, where it sets the default for documents opened afterwards; required fields get a red border (the tint and border round for radio buttons and for check boxes laid out for a round XFA check button); the tint is drawn over the field, where Acrobat paints the highlight colour behind it"
[[feature]]
id = "form.field-appearance-display"