mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-01 05:58:12 -06:00
M12: an XFA label or button without a width is as wide as its text
A draw or a button with no width took all the width available, so a right-aligned label slid to the far side of its subform and a button spread over what lay beside it: DA Form 31's Instructions button covered its header and the email line, and its PERSONNEL/FINANCE line ran into the field after it. They now grow to their text, unwrapped, within what is available and never below their minimum width, as containers do.
This commit is contained in:
@@ -912,7 +912,8 @@ impl Layouter<'_> {
|
||||
let ctx = ctx.with_som(c, 0);
|
||||
let som = ctx.som.clone();
|
||||
// A container without a width is as wide as its content (a radio group beside its
|
||||
// question, say); a leaf without one takes what is available.
|
||||
// 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.
|
||||
let w = match c.w {
|
||||
Some(w) => w,
|
||||
None => match node {
|
||||
@@ -920,7 +921,13 @@ impl Layouter<'_> {
|
||||
let inner = (avail_w - c.margin.horizontal()).max(1.0);
|
||||
(self.content_width(node, inner, &ctx, depth) + c.margin.horizontal()).min(avail_w)
|
||||
}
|
||||
Node::Field(_) | Node::Draw(_) => avail_w,
|
||||
Node::Draw(d) => self.natural_width(&d.value, &ctx.font, &ctx.para).map_or(avail_w, |t| (t + c.margin.horizontal()).min(avail_w)),
|
||||
Node::Field(f) if f.ui == Ui::Button => f
|
||||
.caption
|
||||
.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(_) => avail_w,
|
||||
},
|
||||
};
|
||||
let w = clamp_opt(w, c.min_w, c.max_w);
|
||||
@@ -1004,6 +1011,17 @@ impl Layouter<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
fn natural_width(&self, v: &Value, font: &Font, para: &Para) -> Option<f64> {
|
||||
let rich = match v {
|
||||
Value::Text(t) => text::plain(t),
|
||||
Value::Rich(r) => r.clone(),
|
||||
_ => return None,
|
||||
};
|
||||
Some(text::natural_width(&rich, para, &Face::of(font), &|id| self.embed(id)) + 0.01)
|
||||
}
|
||||
|
||||
fn value_height(&self, v: &Value, w: f64, ctx: &Ctx) -> f64 {
|
||||
match v {
|
||||
Value::Text(t) => text::measure_height(&text::plain(t), w, &ctx.para, &Face::of(&ctx.font), &|id| self.embed(id)),
|
||||
|
||||
@@ -1195,6 +1195,63 @@ fn repeating_subforms_inside_positioned_content_get_their_instances() {
|
||||
/// treated like a subform (at most once), so once the first body page was out the layout fell
|
||||
/// back to the last page area, here one limited to a single use, and drew its content over every
|
||||
/// body page (and listed a copy of its fields per page).
|
||||
/// A positioned subform holding `body`, laid out; its page's items.
|
||||
fn positioned(body: &str) -> Vec<Item> {
|
||||
let xdp = format!(
|
||||
r##"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
|
||||
<subform name="form" layout="tb">
|
||||
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
|
||||
<subform name="box" w="7.5in" h="3in">{body}</subform>
|
||||
</subform>
|
||||
</template></xdp:xdp>"##
|
||||
);
|
||||
layout_xml(&xdp).unwrap().pages.into_iter().next().unwrap().items
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_label_without_a_width_is_as_wide_as_its_text() {
|
||||
const TEXT: &str = "PERSONNEL ONLY: from date (YYYYMMDD):";
|
||||
let natural = pdfcraft_fonts::helvetica_width(TEXT, 8.0);
|
||||
// Right-aligned, with a minimum narrower than the text: the box is as wide as the text and
|
||||
// starts where it is placed, so the text does too (not at the far right of the subform).
|
||||
let items = positioned(&format!(
|
||||
r#"<draw name="a" x="1in" y="0" minW="50pt"><value><text>{TEXT}</text></value><font typeface="Arial" size="8pt"/><para hAlign="right"/></draw>"#
|
||||
));
|
||||
let span = items.iter().find_map(|i| if let Item::Text(s) = i { Some(s) } else { None }).expect("the text");
|
||||
assert!((span.x - (36.0 + 72.0)).abs() < 0.05, "starts at its x: {}", span.x);
|
||||
// With a minimum wider than the text, the box is that wide and the text ends at its right.
|
||||
let items = positioned(&format!(
|
||||
r#"<draw name="a" x="1in" y="0" minW="{}pt"><value><text>{TEXT}</text></value><font typeface="Arial" size="8pt"/><para hAlign="right"/></draw>"#,
|
||||
natural + 40.0
|
||||
));
|
||||
let span = items.iter().find_map(|i| if let Item::Text(s) = i { Some(s) } else { None }).expect("the text");
|
||||
assert!((span.x + span.face.width(&span.text) - (36.0 + 72.0 + natural + 40.0)).abs() < 0.05, "ends at its right: {}", span.x);
|
||||
// Text longer than the room left still wraps at the container's width.
|
||||
let long = "word ".repeat(200);
|
||||
let items = positioned(&format!(r#"<draw name="a" x="0" y="0"><value><text>{long}</text></value><font typeface="Arial" size="8pt"/></draw>"#));
|
||||
let lines: std::collections::BTreeSet<i64> =
|
||||
items.iter().filter_map(|i| if let Item::Text(s) = i { Some((s.baseline * 100.0).round() as i64) } else { None }).collect();
|
||||
assert!(lines.len() > 1, "wraps: {} line(s)", lines.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_button_without_a_width_is_as_wide_as_its_caption() {
|
||||
// No width, a minimum narrower than its caption: as wide as the caption, not the subform.
|
||||
let caption = "Open the instructions for this form";
|
||||
let items = positioned(&format!(
|
||||
r#"<field name="help" x="1in" y="0" minW="40pt" minH="12pt"><ui><button/></ui><caption><value><text>{caption}</text></value><font typeface="Arial" size="8pt"/></caption></field>"#
|
||||
));
|
||||
let button = items.iter().find_map(|i| if let Item::Widget(w) = i { Some(w) } else { None }).expect("the button");
|
||||
let natural = pdfcraft_fonts::helvetica_width(caption, 8.0);
|
||||
assert!(button.rect.w >= natural && button.rect.w < natural + 10.0, "{} for a caption of {natural}", button.rect.w);
|
||||
// A minimum wider than the caption wins.
|
||||
let items = positioned(
|
||||
r#"<field name="help" x="1in" y="0" minW="200pt" minH="12pt"><ui><button/></ui><caption><value><text>Help</text></value><font typeface="Arial" size="8pt"/></caption></field>"#,
|
||||
);
|
||||
let button = items.iter().find_map(|i| if let Item::Widget(w) = i { Some(w) } else { None }).expect("the button");
|
||||
assert!((button.rect.w - 200.0).abs() < 0.05, "{}", button.rect.w);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_area_without_occur_serves_every_body_page() {
|
||||
let section = |n: u32| {
|
||||
|
||||
@@ -280,6 +280,14 @@ pub fn layout(rich: &RichText, rect: Rect, para: &Para, base: &Face, embed: &dyn
|
||||
Block { spans, height: total }
|
||||
}
|
||||
|
||||
/// The width `rich` takes with no line wrapped (its longest paragraph, with the paragraph's
|
||||
/// margins and first-line indent): how wide a box without an explicit width grows.
|
||||
pub fn natural_width(rich: &RichText, para: &Para, base: &Face, embed: &dyn Fn(&str) -> String) -> f64 {
|
||||
let indent = para.text_indent.max(0.0);
|
||||
let widest = rich.paragraphs.iter().map(|p| words(&p.runs, base, embed).iter().map(|w| w.width).sum::<f64>()).fold(0.0, f64::max);
|
||||
(para.margin_left + indent + widest + para.margin_right).max(0.0)
|
||||
}
|
||||
|
||||
/// Height `rich` needs at `width` (for boxes without an explicit height).
|
||||
pub fn measure_height(rich: &RichText, width: f64, para: &Para, base: &Face, embed: &dyn Fn(&str) -> String) -> f64 {
|
||||
let p = Para { v_align: crate::model::VAlign::Top, ..para.clone() };
|
||||
|
||||
Reference in New Issue
Block a user