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