Files
pdfcraft/crates/xfa/src/layout.rs
T
11ffedcc98 XFA: run the forms' JavaScript: calculations, validation, rows, visibility (#60) (#230)
* M12: run XFA forms' JavaScript: calculations, validation, rows, visibility (#60)

Laid-out XFA forms had their fields but none of their behaviour: totals
didn't compute, "Add row" buttons did nothing, and subforms a check box
should reveal stayed hidden.

The js crate gains the XFA scripting object model as native boa objects
over a snapshot of the form tree: xfa.form navigation by name, SOM
resolution (absolute, relative with the containing-subform search,
descendants, indexes and wildcards), rawValue/presence/access, instance
managers (_name, addInstance, removeInstance, setInstances, count),
xfa.host (messageBox, resetData, print, setFocus, gotoURL), xfa.layout
page numbers, xfa.event, app, console and util, plus unqualified sibling
names as XFA resolves them. Scripts run with `this` bound to the node and
yield their completion value; everything they change or ask for comes
back as effects.

The xfa crate builds the live form tree (instances from the data, values,
presence and access with overrides) and the event table from the
template, keeps presence and access overrides on the AcroForm
(/PCXfaOverrides) so undo and reopening preserve them, adds and removes
row instances in the datasets packet through the existing merge
pipeline, and lays the form out again on demand.

The engine runs initialize and calculate scripts on open (an appended
revision, nothing dirty), exit, validate and calculate after a field
changes (a failing validate shows its message and keeps the value, as
Acrobat does), and click scripts for buttons through run_javascript, so
the app's buttons and the js_run tool work unchanged. Values a script
sets go to the fields and the data at once; rows and visibility changes
re-lay the form out. All of it is one undo step. JavaScript off turns it
off. FormCalc scripts are recognised and reported, not run.

Bounds: script length and nesting as before, loop and recursion limits,
at most 1000 instances added per run, 2000 scripts and 50 re-layouts per
event; a calculate script that adds rows, or a click script looping on
addInstance/setInstances, ends quickly with a bounded form (tested).

Checked on IRCC's IMM 5669: opens with its page-number scripts, the Print
button's xfa.host.print runs through js_run, fill and save keep the
original bytes.

* M12: Save and SaveAs named actions run the file commands (#60)

XFA forms' Save buttons are app.execMenuItem("SaveAs"), which the layout
turns into a SaveAs named action; the shell answered it with "the SaveAs
action isn't supported yet". Named Save and SaveAs now run file.save and
file.save_as, like Named Print opens the print dialog.

The buttons test covers Save against a file on disk (SaveAs opens the
native dialog, which a headless test must not).

* M12: bound XFA form scripts against hostile forms (#60)

Review fixes for the XFA scripting PR (#230):

- Effects: values, presence and access set again on the same object merge
  (last wins, behind order-dependent effects); 10k effects, 100 message boxes
  and 1000 console lines per script and per event, reported when hit. An
  event's data writes are kept in memory and written as one rewrite of the
  datasets packet; later writes in the same edit replace that stream in place.
- form_tree stops at 200k objects and says so; forms without scripts build no
  live form and run nothing.
- Only click scripts may print, save, open links or move the focus; others are
  noted in the console. Open-time messages and errors show right after open.
- Lower loop limits for initialize/calculate/validate, a time budget per event
  after which remaining scripts are skipped and reported, and a per-script time
  limit after which a runaway script is abandoned and the document's scripts
  turned off.
- What open-time scripts change is noted in the XFA warnings.
- A document's Named /Save no longer writes the file on click (SaveAs opens
  the Save As dialog, as a script's execMenuItem does).
- XFA click scripts are no longer written as /S /JavaScript actions: the
  button carries /PCXfaClick and runs by SOM path. Private keys documented.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:16:54 -04:00

1284 lines
53 KiB
Rust

//! The layout engine: places a template's subforms, draws and fields on pages (XFA 3.3 part 2,
//! "Layout"). Supports positioned, top-to-bottom, left-to-right and table/row layouts, page
//! masters with content areas, `breakBefore`/`breakAfter`, and overflow leaders (header rows
//! repeated after a page break). Repeating subforms get their initial instances.
//!
//! Output is a list of pages of resolution-independent items; `pdf` turns them into PDF.
use std::cell::{Cell, RefCell};
use std::collections::{HashMap, HashSet};
use std::rc::Rc;
use std::sync::Arc;
use crate::XfaError;
use crate::data::{DataNode, is_on, iso_to_pattern, som_to_path};
use crate::model::*;
use crate::text::{self, Block, Face, Span};
/// Most pages a form may lay out to.
pub const MAX_PAGES: usize = 500;
/// Most items (text spans, lines, widgets…) across all pages.
const MAX_ITEMS: usize = 400_000;
/// Deepest nesting followed.
const MAX_DEPTH: usize = 64;
/// Most size measurements of one layout. Measurements of containers are memoized, so a real
/// form stays far below this; it bounds the work a hostile template can cause.
const MAX_MEASURES: usize = 2_000_000;
/// Most initial instances of one repeating subform.
const MAX_INSTANCES: usize = 50;
/// Placeholder for the page count in text laid out before the count is known.
const PAGE_COUNT_MARK: char = '\u{E000}';
#[derive(Clone, Debug, PartialEq)]
pub enum Item {
Fill { rect: Rect, color: [f64; 3] },
Line { from: (f64, f64), to: (f64, f64), width: f64, color: [f64; 3], dashed: bool },
Text(Span),
Image { rect: Rect, data: Arc<Vec<u8>>, content_type: String },
Widget(Box<Widget>),
}
#[derive(Clone, Debug, PartialEq)]
pub enum WidgetKind {
Text,
/// A date field with its Acrobat format pattern (`yyyy-mm-dd`).
Date(String),
CheckBox {
on: String,
round: bool,
},
Radio {
group: String,
on: String,
round: bool,
},
Button {
caption: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BorderShape {
None,
Full,
Underline,
}
#[derive(Clone, Debug, PartialEq)]
pub struct WidgetBorder {
pub color: Option<[f64; 3]>,
pub fill: Option<[f64; 3]>,
pub shape: BorderShape,
pub width: f64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Action {
Reset,
Print,
SaveAs,
Url(String),
/// Any other click script: run by the XFA scripting engine.
Script(String),
}
/// An interactive field to create as an AcroForm widget.
#[derive(Clone, Debug, PartialEq)]
pub struct Widget {
pub rect: Rect,
pub kind: WidgetKind,
/// The PDF field name (unique in the form, no periods).
pub name: String,
/// The template's scripting object model path, for data round trips.
pub som: String,
pub tooltip: Option<String>,
pub face: Face,
pub h_align: HAlign,
pub multiline: bool,
pub max_chars: Option<usize>,
pub read_only: bool,
pub border: WidgetBorder,
pub value: Option<String>,
/// The template's default value (what a reset restores); data values are not defaults.
pub default: Option<String>,
pub action: Option<Action>,
/// Check boxes: the on and off values of the data; radio buttons: the on value.
pub items: Vec<String>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Page {
pub width: f64,
pub height: f64,
pub items: Vec<Item>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Form {
pub pages: Vec<Page>,
pub fields: usize,
pub warnings: Vec<String>,
}
/// Inherited text defaults and the SOM path of the enclosing containers.
#[derive(Clone)]
struct Ctx {
font: Font,
para: Para,
som: String,
}
impl Ctx {
fn child(&self, c: &Common) -> Ctx {
Ctx { font: self.font.apply(&c.font), para: self.para.apply(&c.para), som: self.som.clone() }
}
fn with_som(&self, c: &Common, index: usize) -> Ctx {
let mut out = self.child(c);
if let Some(n) = &c.name {
if !out.som.is_empty() {
out.som.push('.');
}
out.som.push_str(&format!("{n}[{index}]"));
}
out
}
}
/// (address of a subform or area in the template, available width bits, depth).
type MeasureKey = (usize, u64, usize);
/// A cached size, with the inherited text defaults it was measured with.
struct Measured {
font: Font,
para: Para,
size: (f64, f64),
}
struct Cursor {
area: usize,
content: usize,
rect: Rect,
y: f64,
/// Something from the flow was placed in this content area.
placed: bool,
}
struct Layouter<'t> {
tpl: &'t Template,
data: Option<&'t DataNode>,
/// Shared so a page's master items keep their addresses (the measurement cache keys on them).
areas: Rc<Vec<PageArea>>,
uses: Vec<usize>,
pages: Vec<Page>,
cur: Option<Cursor>,
names: HashMap<String, usize>,
/// Field values by `id`, for embedded fields in rich text.
values: HashMap<String, String>,
warnings: Vec<String>,
warned: HashSet<String>,
fields: usize,
items: usize,
/// Container sizes by (address of the subform or area in the template, available width,
/// depth), with the inherited font and paragraph they were measured with.
measured: RefCell<HashMap<MeasureKey, Measured>>,
/// Measurements made so far (see [`MAX_MEASURES`]).
measures: Cell<usize>,
}
/// How many instances of a repeating subform to lay out: what the data holds once it holds
/// any (rows added or removed), else the template's initial count; never below one, never
/// above the maximum.
pub fn instance_count(occur: &Occur, from_data: usize) -> usize {
let n = if from_data > 0 { from_data } else { occur.initial };
n.min(occur.max.unwrap_or(usize::MAX)).clamp(1, MAX_INSTANCES)
}
/// Index of child `i` among the earlier siblings with the same name (the SOM index).
fn sib_index(children: &[Node], i: usize) -> usize {
let name = children.get(i).and_then(|c| c.common().name.as_deref());
children.iter().take(i).filter(|c| c.common().name.as_deref() == name && name.is_some()).count()
}
/// Lay a template out, with the values and repeat counts of `data` (the `xfa:data` element).
pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result<Form, XfaError> {
let areas = match &tpl.root.page_set {
Some(ps) if !ps.areas.is_empty() => ps.areas.clone(),
_ => vec![PageArea {
name: None,
width: 612.0,
height: 792.0,
content: vec![Rect::new(36.0, 36.0, 540.0, 720.0)],
items: Vec::new(),
occur_max: None,
}],
};
let mut values = HashMap::new();
// One copy of the root for the whole layout: measurements are cached by node address.
let root_node = Node::Subform(Box::new(tpl.root.clone()));
collect_values(std::slice::from_ref(&root_node), &mut values, 0);
let mut l = Layouter {
tpl,
data,
uses: vec![0; areas.len()],
areas: Rc::new(areas),
pages: Vec::new(),
cur: None,
names: HashMap::new(),
values,
warnings: Vec::new(),
warned: HashSet::new(),
fields: 0,
items: 0,
measured: RefCell::new(HashMap::new()),
measures: Cell::new(0),
};
let ctx = Ctx { font: Font::default(), para: Para::default(), som: String::new() };
l.new_page()?;
if tpl.root.common.presence.occupies() {
l.flow_subform(&root_node, None, &ctx, 0, 0)?;
}
l.check_measures()?;
let total = l.pages.len();
let count = total.to_string();
for p in &mut l.pages {
for it in &mut p.items {
if let Item::Text(s) = it
&& s.text.contains(PAGE_COUNT_MARK)
{
s.text = s.text.replace(PAGE_COUNT_MARK, &count);
}
}
}
Ok(Form { pages: l.pages, fields: l.fields, warnings: l.warnings })
}
fn collect_values(nodes: &[Node], out: &mut HashMap<String, String>, depth: usize) {
if depth > MAX_DEPTH {
return;
}
for n in nodes {
match n {
Node::Field(f) => {
if let Some(id) = &f.common.id {
out.insert(id.clone(), f.value.plain().unwrap_or_default());
}
}
Node::Subform(s) => {
collect_values(&s.children, out, depth + 1);
if let Some(ps) = &s.page_set {
for a in &ps.areas {
collect_values(&a.items, out, depth + 1);
}
}
}
Node::Area(a) => collect_values(&a.children, out, depth + 1),
Node::ExclGroup(_) | Node::Draw(_) => {}
}
}
}
fn clamp_opt(v: f64, min: Option<f64>, max: Option<f64>) -> f64 {
let mut v = v;
if let Some(m) = min {
v = v.max(m);
}
if let Some(m) = max
&& m > 0.0
{
v = v.min(m);
}
v.max(0.0)
}
/// Does `node` flow (may be split across pages) rather than being placed as one block?
fn splittable(node: &Node) -> bool {
matches!(node, Node::Subform(s) if matches!(s.layout, Layout::Tb | Layout::Table) && s.common.h.is_none())
}
fn is_row(node: &Node) -> bool {
matches!(node, Node::Subform(s) if s.layout == Layout::Row)
}
/// The Acrobat date pattern for an XFA picture clause such as `date{YYYY-MM-DD}`.
fn date_pattern(picture: Option<&str>) -> String {
let p = picture.unwrap_or("");
let inner = p.find('{').and_then(|a| p[a + 1..].find('}').map(|b| &p[a + 1..a + 1 + b])).unwrap_or(p);
let inner = inner.trim();
if inner.is_empty() || inner.chars().any(|c| !"YMDEJ-/., :".contains(c)) {
return "yyyy-mm-dd".into();
}
inner.replace('E', "d").to_ascii_lowercase()
}
fn button_action(scripts: &[Script]) -> Option<Action> {
let s = scripts.iter().find(|s| s.activity == "click")?;
let t = &s.text;
// Only a single-statement idiom becomes a native PDF action; anything else is run by the
// XFA scripting engine.
let one = t.trim().trim_end_matches(';');
let single = !one.contains(';') && !one.contains('\n');
if !single {
return (!t.trim().is_empty()).then(|| Action::Script(t.clone()));
}
if t.contains("resetData") {
Some(Action::Reset)
} else if t.contains("host.print") || t.contains("execMenuItem(\"Print\")") {
Some(Action::Print)
} else if t.contains("SaveAs") || t.contains("execMenuItem(\"Save\")") {
Some(Action::SaveAs)
} else if let Some(i) = t.find("launchURL(") {
let rest = &t[i + "launchURL(".len()..];
let q = rest.find(['"', '\''])?;
let quote = rest[q..].chars().next()?;
let body = &rest[q + 1..];
let end = body.find(quote)?;
Some(Action::Url(body[..end].to_string()))
} else if t.trim().is_empty() {
None
} else {
Some(Action::Script(t.clone()))
}
}
/// A PDF field name: no periods (they separate hierarchy levels), no control characters.
fn clean_name(name: &str) -> String {
let s: String = name.chars().filter(|c| !c.is_control()).map(|c| if c == '.' { '_' } else { c }).collect();
let s = s.trim().to_string();
if s.is_empty() { "field".into() } else { s }
}
impl Layouter<'_> {
fn warn_once(&mut self, msg: impl Into<String>) {
let m = msg.into();
if self.warned.insert(m.clone()) && self.warnings.len() < 100 {
self.warnings.push(m);
}
}
fn page_no(&self) -> usize {
self.pages.len()
}
fn embed(&self, id: &str) -> String {
match self.tpl.page_roles.iter().find(|(i, _)| i == id).map(|(_, r)| *r) {
Some(PageRole::Number) => self.page_no().to_string(),
Some(PageRole::Count) => PAGE_COUNT_MARK.to_string(),
None => self.values.get(id).cloned().unwrap_or_default(),
}
}
/// `TooLarge` once the measurement budget is spent (sizes measured after that are guesses).
fn check_measures(&self) -> Result<(), XfaError> {
if self.measures.get() > MAX_MEASURES {
return Err(XfaError::TooLarge("the form is nested too deeply to lay out".into()));
}
Ok(())
}
fn push(&mut self, item: Item) -> Result<(), XfaError> {
self.check_measures()?;
self.items += 1;
if self.items > MAX_ITEMS {
return Err(XfaError::TooLarge("the form has too many objects to lay out".into()));
}
if let Some(p) = self.pages.last_mut() {
p.items.push(item);
}
Ok(())
}
fn push_block(&mut self, b: Block) -> Result<(), XfaError> {
for s in b.spans {
if s.face.underline {
let w = s.face.width(&s.text);
let y = s.baseline + s.face.size * 0.1;
self.push(Item::Line { from: (s.x, y), to: (s.x + w, y), width: (s.face.size * 0.06).max(0.3), color: s.face.color, dashed: false })?;
}
if !s.text.trim().is_empty() {
self.push(Item::Text(s))?;
}
}
Ok(())
}
// ───────────────────────────────────────────────────────────────────────── pages
fn new_page(&mut self) -> Result<(), XfaError> {
if self.pages.len() >= MAX_PAGES {
return Err(XfaError::TooLarge(format!("the form lays out to more than {MAX_PAGES} pages")));
}
let idx =
(0..self.areas.len()).find(|&i| self.areas[i].occur_max.is_none_or(|m| self.uses[i] < m)).unwrap_or(self.areas.len().saturating_sub(1));
let areas = Rc::clone(&self.areas);
let Some(area) = areas.get(idx) else {
return Err(XfaError::Malformed("the template has no page area".into()));
};
if let Some(u) = self.uses.get_mut(idx) {
*u += 1;
}
self.pages.push(Page { width: area.width, height: area.height, items: Vec::new() });
let content = area.content.first().copied().unwrap_or(Rect::new(0.0, 0.0, area.width, area.height));
self.cur = Some(Cursor { area: idx, content: 0, rect: content, y: content.y, placed: false });
// Master page content: positioned draws and fields.
let ctx = Ctx { font: Font::default(), para: Para::default(), som: area.name.clone().map(|n| format!("{n}[0]")).unwrap_or_default() };
let page_rect = Rect::new(0.0, 0.0, area.width, area.height);
for (i, item) in area.items.iter().enumerate() {
self.draw_positioned_child(item, page_rect, &ctx, 1, sib_index(&area.items, i))?;
}
Ok(())
}
/// Move to the next content area, or the next page.
fn next_area(&mut self) -> Result<(), XfaError> {
if let Some(c) = &self.cur
&& let Some(area) = self.areas.get(c.area)
&& let Some(next) = area.content.get(c.content + 1).copied()
{
let i = c.content + 1;
self.cur = Some(Cursor { area: c.area, content: i, rect: next, y: next.y, placed: false });
return Ok(());
}
self.new_page()
}
fn cursor(&self) -> (Rect, f64, bool) {
match &self.cur {
Some(c) => (c.rect, c.y, c.placed),
None => (Rect::new(0.0, 0.0, 612.0, 792.0), 0.0, false),
}
}
/// Move the cursor down (margins and spacing: the area still counts as empty).
fn advance(&mut self, dy: f64) {
if let Some(c) = &mut self.cur {
c.y += dy.max(0.0);
}
}
/// Move past something that was placed.
fn place(&mut self, dy: f64) {
if let Some(c) = &mut self.cur {
c.y += dy.max(0.0);
c.placed = true;
}
}
/// Make room for `h`: stay, or move to the next content area / page. Returns whether a
/// break happened.
fn make_room(&mut self, h: f64) -> Result<bool, XfaError> {
let (rect, y, placed) = self.cursor();
if y + h <= rect.bottom() + 0.01 || !placed {
return Ok(false);
}
self.next_area()?;
Ok(true)
}
fn page_has_flow(&self) -> bool {
self.cur.as_ref().is_some_and(|c| c.placed || c.content > 0)
}
// ───────────────────────────────────────────────────────────────────────── flow
/// Flow `node` (a subform; anything else is ignored) into the content areas.
fn flow_subform(&mut self, node: &Node, avail: Option<(f64, f64)>, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if depth > MAX_DEPTH {
return Ok(());
}
let Node::Subform(sf) = node else { return Ok(()) };
self.check_measures()?;
if sf.break_before_page && self.page_has_flow() {
self.new_page()?;
}
// Data adds instances of a repeating subform (rows added in another viewer).
let from_data = match (&sf.common.name, self.data) {
(Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&som_to_path(&ctx.som), name),
_ => 0,
};
let instances = instance_count(&sf.occur, from_data);
let ctx_parent = ctx;
for i in 0..instances {
let ctx = ctx_parent.with_som(&sf.common, sib + i);
let (rect, _, _) = self.cursor();
let (x, w) = avail.unwrap_or((rect.x, rect.w));
let w = clamp_opt(sf.common.w.unwrap_or(w), sf.common.min_w, sf.common.max_w).min(w.max(1.0));
if !matches!(sf.layout, Layout::Tb | Layout::Table) || sf.common.h.is_some() {
// Not splittable: one block.
let (_, h) = self.size_of(node, w, ctx_parent, depth);
self.make_room(h)?;
let (_, y, _) = self.cursor();
self.draw_node(node, Rect::new(x, y, w, h), ctx_parent, depth, sib + i)?;
self.place(h);
continue;
}
let m = sf.common.margin;
self.advance(m.top);
let inner_x = x + m.left;
let inner_w = (w - m.horizontal()).max(1.0);
match sf.layout {
Layout::Table => self.flow_table(sf, inner_x, inner_w, &ctx, depth)?,
_ => {
for (ci, child) in sf.children.iter().enumerate() {
if !child.common().presence.occupies() {
continue;
}
let csib = sib_index(&sf.children, ci);
if let Node::Subform(_) = child
&& splittable(child)
{
self.flow_subform(child, Some((inner_x, inner_w)), &ctx, depth + 1, csib)?;
continue;
}
let (cw, ch) = self.size_of(child, inner_w, &ctx, depth + 1);
self.make_room(ch)?;
let (_, y, _) = self.cursor();
self.draw_node(child, Rect::new(inner_x, y, cw, ch), &ctx, depth + 1, csib)?;
self.place(ch);
}
}
}
self.advance(m.bottom);
}
if sf.break_after_page {
self.new_page()?;
}
Ok(())
}
fn columns(&self, sf: &Subform, inner_w: f64) -> Vec<f64> {
if !sf.column_widths.is_empty() {
return sf.column_widths.clone();
}
let cells = sf.children.iter().filter_map(|c| if let Node::Subform(r) = c { Some(r.children.len()) } else { None }).max().unwrap_or(1).max(1);
vec![inner_w / cells as f64; cells]
}
fn flow_table(&mut self, sf: &Subform, x: f64, inner_w: f64, ctx: &Ctx, depth: usize) -> Result<(), XfaError> {
let cols = self.columns(sf, inner_w);
let leader =
sf.overflow_leader.as_ref().and_then(|name| sf.children.iter().position(|c| c.common().name.as_deref() == Some(name) && is_row(c)));
for (ci, child) in sf.children.iter().enumerate() {
if !child.common().presence.occupies() {
continue;
}
let csib = sib_index(&sf.children, ci);
if let Node::Subform(row) = child
&& row.layout == Layout::Row
{
// Rows added as data (in another viewer) repeat the row.
let from_data = match (&row.common.name, self.data) {
(Some(name), Some(d)) if row.occur.max != Some(1) => d.count(&som_to_path(&ctx.som), name),
_ => 0,
};
let instances = instance_count(&row.occur, from_data);
for inst in 0..instances {
let h = self.row_height(row, &cols, inner_w, ctx, depth + 1);
if self.make_room(h)?
&& let Some(li) = leader
&& li != ci
&& let Some(Node::Subform(l)) = sf.children.get(li)
{
let lh = self.row_height(l, &cols, inner_w, ctx, depth + 1);
let (_, y, _) = self.cursor();
self.draw_row(l, &cols, Rect::new(x, y, inner_w, lh), ctx, depth + 1, sib_index(&sf.children, li))?;
self.place(lh);
}
let (_, y, _) = self.cursor();
self.draw_row(row, &cols, Rect::new(x, y, inner_w, h), ctx, depth + 1, csib + inst)?;
self.place(h);
}
continue;
}
if splittable(child)
&& let Node::Subform(_) = child
{
self.flow_subform(child, Some((x, inner_w)), ctx, depth + 1, csib)?;
continue;
}
let (cw, ch) = self.size_of(child, inner_w, ctx, depth + 1);
self.make_room(ch)?;
let (_, y, _) = self.cursor();
self.draw_node(child, Rect::new(x, y, cw, ch), ctx, depth + 1, csib)?;
self.place(ch);
}
Ok(())
}
// ───────────────────────────────────────────────────────────────────────── sizing
/// The size a node takes when given `avail_w` of width. Containers are measured once per
/// (node, width, depth): measuring a width-less container measures its content twice (for
/// its width, then its height), which would otherwise be exponential in the nesting.
fn size_of(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) {
let c = node.common();
let spent = self.measures.get().saturating_add(1);
self.measures.set(spent);
if depth > MAX_DEPTH || spent > MAX_MEASURES {
return (c.w.unwrap_or(avail_w), c.h.unwrap_or(0.0));
}
// Only containers recurse; their addresses are stable for the whole layout (the
// template is borrowed, the root and page areas are held by the layouter).
let key = match node {
Node::Subform(s) => Some((&**s as *const Subform as usize, avail_w.to_bits(), depth)),
Node::Area(a) => Some((&**a as *const Area as usize, avail_w.to_bits(), depth)),
_ => None,
};
if let Some(key) = key
&& let Ok(cache) = self.measured.try_borrow()
&& let Some(m) = cache.get(&key)
&& m.font == ctx.font
&& m.para == ctx.para
{
return m.size;
}
let size = self.measure(node, avail_w, ctx, depth);
if let Some(key) = key
&& let Ok(mut cache) = self.measured.try_borrow_mut()
{
cache.insert(key, Measured { font: ctx.font.clone(), para: ctx.para.clone(), size });
}
size
}
fn measure(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) {
let c = node.common();
let ctx = ctx.child(c);
// 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.
let w = match c.w {
Some(w) => w,
None => match node {
Node::Subform(_) | Node::ExclGroup(_) | Node::Area(_) => {
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,
},
};
let w = clamp_opt(w, c.min_w, c.max_w);
let h = match c.h {
Some(h) => h,
None => {
let inner_w = (w - c.margin.horizontal()).max(1.0);
let content = match node {
Node::Draw(d) => self.value_height(&d.value, inner_w, &ctx),
Node::Field(f) => self.field_height(f, inner_w, &ctx),
Node::ExclGroup(g) => self.group_height(g, inner_w, &ctx, depth),
Node::Area(a) => self.positioned_height(&a.children, inner_w, &ctx, depth),
Node::Subform(s) => match s.layout {
Layout::Positioned => self.positioned_height(&s.children, inner_w, &ctx, depth),
Layout::Tb => {
s.children.iter().filter(|k| k.common().presence.occupies()).map(|k| self.size_of(k, inner_w, &ctx, depth + 1).1).sum()
}
Layout::LrTb | Layout::RlTb => self.packed_height(&s.children, inner_w, &ctx, depth),
Layout::Table => {
let cols = self.columns(s, inner_w);
s.children
.iter()
.filter(|k| k.common().presence.occupies())
.map(|k| match k {
Node::Subform(r) if r.layout == Layout::Row => self.row_height(r, &cols, inner_w, &ctx, depth + 1),
_ => self.size_of(k, inner_w, &ctx, depth + 1).1,
})
.sum()
}
Layout::Row => self.row_height(s, &self.columns(s, inner_w), inner_w, &ctx, depth + 1),
},
};
content + c.margin.vertical()
}
};
(w, clamp_opt(h, c.min_h, c.max_h))
}
/// The natural width of a container's content within `avail_w`.
fn content_width(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> f64 {
let group: Vec<Node>;
let (children, layout): (&[Node], Layout) = match node {
Node::Subform(s) => {
if s.layout == Layout::Table && !s.column_widths.is_empty() {
return s.column_widths.iter().sum::<f64>().min(avail_w);
}
(&s.children, s.layout)
}
Node::ExclGroup(g) => {
// Fields are leaves: measuring these copies caches nothing.
group = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
(&group, g.layout)
}
Node::Area(a) => (&a.children, Layout::Positioned),
_ => return avail_w,
};
let visible = children.iter().filter(|k| k.common().presence.occupies());
match layout {
Layout::LrTb | Layout::RlTb | Layout::Row => {
let sum: f64 = visible.map(|k| self.size_of(k, avail_w, ctx, depth + 1).0).sum();
if sum <= avail_w + 0.05 { sum } else { avail_w }
}
Layout::Positioned => {
visible.map(|k| k.common().x.unwrap_or(0.0) + self.size_of(k, avail_w, ctx, depth + 1).0).fold(0.0, f64::max).min(avail_w)
}
Layout::Tb | Layout::Table => visible.map(|k| self.size_of(k, avail_w, ctx, depth + 1).0).fold(0.0, f64::max).min(avail_w),
}
}
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)),
Value::Rich(r) => text::measure_height(r, w, &ctx.para, &Face::of(&ctx.font), &|id| self.embed(id)),
Value::Image { data, .. } => crate::pdf::jpeg_size(data).map_or(w, |(iw, ih)| if iw > 0 { w * ih as f64 / iw as f64 } else { w }),
Value::Line { .. } | Value::Rectangle(_) | Value::Empty => 0.0,
}
}
fn caption_reserve(&self, f: &Field, inner_w: f64, ctx: &Ctx) -> Option<(Placement, f64)> {
let cap = f.caption.as_ref()?;
if !cap.presence.occupies() || f.ui == Ui::Button {
return None;
}
if cap.value.is_empty() && cap.reserve.is_none() {
return None;
}
let placement = if cap.placement == Placement::Inline { Placement::Left } else { cap.placement };
let font = ctx.font.apply(&cap.font);
let para = ctx.para.apply(&cap.para);
let reserve = cap.reserve.unwrap_or_else(|| match placement {
Placement::Top | Placement::Bottom => {
let rich = match &cap.value {
Value::Rich(r) => r.clone(),
other => text::plain(&other.plain().unwrap_or_default()),
};
text::measure_height(&rich, inner_w, &para, &Face::of(&font), &|id| self.embed(id))
}
_ => (Face::of(&font).width(&cap.value.plain().unwrap_or_default()) + 2.0).min(inner_w * 0.6),
});
Some((placement, reserve))
}
fn field_height(&self, f: &Field, inner_w: f64, ctx: &Ctx) -> f64 {
let font = ctx.font.apply(&f.common.font);
let lines = if f.multiline { 3.0 } else { 1.0 };
let ui = match f.ui {
Ui::CheckButton => f.check_size.unwrap_or(10.0).max(text::line_height(font.size)),
_ => text::line_height(font.size) * lines + 4.0 + f.ui_margin.vertical(),
};
match self.caption_reserve(f, inner_w, ctx) {
Some((Placement::Top | Placement::Bottom, r)) => ui + r,
Some((_, r)) => {
let cap = f.caption.as_ref().map(|c| {
let rich = match &c.value {
Value::Rich(r) => r.clone(),
other => text::plain(&other.plain().unwrap_or_default()),
};
text::measure_height(&rich, (inner_w - r).max(1.0), &ctx.para.apply(&c.para), &Face::of(&ctx.font.apply(&c.font)), &|id| {
self.embed(id)
})
});
ui.max(cap.unwrap_or(0.0))
}
None => ui,
}
}
fn group_height(&self, g: &ExclGroup, inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
let nodes: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
match g.layout {
Layout::Tb => nodes.iter().filter(|k| k.common().presence.occupies()).map(|k| self.size_of(k, inner_w, ctx, depth + 1).1).sum(),
Layout::Positioned => self.positioned_height(&nodes, inner_w, ctx, depth),
_ => self.packed_height(&nodes, inner_w, ctx, depth),
}
}
fn positioned_height(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
children
.iter()
.filter(|k| k.common().presence.occupies())
.map(|k| {
let c = k.common();
let (_, h) = self.size_of(k, (inner_w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
c.y.unwrap_or(0.0) + h
})
.fold(0.0, f64::max)
}
/// Left-to-right packing: rows of children, wrapped at the available width.
fn pack(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> (Vec<(usize, Rect)>, f64) {
let mut out = Vec::new();
let (mut x, mut y, mut line_h) = (0.0, 0.0, 0.0);
for (i, k) in children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let (w, h) = self.size_of(k, inner_w, ctx, depth + 1);
if x > 0.0 && x + w > inner_w + 0.05 {
y += line_h;
x = 0.0;
line_h = 0.0;
}
out.push((i, Rect::new(x, y, w, h)));
x += w;
line_h = line_h.max(h);
}
(out, y + line_h)
}
fn packed_height(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
self.pack(children, inner_w, ctx, depth).1
}
/// Cell rectangles of a row (relative to the row's inner box) and the row height.
fn cells(&self, row: &Subform, cols: &[f64], inner_w: f64, ctx: &Ctx, depth: usize) -> (Vec<(usize, Rect)>, f64) {
let ctx = ctx.child(&row.common);
let mut out = Vec::new();
let mut col = 0usize;
let mut x = 0.0;
let mut h: f64 = 0.0;
for (i, k) in row.children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let span = k.common().col_span.max(1);
let w = if col < cols.len() { cols.iter().skip(col).take(span).sum::<f64>() } else { k.common().w.unwrap_or((inner_w - x).max(1.0)) };
let (_, ch) = self.size_of(k, w, &ctx, depth + 1);
out.push((i, Rect::new(x, 0.0, w, ch)));
h = h.max(ch);
x += w;
col += span;
}
let h = match row.common.h {
Some(rh) => rh,
None => h + row.common.margin.vertical(),
};
(out, clamp_opt(h, row.common.min_h, row.common.max_h))
}
fn row_height(&self, row: &Subform, cols: &[f64], inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
self.cells(row, cols, inner_w, ctx, depth).1
}
// ───────────────────────────────────────────────────────────────────────── drawing
fn draw_row(&mut self, row: &Subform, cols: &[f64], rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if depth > MAX_DEPTH {
return Ok(());
}
if let Some(b) = &row.common.border {
self.draw_border(b, rect)?;
}
let ctx = ctx.with_som(&row.common, sib);
let inner = rect.inset(&row.common.margin);
let (cells, _) = self.cells(row, cols, inner.w, &ctx, depth);
for (i, r) in cells {
if let Some(k) = row.children.get(i) {
// Cells stretch to the row height.
self.draw_node(k, Rect::new(inner.x + r.x, inner.y, r.w, inner.h), &ctx, depth + 1, sib_index(&row.children, i))?;
}
}
Ok(())
}
fn draw_positioned_child(&mut self, k: &Node, inner: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
let c = k.common();
if !c.presence.occupies() {
return Ok(());
}
let (x, y) = (c.x.unwrap_or(0.0), c.y.unwrap_or(0.0));
let (w, h) = self.size_of(k, (inner.w - x).max(1.0), ctx, depth + 1);
self.draw_node(k, Rect::new(inner.x + x, inner.y + y, w, h), ctx, depth + 1, sib)
}
fn draw_node(&mut self, node: &Node, rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if depth > MAX_DEPTH {
return Ok(());
}
let c = node.common();
if c.presence == Presence::Invisible {
return Ok(());
}
match node {
Node::Draw(d) => self.draw_draw(d, rect, ctx),
Node::Field(f) => self.draw_field(f, rect, ctx, None, sib),
Node::ExclGroup(g) => self.draw_group(g, rect, ctx, depth, sib),
Node::Area(a) => {
let ctx = ctx.with_som(&a.common, sib);
for (i, k) in a.children.iter().enumerate() {
self.draw_positioned_child(k, rect, &ctx, depth + 1, sib_index(&a.children, i))?;
}
Ok(())
}
Node::Subform(s) => {
if let Some(b) = &s.common.border {
self.draw_border(b, rect)?;
}
let parent_ctx = ctx;
let ctx = ctx.with_som(&s.common, sib);
let inner = rect.inset(&s.common.margin);
match s.layout {
Layout::Positioned => {
for (i, k) in s.children.iter().enumerate() {
self.draw_positioned_child(k, inner, &ctx, depth + 1, sib_index(&s.children, i))?;
}
}
Layout::Tb => {
let mut y = inner.y;
for (i, k) in s.children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, sib_index(&s.children, i))?;
y += h;
}
}
Layout::LrTb | Layout::RlTb => {
let (placed, _) = self.pack(&s.children, inner.w, &ctx, depth);
for (i, r) in placed {
if let Some(k) = s.children.get(i) {
let x = if s.layout == Layout::RlTb { inner.right() - r.x - r.w } else { inner.x + r.x };
self.draw_node(k, Rect::new(x, inner.y + r.y, r.w, r.h), &ctx, depth + 1, sib_index(&s.children, i))?;
}
}
}
Layout::Table => {
let cols = self.columns(s, inner.w);
let mut y = inner.y;
for (i, k) in s.children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let csib = sib_index(&s.children, i);
if let Node::Subform(r) = k
&& r.layout == Layout::Row
{
let h = self.row_height(r, &cols, inner.w, &ctx, depth + 1);
self.draw_row(r, &cols, Rect::new(inner.x, y, inner.w, h), &ctx, depth + 1, csib)?;
y += h;
} else {
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, csib)?;
y += h;
}
}
}
Layout::Row => {
let cols = self.columns(s, inner.w);
self.draw_row(s, &cols, rect, parent_ctx, depth, sib)?;
}
}
Ok(())
}
}
}
fn draw_border(&mut self, b: &Border, rect: Rect) -> Result<(), XfaError> {
if let Some(fill) = b.fill() {
self.push(Item::Fill { rect, color: fill.0 })?;
}
let vis = b.visible_edges();
let (x0, y0, x1, y1) = (rect.x, rect.y, rect.right(), rect.bottom());
// Top, right, bottom, left; lines sit inside the box.
let segs = [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x0, y1), (x1, y1)), ((x0, y0), (x0, y1))];
for (i, seg) in segs.iter().enumerate() {
if !vis[i] {
continue;
}
let e = b.edges[i];
let t = e.thickness.clamp(0.1, 20.0);
let (dx, dy) = match i {
0 => (0.0, t / 2.0),
1 => (-t / 2.0, 0.0),
2 => (0.0, -t / 2.0),
_ => (t / 2.0, 0.0),
};
let color = if e.stroke == Stroke::Relief { [0.55, 0.55, 0.55] } else { e.color.0 };
self.push(Item::Line {
from: (seg.0.0 + dx, seg.0.1 + dy),
to: (seg.1.0 + dx, seg.1.1 + dy),
width: t,
color,
dashed: matches!(e.stroke, Stroke::Dashed | Stroke::Dotted),
})?;
}
Ok(())
}
fn draw_value_text(&mut self, v: &Value, rect: Rect, font: &Font, para: &Para) -> Result<(), XfaError> {
let rich = match v {
Value::Text(t) => text::plain(t),
Value::Rich(r) => r.clone(),
_ => return Ok(()),
};
let block = text::layout(&rich, rect, para, &Face::of(font), &|id| self.embed(id));
self.push_block(block)
}
fn draw_draw(&mut self, d: &Draw, rect: Rect, ctx: &Ctx) -> Result<(), XfaError> {
if let Some(b) = &d.common.border {
self.draw_border(b, rect)?;
}
let ctx = ctx.child(&d.common);
let inner = rect.inset(&d.common.margin);
match &d.value {
Value::Text(_) | Value::Rich(_) => self.draw_value_text(&d.value, inner, &ctx.font, &ctx.para),
Value::Image { content_type, data } => {
let fitted = match crate::pdf::jpeg_size(data) {
Some((iw, ih)) if iw > 0 && ih > 0 && inner.w > 0.0 && inner.h > 0.0 => {
let k = (inner.w / iw as f64).min(inner.h / ih as f64);
Rect::new(inner.x, inner.y, iw as f64 * k, ih as f64 * k)
}
_ => inner,
};
self.push(Item::Image { rect: fitted, data: Arc::new(data.clone()), content_type: content_type.clone() })
}
Value::Line { slope_up, edge } => {
if !edge.visible() {
return Ok(());
}
let (from, to) = if *slope_up {
((inner.x, inner.bottom()), (inner.right(), inner.y))
} else {
((inner.x, inner.y), (inner.right(), inner.bottom()))
};
self.push(Item::Line { from, to, width: edge.thickness.clamp(0.1, 20.0), color: edge.color.0, dashed: edge.stroke == Stroke::Dashed })
}
Value::Rectangle(b) => self.draw_border(b, inner),
Value::Empty => Ok(()),
}
}
fn draw_group(&mut self, g: &ExclGroup, rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if let Some(b) = &g.common.border {
self.draw_border(b, rect)?;
}
let ctx = ctx.with_som(&g.common, sib);
// The group's data node holds the selected button's on value.
let selected: Option<String> =
self.data.and_then(|d| d.text_at(&som_to_path(&ctx.som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
let inner = rect.inset(&g.common.margin);
let group = self.unique_name(g.common.name.as_deref().unwrap_or("group"));
let nodes: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
let placed: Vec<(usize, Rect)> = match g.layout {
Layout::Tb => {
let mut y = 0.0;
let mut out = Vec::new();
for (i, k) in nodes.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
out.push((i, Rect::new(0.0, y, w, h)));
y += h;
}
out
}
Layout::Positioned => nodes
.iter()
.enumerate()
.filter(|(_, k)| k.common().presence.occupies())
.map(|(i, k)| {
let c = k.common();
let (w, h) = self.size_of(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), &ctx, depth + 1);
(i, Rect::new(c.x.unwrap_or(0.0), c.y.unwrap_or(0.0), w, h))
})
.collect(),
_ => self.pack(&nodes, inner.w, &ctx, depth).0,
};
for (i, r) in placed {
if let Some(f) = g.fields.get(i) {
let on = f.items.first().cloned().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| (i + 1).to_string());
let tip = f.tooltip.clone().or_else(|| g.tooltip.clone());
let chosen = selected.as_deref() == Some(on.as_str());
self.draw_field(f, Rect::new(inner.x + r.x, inner.y + r.y, r.w, r.h), &ctx, Some((&group, &on, tip, chosen)), sib_index(&nodes, i))?;
}
}
Ok(())
}
fn unique_name(&mut self, base: &str) -> String {
let base = clean_name(base);
let n = self.names.entry(base.clone()).or_insert(0);
*n += 1;
if *n == 1 { base } else { format!("{base}_{n}") }
}
fn draw_field(
&mut self,
f: &Field,
rect: Rect,
ctx: &Ctx,
radio: Option<(&str, &str, Option<String>, bool)>,
sib: usize,
) -> Result<(), XfaError> {
if let Some(b) = &f.common.border
&& f.ui != Ui::Button
{
self.draw_border(b, rect)?;
}
let fctx = ctx.child(&f.common);
let inner = rect.inset(&f.common.margin);
let (mut ui, mut cap_rect) = (inner, None);
if let Some((placement, reserve)) = self.caption_reserve(f, inner.w, ctx) {
let r = reserve.max(0.0);
match placement {
Placement::Top => {
cap_rect = Some(Rect::new(inner.x, inner.y, inner.w, r.min(inner.h)));
ui = Rect::new(inner.x, inner.y + r, inner.w, inner.h - r);
}
Placement::Bottom => {
cap_rect = Some(Rect::new(inner.x, inner.bottom() - r.min(inner.h), inner.w, r.min(inner.h)));
ui = Rect::new(inner.x, inner.y, inner.w, inner.h - r);
}
Placement::Right => {
cap_rect = Some(Rect::new(inner.right() - r.min(inner.w), inner.y, r.min(inner.w), inner.h));
ui = Rect::new(inner.x, inner.y, inner.w - r, inner.h);
}
_ => {
cap_rect = Some(Rect::new(inner.x, inner.y, r.min(inner.w), inner.h));
ui = Rect::new(inner.x + r, inner.y, inner.w - r, inner.h);
}
}
}
if let (Some(cr), Some(cap)) = (cap_rect, &f.caption)
&& cap.presence == Presence::Visible
{
let font = ctx.font.apply(&cap.font);
let para = ctx.para.apply(&cap.para);
self.draw_value_text(&cap.value, cr, &font, &para)?;
}
match f.ui {
Ui::Signature | Ui::ImageEdit | Ui::Barcode | Ui::Unknown => {
self.warn_once(format!("{:?} fields are not supported yet and are left blank", f.ui));
return Ok(());
}
_ => {}
}
if f.access == Access::NonInteractive && f.ui != Ui::Button {
// Shown as text, not as a field.
return self.draw_value_text(&f.value, ui.inset(&f.ui_margin), &fctx.font, &fctx.para);
}
let face = match (&f.ui, &f.caption) {
// Buttons show their caption in the caption's font.
(Ui::Button, Some(cap)) => Face::of(&ctx.font.apply(&cap.font)),
_ => Face::of(&fctx.font),
};
let h_align = match (&f.ui, &f.caption) {
(Ui::Button, Some(cap)) => ctx.para.apply(&cap.para).h_align,
_ => fctx.para.h_align,
};
let name = self.unique_name(f.common.name.as_deref().unwrap_or("field"));
let som = if fctx.som.is_empty() {
format!("{}[{sib}]", f.common.name.as_deref().unwrap_or("field"))
} else {
format!("{}.{}[{sib}]", fctx.som, f.common.name.as_deref().unwrap_or("field"))
};
let read_only = matches!(f.access, Access::ReadOnly | Access::Protected);
// The data's value wins over the template's default.
let data_value: Option<String> =
self.data.and_then(|d| d.text_at(&som_to_path(&som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
let template_default = f.value.plain().map(|v| v.trim().to_string()).filter(|v| !v.is_empty());
let plain_value = data_value.clone().or_else(|| template_default.clone());
let (kind, rect, border, value, tooltip) = match f.ui {
Ui::CheckButton => {
let s = f.check_size.unwrap_or(10.0).min(ui.w.max(1.0)).min(ui.h.max(1.0));
let y = match fctx.para.v_align {
VAlign::Top => ui.y + f.ui_margin.top,
VAlign::Middle => ui.y + (ui.h - s) / 2.0,
VAlign::Bottom => ui.bottom() - s - f.ui_margin.bottom,
};
let r = Rect::new(ui.x + f.ui_margin.left, y, s, s);
let on = f.items.first().cloned().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| "1".into());
let (kind, tip, checked) = match radio {
Some((group, on_value, tip, chosen)) => (
WidgetKind::Radio { group: group.to_string(), on: on_value.to_string(), round: f.check_round },
tip.or_else(|| f.tooltip.clone()),
chosen,
),
None => (
WidgetKind::CheckBox { on: on.clone(), round: f.check_round },
f.tooltip.clone(),
plain_value.as_deref().is_some_and(|v| is_on(v, &on)),
),
};
let border = widget_border(f.ui_border.as_ref(), true);
(kind, r, border, checked.then(|| on.clone()), tip)
}
Ui::Button => {
let caption = f.caption.as_ref().and_then(|c| c.value.plain()).unwrap_or_default().trim().to_string();
// A button's box is its field border; without one it is invisible (a link
// over text, say).
let border = match &f.common.border {
Some(b) => widget_border(Some(b), false),
None => WidgetBorder { color: None, fill: None, shape: BorderShape::None, width: 0.0 },
};
(WidgetKind::Button { caption }, ui, border, None, f.tooltip.clone())
}
Ui::DateTimeEdit => {
let pattern = date_pattern(f.picture.as_deref());
// Data holds ISO dates; the field shows them in its pattern.
let shown = plain_value.as_ref().map(|v| if data_value.is_some() { iso_to_pattern(v, &pattern) } else { v.clone() });
(WidgetKind::Date(pattern), ui, widget_border(f.ui_border.as_ref(), false), shown, f.tooltip.clone())
}
_ => {
if f.ui == Ui::ChoiceList {
self.warn_once("drop-down lists are shown as text fields");
}
(WidgetKind::Text, ui, widget_border(f.ui_border.as_ref(), false), plain_value, f.tooltip.clone())
}
};
if rect.w < 1.0 || rect.h < 1.0 {
return Ok(());
}
let action = if f.ui == Ui::Button { button_action(&f.scripts) } else { None };
let items: Vec<String> = match &kind {
WidgetKind::CheckBox { on, .. } => vec![on.clone(), f.items.get(1).cloned().unwrap_or_default()],
WidgetKind::Radio { on, .. } => vec![on.clone()],
_ => Vec::new(),
};
self.fields += 1;
self.push(Item::Widget(Box::new(Widget {
rect,
kind,
name,
som,
tooltip,
face,
h_align,
multiline: f.multiline,
max_chars: f.max_chars,
read_only,
border,
value,
default: template_default,
action,
items,
})))
}
}
/// The widget's own border from the `<ui>` border element. Without one, text fields and check
/// boxes get a solid box, as Designer draws them.
fn widget_border(b: Option<&Border>, check: bool) -> WidgetBorder {
let Some(b) = b else {
return WidgetBorder {
color: Some([0.0, 0.0, 0.0]),
fill: if check { Some([1.0, 1.0, 1.0]) } else { None },
shape: BorderShape::Full,
width: 0.5,
};
};
let vis = b.visible_edges();
let shape = match vis {
[false, false, false, false] => BorderShape::None,
[false, false, true, false] => BorderShape::Underline,
_ => BorderShape::Full,
};
let edge = (0..4).find(|&i| vis[i]).map(|i| b.edges[i]).unwrap_or_default();
WidgetBorder {
color: (shape != BorderShape::None).then_some(edge.color.0),
fill: b.fill().map(|c| c.0),
shape,
width: edge.thickness.clamp(0.1, 12.0),
}
}