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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
KenM76andClaude Opus 5.5 committed 2026-10-30 14:28:05 -04:00
1 parent fa5f9f8c3b
commit bb4a8accdd
2 files changed
+41 -1

No files matched your search

+18 -1
View File
@@ -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.
+23
View File
@@ -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#"<field name="{name}" x="{x}" y="0mm" h="5.906mm" minW="20.32mm"><ui><checkButton shape="round"/></ui><caption reserve="-5.08mm"><value><text>{name}</text></value><para hAlign="center"/></caption><font typeface="Arial" size="10pt"/><items><text>{item}</text></items></field>"#
)
};
let body = format!(
r#"<subform name="body" layout="position" w="7.5in" h="1in"><exclGroup name="pick" x="0mm" y="0mm" w="40.64mm">{}{}</exclGroup></subform>"#,
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.