From bb4a8accdd8df8cc8f4658a3e6fa3a68bde5106b Mon Sep 17 00:00:00 2001 From: KenM76 Date: Fri, 30 Oct 2026 14:28:05 -0400 Subject: [PATCH] M12: an XFA check box without a width is as wide as its box and caption A field with no `w` is growable in width (XFA 3.3 Ch.8, growable objects): it takes its content's width, clamped to minW/maxW. The layouter gave every non-button field without a width all the room left in its container, so a positioned radio group whose choices set only minW stretched the first choice to the group's right edge. Its box (drawn at the field's right end) then sat on the last choice's box, and the group showed one circle where it had two (#5442's Yes/No groups). A check button without a width is now its box and widget margins plus its caption beside it (the caption's reserve, else its text), or the wider of the two when the caption is above or below, within what is available and clamped by minW/maxW. Other fields keep taking the available width. Test: a_radio_choice_without_a_width_is_as_wide_as_its_content, two choices with minW and no w in a positioned exclGroup; with the change reverted the Yes box lands at 105.2pt, on top of the No box. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9 --- crates/xfa/src/layout/measure.rs | 19 ++++++++++++++++++- crates/xfa/src/tests.rs | 23 +++++++++++++++++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index 2fffb3dfa0..6532f6511e 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -51,7 +51,8 @@ impl Layouter<'_> { let som = ctx.som.clone(); // A container without a width is as wide as its content (a radio group beside its // question, say), and so is a text label or a button (as wide as its caption), within - // what is available; any other field without one takes what is available. + // what is available, and so is a check box (its box and caption); any other field without + // one takes what is available. let w = match c.w { Some(w) => w, None => match node { @@ -65,6 +66,7 @@ impl Layouter<'_> { .as_ref() .and_then(|cap| self.natural_width(&cap.value, &ctx.font.apply(&cap.font), &ctx.para.apply(&cap.para))) .map_or(avail_w, |t| (t + c.margin.horizontal() + f.ui_margin.horizontal()).min(avail_w)), + Node::Field(f) if f.ui == Ui::CheckButton => (self.check_width(f, &ctx) + c.margin.horizontal()).min(avail_w), Node::Field(_) => avail_w, }, }; @@ -149,6 +151,21 @@ impl Layouter<'_> { } } + /// How wide check box `f` is without a width of its own (XFA 3.3 Ch.8, growable fields): its + /// box and widget margins, plus its caption beside it (its reserve, else its text), or the + /// wider of the two when the caption is above or below. + fn check_width(&self, f: &Field, ctx: &Ctx) -> f64 { + let check = f.check_size.unwrap_or(10.0) + f.ui_margin.horizontal(); + let Some(cap) = f.caption.as_ref().filter(|c| c.presence.occupies() && c.placement != Placement::Inline) else { + return check; + }; + let text = || self.natural_width(&cap.value, &ctx.font.apply(&cap.font), &ctx.para.apply(&cap.para)).map_or(0.0, |t| t + 2.0); + match cap.placement { + Placement::Top | Placement::Bottom => check.max(text()), + _ => check + cap.reserve.unwrap_or_else(text), + } + } + /// How wide text `v` is unwrapped, in `font` and `para`, with a hair to spare so that laying /// it out at that width keeps it on its lines; `None` for what isn't text, or text that draws /// nothing. diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index e424be5441..ff7c43fdbc 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -2997,6 +2997,29 @@ fn a_radio_button_sits_where_a_check_box_would() { } } +/// A check box without a width is as wide as its box and caption, at least its minW, not the rest +/// of its group: two choices side by side in a positioned radio group keep their own boxes. +#[test] +fn a_radio_choice_without_a_width_is_as_wide_as_its_content() { + let choice = |name: &str, x: &str, item: &str| { + format!( + r#"{name}{item}"# + ) + }; + let body = format!( + r#"{}{}"#, + choice("Yes", "0mm", "Y"), + choice("No", "20.308mm", "N"), + ); + let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap(); + let laid = widgets(&form.pages[0]); + let x = |name: &str| laid.iter().find(|w| w.name == name).map(|w| w.rect.x - 36.0).unwrap_or_else(|| panic!("no radio button {name}")); + let mm = 72.0 / 25.4; + // Each box sits at the right end of its own 20.32mm-wide choice. + assert!((x("Yes") - (20.32 * mm - 10.0)).abs() < 0.01, "Yes box at {} pt", x("Yes")); + assert!((x("No") - ((20.308 + 20.32) * mm - 10.0)).abs() < 0.01, "No box at {} pt", x("No")); +} + /// A caption without a reserve leaves room for the box at the size it is drawn, a box larger than /// its field shrinking to fit; and a field without a height is as tall as that caption drawn in /// all the box leaves, not at some narrower width.