mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-30 04:58:34 -06:00
A page area with <occur min="1" max="1"> that the body never reached was never laid out, so the public ISCIII form lost the page that holds its finishing instructions and the TERMINAR MEMORIA button once the repeated copies over the body pages were removed. PageArea records occur min (0 without an <occur>). When the body has been laid out, an area used fewer times than its minimum gets the pages it still needs, in template order, after the body's pages. The form is 7 pages with the once-only page last, which is what Acrobat Pro shows.
1723 lines
74 KiB
Rust
1723 lines
74 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 base64::Engine;
|
|
|
|
use crate::XfaError;
|
|
use crate::data::{DataNode, data_path, is_on, iso_to_pattern};
|
|
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;
|
|
/// Largest picture taken from the data (decoded bytes), as for template pictures.
|
|
const MAX_DATA_IMAGE: usize = 32 << 20;
|
|
/// Most picture bytes one layout keeps, all copies together. Each laid-out picture holds its
|
|
/// own copy, so a large picture in repeating subforms would otherwise multiply into gigabytes
|
|
/// (a failed allocation aborts the app); a real form's logos and photos stay far below.
|
|
const MAX_IMAGE_BYTES: usize = 256 << 20;
|
|
/// 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 text field whose characters are shown as asterisks.
|
|
Password,
|
|
/// A drop-down (`list_box` false) or list box: `options` are (saved value, shown text).
|
|
Choice {
|
|
options: Vec<(String, String)>,
|
|
list_box: bool,
|
|
multi: bool,
|
|
editable: bool,
|
|
},
|
|
/// A signature field, to be signed in the app.
|
|
Signature,
|
|
/// An image field: a push button that picks a picture (the picture itself is page content).
|
|
Image,
|
|
/// 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,
|
|
/// The field's value: saved (bound) values for choice lists, newline-separated when several.
|
|
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,
|
|
/// Picture bytes laid out so far (see [`MAX_IMAGE_BYTES`]).
|
|
image_bytes: 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>,
|
|
/// The fields and draws of the block being placed: (first item, one past the last, top,
|
|
/// bottom) on the last page. A page break keeps each whole (see `split_block`).
|
|
leaves: Vec<(usize, usize, f64, f64)>,
|
|
}
|
|
|
|
/// 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)
|
|
}
|
|
|
|
/// 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,
|
|
occur_min: 0,
|
|
}],
|
|
};
|
|
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,
|
|
image_bytes: 0,
|
|
measured: RefCell::new(HashMap::new()),
|
|
measures: Cell::new(0),
|
|
leaves: Vec::new(),
|
|
};
|
|
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.satisfy_page_area_minimums()?;
|
|
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())
|
|
}
|
|
|
|
/// Top and bottom of an item a page break must not cut (text, a picture, a field); `None`
|
|
/// for fills and lines, which are cut at the break.
|
|
fn solid_extent(item: &Item) -> Option<(f64, f64)> {
|
|
match item {
|
|
Item::Text(s) => Some((s.baseline - s.face.size * 0.8, s.baseline + s.face.size * 0.2)),
|
|
Item::Image { rect, .. } => Some((rect.y, rect.bottom())),
|
|
Item::Widget(w) => Some((w.rect.y, w.rect.bottom())),
|
|
Item::Fill { .. } | Item::Line { .. } => None,
|
|
}
|
|
}
|
|
|
|
/// `item` as the part above `cut` and the part below: solid items go whole to the side their
|
|
/// top is on, fills and lines crossing the cut are cut in two.
|
|
fn divide(item: Item, cut: f64) -> (Option<Item>, Option<Item>) {
|
|
if let Some((top, _)) = solid_extent(&item) {
|
|
return if top < cut - 0.01 { (Some(item), None) } else { (None, Some(item)) };
|
|
}
|
|
match item {
|
|
Item::Fill { rect, color } => {
|
|
if rect.bottom() <= cut + 0.01 {
|
|
(Some(Item::Fill { rect, color }), None)
|
|
} else if rect.y >= cut - 0.01 {
|
|
(None, Some(Item::Fill { rect, color }))
|
|
} else {
|
|
let above = Rect::new(rect.x, rect.y, rect.w, cut - rect.y);
|
|
let below = Rect::new(rect.x, cut, rect.w, rect.bottom() - cut);
|
|
(Some(Item::Fill { rect: above, color }), Some(Item::Fill { rect: below, color }))
|
|
}
|
|
}
|
|
Item::Line { from, to, width, color, dashed } => {
|
|
let (lo, hi) = if from.1 <= to.1 { (from, to) } else { (to, from) };
|
|
if lo.1 >= cut {
|
|
(None, Some(Item::Line { from, to, width, color, dashed }))
|
|
} else if hi.1 <= cut {
|
|
(Some(Item::Line { from, to, width, color, dashed }), None)
|
|
} else {
|
|
// lo.1 < cut < hi.1, so the divisor is positive.
|
|
let t = (cut - lo.1) / (hi.1 - lo.1);
|
|
let mid = (lo.0 + (hi.0 - lo.0) * t, cut);
|
|
(Some(Item::Line { from: lo, to: mid, width, color, dashed }), Some(Item::Line { from: mid, to: hi, width, color, dashed }))
|
|
}
|
|
}
|
|
other => (Some(other), None),
|
|
}
|
|
}
|
|
|
|
/// `item` moved by (`dx`, `dy`).
|
|
fn moved(mut item: Item, dx: f64, dy: f64) -> Item {
|
|
match &mut item {
|
|
Item::Fill { rect, .. } | Item::Image { rect, .. } => {
|
|
rect.x += dx;
|
|
rect.y += dy;
|
|
}
|
|
Item::Line { from, to, .. } => {
|
|
*from = (from.0 + dx, from.1 + dy);
|
|
*to = (to.0 + dx, to.1 + dy);
|
|
}
|
|
Item::Text(s) => {
|
|
s.x += dx;
|
|
s.baseline += dy;
|
|
}
|
|
Item::Widget(w) => {
|
|
w.rect.x += dx;
|
|
w.rect.y += dy;
|
|
}
|
|
}
|
|
item
|
|
}
|
|
|
|
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> {
|
|
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));
|
|
self.new_page_of(idx)
|
|
}
|
|
|
|
/// Once the body has been laid out, give every page area that has been used fewer times than
|
|
/// its `occur min` the pages it still needs, in template order, after the body's pages.
|
|
fn satisfy_page_area_minimums(&mut self) -> Result<(), XfaError> {
|
|
let areas = Rc::clone(&self.areas);
|
|
for (idx, area) in areas.iter().enumerate() {
|
|
while self.uses.get(idx).is_some_and(|u| *u < area.occur_min) {
|
|
self.new_page_of(idx)?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Start a page from page area `idx`.
|
|
fn new_page_of(&mut self, idx: usize) -> Result<(), XfaError> {
|
|
if self.pages.len() >= MAX_PAGES {
|
|
return Err(XfaError::TooLarge(format!("the form lays out to more than {MAX_PAGES} pages")));
|
|
}
|
|
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);
|
|
let sibs = SomIndices::of(&area.items);
|
|
for (i, item) in area.items.iter().enumerate() {
|
|
self.draw_positioned_child(item, page_rect, &ctx, 1, sibs.at(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)
|
|
}
|
|
|
|
/// Place `node` as one block of size (`w`, `h`) at the cursor: in this content area when it fits,
|
|
/// in the next when it fits there, and across as many as it takes when it is taller than a
|
|
/// content area (a positioned or fixed-height subform holding more than a page).
|
|
fn flow_block(&mut self, node: &Node, x: f64, (w, h): (f64, f64), ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
|
|
let (rect, y, _) = self.cursor();
|
|
if y + h > rect.bottom() + 0.01 && h <= rect.h + 0.01 {
|
|
self.make_room(h)?;
|
|
}
|
|
let (rect, y, _) = self.cursor();
|
|
let first = self.pages.last().map_or(0, |p| p.items.len());
|
|
self.leaves.clear();
|
|
self.draw_node(node, Rect::new(x, y, w, h), ctx, depth, sib)?;
|
|
let page_h = self.pages.last().map_or(f64::INFINITY, |p| p.height);
|
|
// What was drawn, when it is lower than the block's own height says (content that
|
|
// outgrows a fixed height) and ends off the page.
|
|
let drawn = self
|
|
.pages
|
|
.last()
|
|
.and_then(|p| p.items.get(first..))
|
|
.unwrap_or_default()
|
|
.iter()
|
|
.filter_map(solid_extent)
|
|
.map(|e| e.1)
|
|
.fold(y + h, f64::max);
|
|
if y + h <= rect.bottom() + 0.01 && drawn <= page_h + 0.01 {
|
|
self.leaves.clear();
|
|
self.place(h);
|
|
return Ok(());
|
|
}
|
|
self.split_block(first, y, drawn)
|
|
}
|
|
|
|
/// Remember that the items the last page got since `start` are one field or draw at
|
|
/// `rect`, to be kept together by a page break.
|
|
fn keep_whole(&mut self, start: usize, rect: Rect) {
|
|
let end = self.pages.last().map_or(0, |p| p.items.len());
|
|
if end > start {
|
|
self.leaves.push((start, end, rect.y, rect.bottom()));
|
|
}
|
|
}
|
|
|
|
fn item_count(&self) -> usize {
|
|
self.pages.last().map_or(0, |p| p.items.len())
|
|
}
|
|
|
|
/// Spread the items the last page holds from index `first` on (a block drawn from `top` to
|
|
/// `end`, past the bottom of its content area) over this and the following content areas.
|
|
/// Breaks fall between text, pictures and fields, never through one that fits in an area;
|
|
/// fills and lines are cut at the break.
|
|
fn split_block(&mut self, first: usize, top: f64, end: f64) -> Result<(), XfaError> {
|
|
let items: Vec<Item> = match self.pages.last_mut() {
|
|
Some(p) if first <= p.items.len() => p.items.split_off(first),
|
|
_ => return Ok(()),
|
|
};
|
|
// Each item with the extent of the field or draw it belongs to: such a leaf moves
|
|
// whole (its border with its text), unless it is taller than a content area.
|
|
let area_h = self.cursor().0.h;
|
|
let mut extents: Vec<Option<(f64, f64)>> = vec![None; items.len()];
|
|
for (start, end, top, bottom) in std::mem::take(&mut self.leaves) {
|
|
if bottom - top <= area_h + 0.01
|
|
&& let Some(slots) = start.checked_sub(first).and_then(|s| extents.get_mut(s..end.saturating_sub(first)))
|
|
{
|
|
slots.fill(Some((top, bottom)));
|
|
}
|
|
}
|
|
let mut rest: Vec<(Item, Option<(f64, f64)>)> = items.into_iter().zip(extents).collect();
|
|
let origin_x = self.cursor().0.x;
|
|
// An item's place on the current page is its drawn place moved by (dx, -shift).
|
|
let (mut dx, mut shift) = (0.0, 0.0);
|
|
let mut seg_top = top;
|
|
let mut first_segment = true;
|
|
loop {
|
|
let (rect, _, placed) = self.cursor();
|
|
let limit = rect.bottom() + shift;
|
|
let mut cut = limit;
|
|
if rect.h < 1.0 {
|
|
// No room to split into: leave the rest here.
|
|
cut = f64::INFINITY;
|
|
} else {
|
|
// The lowest break at or above the bottom that crosses no solid item: going
|
|
// through them from the lowest top up, a break moved to an item's top can't
|
|
// cross one already passed.
|
|
let mut solid: Vec<(f64, f64)> =
|
|
rest.iter().filter_map(|(item, kept)| kept.or_else(|| solid_extent(item))).filter(|e| e.0 < limit - 0.01).collect();
|
|
solid.sort_by(|a, b| b.0.total_cmp(&a.0));
|
|
for (t, b) in solid {
|
|
if t < cut - 0.01 && b > cut + 0.01 {
|
|
cut = t;
|
|
}
|
|
}
|
|
if cut <= seg_top + 0.01 {
|
|
// An item taller than what is left. Start it in a fresh area if this one
|
|
// already holds something; otherwise cut through it.
|
|
cut = if first_segment && placed && seg_top > rect.y + 0.01 { seg_top } else { limit };
|
|
}
|
|
}
|
|
let mut here = Vec::new();
|
|
let mut below = Vec::new();
|
|
for (item, kept) in rest {
|
|
if let Some((top, _)) = kept {
|
|
if top < cut - 0.01 {
|
|
here.push(item)
|
|
} else {
|
|
below.push((item, kept))
|
|
}
|
|
continue;
|
|
}
|
|
match divide(item, cut) {
|
|
(Some(a), None) => here.push(a),
|
|
(None, Some(b)) => below.push((b, None)),
|
|
(Some(a), Some(b)) => {
|
|
here.push(a);
|
|
below.push((b, None));
|
|
}
|
|
(None, None) => {}
|
|
}
|
|
}
|
|
if let Some(p) = self.pages.last_mut() {
|
|
p.items.extend(here.into_iter().map(|it| moved(it, dx, -shift)));
|
|
}
|
|
if below.is_empty() {
|
|
if let Some(c) = &mut self.cur {
|
|
c.y = (end - shift).min(c.rect.bottom()).max(c.y);
|
|
c.placed = true;
|
|
}
|
|
return Ok(());
|
|
}
|
|
rest = below;
|
|
seg_top = cut;
|
|
first_segment = false;
|
|
self.next_area()?;
|
|
let (rect, _, _) = self.cursor();
|
|
shift = cut - rect.y;
|
|
dx = rect.x - origin_x;
|
|
}
|
|
}
|
|
|
|
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 instances = self.instances(node, &ctx.som);
|
|
self.warn_truncated(node, &ctx.som, instances);
|
|
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.flow_block(node, x, (w, h), ctx_parent, depth, sib + i)?;
|
|
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)?,
|
|
_ => {
|
|
let sibs = SomIndices::of(&sf.children);
|
|
for (ci, child) in sf.children.iter().enumerate() {
|
|
if !child.common().presence.occupies() {
|
|
continue;
|
|
}
|
|
let csib = sibs.at(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.flow_block(child, inner_x, (cw, ch), &ctx, depth + 1, csib)?;
|
|
}
|
|
}
|
|
}
|
|
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)));
|
|
let sibs = SomIndices::of(&sf.children);
|
|
for (ci, child) in sf.children.iter().enumerate() {
|
|
if !child.common().presence.occupies() {
|
|
continue;
|
|
}
|
|
let csib = sibs.at(ci);
|
|
if let Node::Subform(row) = child
|
|
&& row.layout == Layout::Row
|
|
{
|
|
// Rows added as data (in another viewer) repeat the row.
|
|
let instances = self.instances(child, &ctx.som);
|
|
self.warn_truncated(child, &ctx.som, instances);
|
|
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, sibs.at(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.flow_block(child, x, (cw, ch), ctx, depth + 1, csib)?;
|
|
}
|
|
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.
|
|
/// How many instances of `node` to lay out under the container at `parent_som`: a
|
|
/// repeating subform's `occur` and data instances ([`instance_count`]); one for anything else.
|
|
fn instances(&self, node: &Node, parent_som: &str) -> usize {
|
|
let Node::Subform(sf) = node else { return 1 };
|
|
let from_data = match (&sf.common.name, self.data) {
|
|
(Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&data_path(self.tpl, parent_som), name),
|
|
_ => 0,
|
|
};
|
|
instance_count(&sf.occur, from_data)
|
|
}
|
|
|
|
/// The data holds more instances of `node` than `laid_out`: say so, since calculations
|
|
/// then see only those.
|
|
fn warn_truncated(&mut self, node: &Node, parent_som: &str, laid_out: usize) {
|
|
let Node::Subform(sf) = node else { return };
|
|
let from_data = match (&sf.common.name, self.data) {
|
|
(Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&data_path(self.tpl, parent_som), name),
|
|
_ => 0,
|
|
};
|
|
if from_data > laid_out {
|
|
self.warn_once(format!(
|
|
"{}: the data holds {from_data} rows; only the first {laid_out} are laid out (calculations see those)",
|
|
sf.common.name.as_deref().unwrap_or("a subform")
|
|
));
|
|
}
|
|
}
|
|
|
|
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();
|
|
// The node's own SOM: its repeating children count their instances under it, and a
|
|
// growing text field finds its value at it. Measurements are cached per template node,
|
|
// so instance 0 stands for all of them.
|
|
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.
|
|
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),
|
|
// Each instance of a repeating child takes its height again.
|
|
Layout::Tb => s
|
|
.children
|
|
.iter()
|
|
.filter(|k| k.common().presence.occupies())
|
|
.map(|k| self.size_of(k, inner_w, &ctx, depth + 1).1 * self.instances(k, &som) as f64)
|
|
.sum(),
|
|
Layout::LrTb | Layout::RlTb => self.pack_instances(&s.children, inner_w, &ctx, depth, &som).1,
|
|
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()
|
|
}
|
|
};
|
|
// A flowed subform is as tall as what it holds: other viewers do not stretch one to
|
|
// the `minH` a designer left on it.
|
|
let min_h = match node {
|
|
Node::Subform(s) if c.h.is_none() && s.layout != Layout::Positioned => None,
|
|
_ => c.min_h,
|
|
};
|
|
(w, clamp_opt(h, 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| {
|
|
let (w, h) = self.size_of(k, avail_w, ctx, depth + 1);
|
|
k.common().top_left(w, h).0 + w
|
|
})
|
|
.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::image::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, ¶, &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).max(self.grown_text_height(f, inner_w, ctx)) + 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,
|
|
}
|
|
}
|
|
|
|
/// The height the text of a multi-line text field takes (its value in the data, else the
|
|
/// template's), for a field without a fixed height, which grows to hold its text. `ctx`
|
|
/// is the field's own (its SOM ends in the field). Zero for anything else.
|
|
fn grown_text_height(&self, f: &Field, inner_w: f64, ctx: &Ctx) -> f64 {
|
|
if f.ui != Ui::TextEdit || !f.multiline || f.common.h.is_some() || f.common.name.is_none() {
|
|
return 0.0;
|
|
}
|
|
let value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &ctx.som))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain());
|
|
let Some(value) = value.filter(|t| !t.trim().is_empty()) else { return 0.0 };
|
|
// Beside a caption the text has what the caption leaves; 2pt of padding on each side.
|
|
let beside = match self.caption_reserve(f, inner_w, ctx) {
|
|
Some((Placement::Top | Placement::Bottom, _)) | None => 0.0,
|
|
Some((_, r)) => r,
|
|
};
|
|
let w = (inner_w - beside - f.ui_margin.horizontal() - 4.0).max(1.0);
|
|
let font = ctx.font.apply(&f.common.font);
|
|
text::measure_height(&text::plain(value.trim_end()), w, &ctx.para, &Face::of(&font), &|_| String::new())
|
|
}
|
|
|
|
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 (w, h) = self.size_of(k, (inner_w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
|
|
c.top_left(w, h).1 + h
|
|
})
|
|
.fold(0.0, f64::max)
|
|
}
|
|
|
|
/// Left-to-right packing: rows of children, wrapped at the available width, as (child,
|
|
/// instance, place). A repeating subform is packed once per instance it has under the
|
|
/// container at `som`.
|
|
fn pack_instances(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize, som: &str) -> (Vec<(usize, 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);
|
|
for instance in 0..self.instances(k, som) {
|
|
if x > 0.0 && x + w > inner_w + 0.05 {
|
|
y += line_h;
|
|
x = 0.0;
|
|
line_h = 0.0;
|
|
}
|
|
out.push((i, instance, Rect::new(x, y, w, h)));
|
|
x += w;
|
|
line_h = line_h.max(h);
|
|
}
|
|
}
|
|
(out, y + line_h)
|
|
}
|
|
|
|
/// [`Self::pack_instances`] for children that don't repeat (the fields of a group).
|
|
fn pack(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> (Vec<(usize, Rect)>, f64) {
|
|
let (placed, h) = self.pack_instances(children, inner_w, ctx, depth, "");
|
|
(placed.into_iter().map(|(i, _, r)| (i, r)).collect(), 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);
|
|
let sibs = SomIndices::of(&row.children);
|
|
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, sibs.at(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 (w, h) = self.size_of(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
|
|
let (x, y) = c.top_left(w, h);
|
|
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) => {
|
|
let start = self.item_count();
|
|
self.draw_draw(d, rect, ctx)?;
|
|
// Text and pictures stay with their border; a bare line or rectangle is cut.
|
|
if matches!(d.value, Value::Text(_) | Value::Rich(_) | Value::Image { .. }) {
|
|
self.keep_whole(start, rect);
|
|
}
|
|
Ok(())
|
|
}
|
|
Node::Field(f) => {
|
|
let start = self.item_count();
|
|
self.draw_field(f, rect, ctx, None, sib)?;
|
|
self.keep_whole(start, rect);
|
|
Ok(())
|
|
}
|
|
Node::ExclGroup(g) => self.draw_group(g, rect, ctx, depth, sib),
|
|
Node::Area(a) => {
|
|
let ctx = ctx.with_som(&a.common, sib);
|
|
let sibs = SomIndices::of(&a.children);
|
|
for (i, k) in a.children.iter().enumerate() {
|
|
self.draw_positioned_child(k, rect, &ctx, depth + 1, sibs.at(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);
|
|
let sibs = SomIndices::of(&s.children);
|
|
match s.layout {
|
|
Layout::Positioned => {
|
|
for (i, k) in s.children.iter().enumerate() {
|
|
self.draw_positioned_child(k, inner, &ctx, depth + 1, sibs.at(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);
|
|
// A repeating subform here (a table in an area of a positioned page,
|
|
// say) gets its instances, as in a flowed one, one under another.
|
|
for instance in 0..self.instances(k, &ctx.som) {
|
|
self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, sibs.at(i) + instance)?;
|
|
y += h;
|
|
}
|
|
}
|
|
}
|
|
Layout::LrTb | Layout::RlTb => {
|
|
// A repeating subform here (rows of fields under a heading) gets its
|
|
// instances, as in the other layouts.
|
|
let (placed, _) = self.pack_instances(&s.children, inner.w, &ctx, depth, &ctx.som);
|
|
for (i, instance, 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, sibs.at(i) + instance)?;
|
|
}
|
|
}
|
|
}
|
|
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 = sibs.at(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 face = Face::of(font);
|
|
// Text that doesn't fit the height it was given (the template's face is narrower than
|
|
// the standard one drawn in its place, so it wraps to more lines) is drawn smaller,
|
|
// down to 70%, rather than over what lies below.
|
|
let fits = |k: f64| text::measure_height(&rich, rect.w / k, para, &face, &|id| self.embed(id)) <= rect.h / k + 1.0;
|
|
let scale = if rect.h < 4.0 || fits(1.0) { 1.0 } else { [0.95, 0.9, 0.85, 0.8, 0.75].into_iter().find(|k| fits(*k)).unwrap_or(0.7) };
|
|
let mut block = text::layout(&rich, Rect::new(rect.x, rect.y, rect.w / scale, rect.h / scale), para, &face, &|id| self.embed(id));
|
|
if scale < 1.0 {
|
|
for s in &mut block.spans {
|
|
s.x = rect.x + (s.x - rect.x) * scale;
|
|
s.baseline = rect.y + (s.baseline - rect.y) * scale;
|
|
s.face.size *= scale;
|
|
}
|
|
}
|
|
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 } => self.draw_image(data, content_type, inner),
|
|
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(&data_path(self.tpl, &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);
|
|
let (x, y) = c.top_left(w, h);
|
|
(i, Rect::new(x, y, w, h))
|
|
})
|
|
.collect(),
|
|
_ => self.pack(&nodes, inner.w, &ctx, depth).0,
|
|
};
|
|
let sibs = SomIndices::of(&nodes);
|
|
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)), sibs.at(i))?;
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// A picture scaled to fit `inner`, keeping its shape, from its top left.
|
|
fn draw_image(&mut self, data: &[u8], content_type: &str, inner: Rect) -> Result<(), XfaError> {
|
|
self.image_bytes = self.image_bytes.saturating_add(data.len());
|
|
if self.image_bytes > MAX_IMAGE_BYTES {
|
|
return Err(XfaError::TooLarge("the form's pictures are too large together to lay out".into()));
|
|
}
|
|
let fitted = match crate::image::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.to_vec()), content_type: content_type.to_string() })
|
|
}
|
|
|
|
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, ¶)?;
|
|
}
|
|
let som_path = || {
|
|
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"))
|
|
}
|
|
};
|
|
match f.ui {
|
|
Ui::Unknown => {
|
|
self.warn_once("a field of an unknown kind is left blank");
|
|
return Ok(());
|
|
}
|
|
Ui::ImageEdit => {
|
|
// The picture the data holds (base64, as Designer saves it) or the template's.
|
|
let field_name = f.common.name.clone().unwrap_or_else(|| "an image field".into());
|
|
let text: Vec<u8> = self
|
|
.data
|
|
.and_then(|d| d.text_at(&data_path(self.tpl, &som_path())))
|
|
.map(|t| t.bytes().filter(|b| !b.is_ascii_whitespace()).collect())
|
|
.unwrap_or_default();
|
|
let from_data = if text.is_empty() {
|
|
None
|
|
} else if text.len() > MAX_DATA_IMAGE / 3 * 4 + 4 {
|
|
self.warn_once(format!("{field_name}: the picture in the data is larger than 32 MB and is left out"));
|
|
None
|
|
} else {
|
|
match base64::engine::general_purpose::STANDARD.decode(&text) {
|
|
Ok(d) if crate::image::kind(&d).is_some() => Some(d),
|
|
Ok(_) => {
|
|
self.warn_once(format!("{field_name}: the picture in the data is not a JPEG, PNG or GIF and is left out"));
|
|
None
|
|
}
|
|
Err(_) => {
|
|
self.warn_once(format!("{field_name}: the data holds no readable picture (not base64) and is left out"));
|
|
None
|
|
}
|
|
}
|
|
};
|
|
let box_ = ui.inset(&f.ui_margin);
|
|
match (&from_data, &f.value) {
|
|
(Some(d), _) => self.draw_image(d, "", box_)?,
|
|
(None, Value::Image { content_type, data }) => self.draw_image(data, content_type, box_)?,
|
|
_ => {}
|
|
}
|
|
// Then the field itself, which clicking gives a new picture.
|
|
}
|
|
Ui::Barcode => {
|
|
// The value as text, where the bars would be.
|
|
self.warn_once("barcode fields show their value as text, not as bars");
|
|
let value = self
|
|
.data
|
|
.and_then(|d| d.text_at(&data_path(self.tpl, &som_path())))
|
|
.map(str::to_string)
|
|
.or_else(|| f.value.plain())
|
|
.unwrap_or_default();
|
|
if !value.trim().is_empty() {
|
|
let mut para = fctx.para.clone();
|
|
para.h_align = HAlign::Center;
|
|
self.draw_value_text(&Value::Text(value), ui.inset(&f.ui_margin), &fctx.font, ¶)?;
|
|
}
|
|
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);
|
|
}
|
|
// A text field that grew past what a content area holds can't be one widget: its text
|
|
// is drawn as lines the page breaks fall between, and stays what the data holds.
|
|
if f.ui == Ui::TextEdit && f.multiline && f.common.h.is_none() && rect.h > self.cursor().0.h + 0.01 {
|
|
let value =
|
|
self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som_path()))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain());
|
|
if let Some(value) = value.filter(|t| !t.trim().is_empty()) {
|
|
let name = f.common.name.clone().unwrap_or_else(|| "a text field".into());
|
|
self.warn_once(format!("{name}: its text is longer than a page; it is shown across pages and can't be edited here"));
|
|
let m = Margin {
|
|
top: f.ui_margin.top + 2.0,
|
|
bottom: f.ui_margin.bottom + 2.0,
|
|
left: f.ui_margin.left + 2.0,
|
|
right: f.ui_margin.right + 2.0,
|
|
};
|
|
let para = Para { v_align: VAlign::Top, ..fctx.para.clone() };
|
|
let block = text::layout(&text::plain(value.trim_end()), ui.inset(&m), ¶, &Face::of(&fctx.font), &|_| String::new());
|
|
return self.push_block(block);
|
|
}
|
|
}
|
|
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 = som_path();
|
|
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.value_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
|
|
// A choice list's default is a saved value too.
|
|
let template_default = f.value.plain().map(|v| f.saved_value(v.trim())).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())
|
|
}
|
|
Ui::ChoiceList => {
|
|
let options: Vec<(String, String)> = f
|
|
.items
|
|
.iter()
|
|
.enumerate()
|
|
.map(|(i, shown)| (f.item_values.get(i).cloned().unwrap_or_else(|| shown.clone()), shown.clone()))
|
|
.collect();
|
|
// The data holds saved values (several on separate lines); shown text is taken
|
|
// too, as a user typing into an editable list leaves it.
|
|
let value = plain_value.as_ref().map(|v| f.saved_value(v));
|
|
let kind = WidgetKind::Choice { options, list_box: f.choice.list_box, multi: f.choice.multi, editable: f.choice.editable };
|
|
(kind, ui, widget_border(f.ui_border.as_ref(), false), value, f.tooltip.clone())
|
|
}
|
|
Ui::Signature => (WidgetKind::Signature, ui, widget_border(f.ui_border.as_ref(), false), None, f.tooltip.clone()),
|
|
Ui::ImageEdit => {
|
|
let none = WidgetBorder { color: None, fill: None, shape: BorderShape::None, width: 0.0 };
|
|
(WidgetKind::Image, ui, none, None, f.tooltip.clone())
|
|
}
|
|
Ui::PasswordEdit => (WidgetKind::Password, ui, widget_border(f.ui_border.as_ref(), false), plain_value, f.tooltip.clone()),
|
|
_ => (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, a check box gets a
|
|
/// solid box, as Designer draws it, and any other field has no box of its own.
|
|
fn widget_border(b: Option<&Border>, check: bool) -> WidgetBorder {
|
|
let Some(b) = b else {
|
|
if !check {
|
|
return WidgetBorder { color: None, fill: None, shape: BorderShape::None, width: 0.0 };
|
|
}
|
|
return WidgetBorder { color: Some([0.0, 0.0, 0.0]), fill: Some([1.0, 1.0, 1.0]), 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),
|
|
}
|
|
}
|