1use crate::appearance::parse_da;
13use crate::button::{button_kind, ButtonKind};
14use crate::flags::FieldFlags;
15use crate::tree::{ChoiceOption, FieldId, FieldTree, FieldType, FieldValue, Quadding};
16
17#[derive(Debug, Clone, PartialEq)]
19pub enum FormFieldKind {
20 Text {
22 multiline: bool,
24 comb: bool,
27 password: bool,
29 },
30 Checkbox {
32 on_state: String,
35 checked: bool,
37 },
38 RadioGroup {
40 options: Vec<String>,
43 },
44 ComboBox {
46 editable: bool,
48 options: Vec<ChoiceOption>,
50 },
51 ListBox {
53 multi_select: bool,
55 options: Vec<ChoiceOption>,
57 },
58 PushButton,
60 Signature,
62}
63
64#[derive(Debug, Clone, PartialEq)]
66pub struct WidgetModel {
67 pub page_index: Option<usize>,
69 pub rect: [f32; 4],
71 pub on_state: Option<String>,
73 pub appearance_state: Option<String>,
75}
76
77#[derive(Debug, Clone, PartialEq)]
79pub struct DaInfo {
80 pub font_name: Option<String>,
82 pub font_size: f32,
84 pub color: Vec<f32>,
86}
87
88#[derive(Debug, Clone, PartialEq)]
90pub struct FormFieldModel {
91 pub name: String,
93 pub kind: FormFieldKind,
95 pub value: Option<String>,
99 pub selected_values: Option<Vec<String>>,
102 pub default_value: Option<String>,
104 pub tooltip: Option<String>,
106 pub read_only: bool,
108 pub required: bool,
110 pub max_len: Option<u32>,
112 pub quadding: Quadding,
114 pub da: DaInfo,
116 pub widgets: Vec<WidgetModel>,
118}
119
120pub fn build_form_model(tree: &FieldTree) -> Vec<FormFieldModel> {
127 let mut out = Vec::new();
128 for id in tree.all_ids() {
129 if !is_logical_field(tree, id) {
130 continue;
131 }
132 if let Some(model) = field_model(tree, id) {
133 out.push(model);
134 }
135 }
136 out
137}
138
139fn is_logical_field(tree: &FieldTree, id: FieldId) -> bool {
142 if tree.effective_field_type(id).is_none() {
143 return false;
144 }
145 let node = tree.get(id);
146 if node.partial_name.is_empty() && node.parent.is_some() {
148 return false;
149 }
150 !node
152 .children
153 .iter()
154 .any(|&c| !tree.get(c).partial_name.is_empty())
155}
156
157fn field_model(tree: &FieldTree, id: FieldId) -> Option<FormFieldModel> {
158 let node = tree.get(id);
159 let ft = tree.effective_field_type(id)?;
160 let flags = effective_flags_deep(tree, id);
161
162 let widgets = collect_widgets(tree, id);
163 let raw_value = tree.effective_value(id);
164 let selected_values = match &raw_value {
165 Some(FieldValue::StringArray(arr)) => Some(arr.clone()),
166 _ => None,
167 };
168 let value = raw_value.map(value_to_string);
169 let default_value = node.default_value.as_ref().map(value_to_string);
170
171 let kind = match ft {
172 FieldType::Text => FormFieldKind::Text {
173 multiline: flags.multiline(),
174 comb: flags.comb(),
175 password: flags.password(),
176 },
177 FieldType::Button => match button_kind(flags) {
178 ButtonKind::PushButton => FormFieldKind::PushButton,
179 ButtonKind::Checkbox => {
180 let on_state = widgets
181 .iter()
182 .find_map(|w| w.on_state.clone())
183 .unwrap_or_else(|| "Yes".to_string());
184 let checked = value.as_deref().is_some_and(|v| v != "Off")
185 || widgets
186 .iter()
187 .any(|w| w.appearance_state.as_deref().is_some_and(|s| s != "Off"));
188 FormFieldKind::Checkbox { on_state, checked }
189 }
190 ButtonKind::Radio => FormFieldKind::RadioGroup {
191 options: widgets
192 .iter()
193 .map(|w| w.on_state.clone().unwrap_or_default())
194 .collect(),
195 },
196 },
197 FieldType::Choice => {
198 if flags.combo() {
199 FormFieldKind::ComboBox {
200 editable: flags.edit(),
201 options: node.options.clone(),
202 }
203 } else {
204 FormFieldKind::ListBox {
205 multi_select: flags.multi_select(),
206 options: node.options.clone(),
207 }
208 }
209 }
210 FieldType::Signature => FormFieldKind::Signature,
211 };
212
213 let da_str = tree.effective_da(id).unwrap_or("/Helv 0 Tf 0 g");
214 let da = parse_da(da_str);
215
216 Some(FormFieldModel {
217 name: tree.fully_qualified_name(id),
218 kind,
219 value,
220 selected_values,
221 default_value,
222 tooltip: node.alternate_name.clone(),
223 read_only: flags.read_only(),
224 required: flags.required(),
225 max_len: tree.effective_max_len(id),
226 quadding: tree.effective_quadding(id),
227 da: DaInfo {
228 font_name: da.font_name,
229 font_size: da.font_size,
230 color: da.color,
231 },
232 widgets,
233 })
234}
235
236fn effective_flags_deep(tree: &FieldTree, id: FieldId) -> FieldFlags {
239 let mut cur = Some(id);
240 while let Some(cid) = cur {
241 let node = tree.get(cid);
242 if node.flags.bits() != 0 {
243 return node.flags;
244 }
245 cur = node.parent;
246 }
247 FieldFlags::empty()
248}
249
250fn collect_widgets(tree: &FieldTree, id: FieldId) -> Vec<WidgetModel> {
251 let node = tree.get(id);
252 let mut widgets = Vec::new();
253 if node.children.is_empty() {
254 if let Some(rect) = node.rect {
255 widgets.push(WidgetModel {
256 page_index: node.page_index,
257 rect,
258 on_state: node.on_state.clone(),
259 appearance_state: node.appearance_state.clone(),
260 });
261 }
262 } else {
263 for &kid in &node.children {
264 let k = tree.get(kid);
265 if let Some(rect) = k.rect {
266 widgets.push(WidgetModel {
267 page_index: k.page_index,
268 rect,
269 on_state: k.on_state.clone(),
270 appearance_state: k.appearance_state.clone(),
271 });
272 }
273 }
274 }
275 widgets
276}
277
278fn value_to_string(v: &FieldValue) -> String {
279 match v {
280 FieldValue::Text(s) => s.clone(),
281 FieldValue::StringArray(arr) => arr.join(", "),
282 }
283}
284
285#[cfg(test)]
286mod tests {
287 use super::*;
288 use crate::tree::FieldNode;
289
290 fn blank_node(name: &str) -> FieldNode {
291 FieldNode {
292 partial_name: name.into(),
293 alternate_name: None,
294 mapping_name: None,
295 field_type: None,
296 flags: FieldFlags::empty(),
297 value: None,
298 default_value: None,
299 default_appearance: None,
300 quadding: None,
301 max_len: None,
302 options: vec![],
303 top_index: None,
304 rect: None,
305 appearance_state: None,
306 on_state: None,
307 page_index: None,
308 parent: None,
309 children: vec![],
310 object_id: None,
311 has_actions: false,
312 mk: None,
313 border_style: None,
314 }
315 }
316
317 #[test]
318 fn radio_group_is_one_logical_field_with_widget_options() {
319 let mut tree = FieldTree::new();
320 let mut group = blank_node("kleur");
321 group.field_type = Some(FieldType::Button);
322 group.flags = FieldFlags::from_bits(1 << 15); let gid = tree.alloc(group);
324 for (i, state) in ["Rood", "Blauw"].iter().enumerate() {
325 let mut w = blank_node("");
326 w.parent = Some(gid);
327 w.rect = Some([0.0, i as f32 * 20.0, 10.0, i as f32 * 20.0 + 10.0]);
328 w.on_state = Some(state.to_string());
329 w.appearance_state = Some("Off".into());
330 let wid = tree.alloc(w);
331 tree.get_mut(gid).children.push(wid);
332 }
333
334 let model = build_form_model(&tree);
335 assert_eq!(model.len(), 1, "group + 2 widgets must be ONE field");
336 let f = &model[0];
337 assert_eq!(f.name, "kleur");
338 assert_eq!(
339 f.kind,
340 FormFieldKind::RadioGroup {
341 options: vec!["Rood".into(), "Blauw".into()]
342 }
343 );
344 assert_eq!(f.widgets.len(), 2);
345 assert_eq!(f.widgets[1].on_state.as_deref(), Some("Blauw"));
346 }
347
348 #[test]
349 fn comb_and_multiline_flags_surface_in_kind() {
350 let mut tree = FieldTree::new();
351 let mut n = blank_node("bsn");
352 n.field_type = Some(FieldType::Text);
353 n.flags = FieldFlags::from_bits(1 << 24); n.max_len = Some(9);
355 n.rect = Some([0.0, 0.0, 90.0, 12.0]);
356 tree.alloc(n);
357
358 let model = build_form_model(&tree);
359 assert_eq!(model.len(), 1);
360 assert_eq!(
361 model[0].kind,
362 FormFieldKind::Text {
363 multiline: false,
364 comb: true,
365 password: false
366 }
367 );
368 assert_eq!(model[0].max_len, Some(9));
369 }
370
371 #[test]
372 fn container_with_named_children_is_not_a_field() {
373 let mut tree = FieldTree::new();
374 let mut parent = blank_node("adres");
375 parent.field_type = Some(FieldType::Text);
376 let pid = tree.alloc(parent);
377 let mut kid = blank_node("straat");
378 kid.parent = Some(pid);
379 kid.rect = Some([0.0, 0.0, 100.0, 12.0]);
380 let kid_id = tree.alloc(kid);
381 tree.get_mut(pid).children.push(kid_id);
382
383 let model = build_form_model(&tree);
384 assert_eq!(model.len(), 1);
385 assert_eq!(model[0].name, "adres.straat");
386 }
387
388 #[test]
389 fn checkbox_reports_on_state_and_checked() {
390 let mut tree = FieldTree::new();
391 let mut n = blank_node("akkoord");
392 n.field_type = Some(FieldType::Button);
393 n.rect = Some([0.0, 0.0, 12.0, 12.0]);
394 n.on_state = Some("Akkoord".into());
395 n.appearance_state = Some("Akkoord".into());
396 n.value = Some(FieldValue::Text("Akkoord".into()));
397 tree.alloc(n);
398
399 let model = build_form_model(&tree);
400 assert_eq!(
401 model[0].kind,
402 FormFieldKind::Checkbox {
403 on_state: "Akkoord".into(),
404 checked: true
405 }
406 );
407 }
408}