diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs
index 32250ab02f..7751012b8b 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,22 @@ 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;
+ };
+ // The 2pt matches `caption_reserve`'s room for a caption without a reserve.
+ 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 5fd5a88ac1..2e70a81472 100644
--- a/crates/xfa/src/tests.rs
+++ b/crates/xfa/src/tests.rs
@@ -2997,6 +2997,52 @@ 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;
+ // A negative reserve (as Designer writes) sizes the caption to its text, so content is
+ // narrower than minW and 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"));
+}
+
+/// Without a width or minW, a check box is exactly its caption's reserve plus its box, whether
+/// placed (its right-aligned box ends there) or flowed (two fit side by side in an lr-tb row).
+#[test]
+fn a_check_box_without_a_width_is_its_caption_and_box() {
+ let check = |name: &str, at: &str| {
+ format!(
+ r#"{name}10"#
+ )
+ };
+ let box_at = |body: String, name: &str| {
+ let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
+ widgets(&form.pages[0]).iter().find(|w| w.name == name).map(|w| (w.rect.x - 36.0, w.rect.y - 36.0)).unwrap()
+ };
+ let placed = format!(r#"{}"#, check("placed", r#"x="0in" y="0in""#));
+ let (x, _) = box_at(placed, "placed");
+ assert!((x - 36.0).abs() < 0.01, "the box ends the 46pt field, after the 36pt caption: {x}");
+
+ let flowed = format!(r#"{}{}"#, check("first", ""), check("second", ""));
+ let (x, y) = box_at(flowed, "second");
+ assert!((x - 82.0).abs() < 0.01 && y.abs() < 0.01, "the second check box follows the first on its row: ({x}, {y})");
+}
+
/// 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.