mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-26 19:19:10 -06:00
1704 lines
74 KiB
Rust
1704 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,
|
|
}],
|
|
};
|
|
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.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> {
|
|
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);
|
|
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),
|
|
}
|
|
}
|