1use crate::error::{Error, Result};
10use crate::utils::cell_ref::cell_name_to_coordinates;
11
12#[derive(Debug, Clone, PartialEq)]
14pub enum FormControlType {
15 Button,
16 CheckBox,
17 OptionButton,
18 SpinButton,
19 ScrollBar,
20 GroupBox,
21 Label,
22}
23
24impl FormControlType {
25 pub fn object_type(&self) -> &str {
27 match self {
28 FormControlType::Button => "Button",
29 FormControlType::CheckBox => "Checkbox",
30 FormControlType::OptionButton => "Radio",
31 FormControlType::SpinButton => "Spin",
32 FormControlType::ScrollBar => "Scroll",
33 FormControlType::GroupBox => "GBox",
34 FormControlType::Label => "Label",
35 }
36 }
37
38 pub fn from_object_type(s: &str) -> Option<Self> {
40 match s {
41 "Button" => Some(FormControlType::Button),
42 "Checkbox" => Some(FormControlType::CheckBox),
43 "Radio" => Some(FormControlType::OptionButton),
44 "Spin" => Some(FormControlType::SpinButton),
45 "Scroll" => Some(FormControlType::ScrollBar),
46 "GBox" => Some(FormControlType::GroupBox),
47 "Label" => Some(FormControlType::Label),
48 _ => None,
49 }
50 }
51
52 pub fn parse(s: &str) -> Result<Self> {
54 match s.to_lowercase().as_str() {
55 "button" => Ok(FormControlType::Button),
56 "checkbox" | "check_box" | "check" => Ok(FormControlType::CheckBox),
57 "optionbutton" | "option_button" | "radio" | "radiobutton" | "radio_button" => {
58 Ok(FormControlType::OptionButton)
59 }
60 "spinbutton" | "spin_button" | "spin" | "spinner" => Ok(FormControlType::SpinButton),
61 "scrollbar" | "scroll_bar" | "scroll" => Ok(FormControlType::ScrollBar),
62 "groupbox" | "group_box" | "group" => Ok(FormControlType::GroupBox),
63 "label" => Ok(FormControlType::Label),
64 _ => Err(Error::InvalidArgument(format!(
65 "unknown form control type: {s}"
66 ))),
67 }
68 }
69}
70
71#[derive(Debug, Clone)]
73pub struct FormControlConfig {
74 pub control_type: FormControlType,
76 pub cell: String,
78 pub width: Option<f64>,
80 pub height: Option<f64>,
82 pub text: Option<String>,
84 pub macro_name: Option<String>,
86 pub cell_link: Option<String>,
88 pub checked: Option<bool>,
90 pub min_value: Option<u32>,
92 pub max_value: Option<u32>,
94 pub increment: Option<u32>,
96 pub page_increment: Option<u32>,
98 pub current_value: Option<u32>,
100 pub three_d: Option<bool>,
102}
103
104impl FormControlConfig {
105 pub fn button(cell: &str, text: &str) -> Self {
107 Self {
108 control_type: FormControlType::Button,
109 cell: cell.to_string(),
110 width: None,
111 height: None,
112 text: Some(text.to_string()),
113 macro_name: None,
114 cell_link: None,
115 checked: None,
116 min_value: None,
117 max_value: None,
118 increment: None,
119 page_increment: None,
120 current_value: None,
121 three_d: None,
122 }
123 }
124
125 pub fn checkbox(cell: &str, text: &str) -> Self {
127 Self {
128 control_type: FormControlType::CheckBox,
129 cell: cell.to_string(),
130 width: None,
131 height: None,
132 text: Some(text.to_string()),
133 macro_name: None,
134 cell_link: None,
135 checked: None,
136 min_value: None,
137 max_value: None,
138 increment: None,
139 page_increment: None,
140 current_value: None,
141 three_d: None,
142 }
143 }
144
145 pub fn spin_button(cell: &str, min: u32, max: u32) -> Self {
147 Self {
148 control_type: FormControlType::SpinButton,
149 cell: cell.to_string(),
150 width: None,
151 height: None,
152 text: None,
153 macro_name: None,
154 cell_link: None,
155 checked: None,
156 min_value: Some(min),
157 max_value: Some(max),
158 increment: Some(1),
159 page_increment: None,
160 current_value: Some(min),
161 three_d: None,
162 }
163 }
164
165 pub fn scroll_bar(cell: &str, min: u32, max: u32) -> Self {
167 Self {
168 control_type: FormControlType::ScrollBar,
169 cell: cell.to_string(),
170 width: None,
171 height: None,
172 text: None,
173 macro_name: None,
174 cell_link: None,
175 checked: None,
176 min_value: Some(min),
177 max_value: Some(max),
178 increment: Some(1),
179 page_increment: Some(10),
180 current_value: Some(min),
181 three_d: None,
182 }
183 }
184
185 pub fn validate(&self) -> Result<()> {
187 cell_name_to_coordinates(&self.cell)?;
188
189 if let Some(ref cl) = self.cell_link {
190 cell_name_to_coordinates(cl)?;
191 }
192
193 if let (Some(min), Some(max)) = (self.min_value, self.max_value) {
194 if min > max {
195 return Err(Error::InvalidArgument(format!(
196 "min_value ({min}) must not exceed max_value ({max})"
197 )));
198 }
199 }
200
201 if let Some(inc) = self.increment {
202 if inc == 0 {
203 return Err(Error::InvalidArgument(
204 "increment must be greater than 0".to_string(),
205 ));
206 }
207 }
208
209 if let Some(page_inc) = self.page_increment {
210 if page_inc == 0 {
211 return Err(Error::InvalidArgument(
212 "page_increment must be greater than 0".to_string(),
213 ));
214 }
215 }
216
217 Ok(())
218 }
219}
220
221#[derive(Debug, Clone, PartialEq)]
223pub struct FormControlInfo {
224 pub control_type: FormControlType,
225 pub cell: String,
226 pub text: Option<String>,
227 pub macro_name: Option<String>,
228 pub cell_link: Option<String>,
229 pub checked: Option<bool>,
230 pub current_value: Option<u32>,
231 pub min_value: Option<u32>,
232 pub max_value: Option<u32>,
233 pub increment: Option<u32>,
234 pub page_increment: Option<u32>,
235}
236
237fn default_dimensions(ct: &FormControlType) -> (f64, f64) {
239 match ct {
240 FormControlType::Button => (72.0, 24.0),
241 FormControlType::CheckBox => (72.0, 18.0),
242 FormControlType::OptionButton => (72.0, 18.0),
243 FormControlType::SpinButton => (15.75, 30.0),
244 FormControlType::ScrollBar => (15.75, 60.0),
245 FormControlType::GroupBox => (120.0, 72.0),
246 FormControlType::Label => (72.0, 18.0),
247 }
248}
249
250const FORM_CONTROL_SHAPETYPE_ID: &str = "_x0000_t201";
252
253fn build_control_anchor(cell: &str, width_pt: f64, height_pt: f64) -> Result<String> {
258 let (col, row) = cell_name_to_coordinates(cell)?;
259 let col0 = col - 1;
260 let row0 = row - 1;
261
262 let col_span = ((width_pt / 48.0).ceil() as u32).max(1);
265 let row_span = ((height_pt / 15.0).ceil() as u32).max(1);
266
267 let col2 = col0 + col_span;
268 let row2 = row0 + row_span;
269
270 Ok(format!("{col0}, 15, {row0}, 10, {col2}, 63, {row2}, 24"))
271}
272
273pub fn build_form_control_vml(controls: &[FormControlConfig], start_shape_id: usize) -> String {
279 use std::fmt::Write;
280
281 let mut shapes = String::new();
282 for (i, config) in controls.iter().enumerate() {
283 let shape_id = start_shape_id + i;
284 let (default_w, default_h) = default_dimensions(&config.control_type);
285 let width = config.width.unwrap_or(default_w);
286 let height = config.height.unwrap_or(default_h);
287
288 let anchor = match build_control_anchor(&config.cell, width, height) {
292 Ok(a) => a,
293 Err(_) => {
294 #[cfg(debug_assertions)]
295 eprintln!(
296 "warning: skipping form control with invalid cell ref '{}'",
297 config.cell
298 );
299 continue;
300 }
301 };
302
303 write_form_control_shape(&mut shapes, shape_id, i + 1, &anchor, config);
304 }
305
306 let mut doc = String::with_capacity(1024 + shapes.len());
307 doc.push_str("<xml xmlns:v=\"urn:schemas-microsoft-com:vml\"");
308 doc.push_str(" xmlns:o=\"urn:schemas-microsoft-com:office:office\"");
309 doc.push_str(" xmlns:x=\"urn:schemas-microsoft-com:office:excel\">\n");
310 doc.push_str(" <o:shapelayout v:ext=\"edit\">\n");
311 doc.push_str(" <o:idmap v:ext=\"edit\" data=\"1\"/>\n");
312 doc.push_str(" </o:shapelayout>\n");
313
314 let _ = write!(
316 doc,
317 " <v:shapetype id=\"{}\" coordsize=\"21600,21600\" o:spt=\"201\" \
318 path=\"m,l,21600r21600,l21600,xe\">\n\
319 \x20 <v:stroke joinstyle=\"miter\"/>\n\
320 \x20 <v:path gradientshapeok=\"t\" o:connecttype=\"rect\"/>\n\
321 </v:shapetype>\n",
322 FORM_CONTROL_SHAPETYPE_ID,
323 );
324
325 doc.push_str(&shapes);
326 doc.push_str("</xml>\n");
327 doc
328}
329
330fn write_form_control_shape(
332 out: &mut String,
333 shape_id: usize,
334 z_index: usize,
335 anchor: &str,
336 config: &FormControlConfig,
337) {
338 use std::fmt::Write;
339
340 let _ = write!(out, " <v:shape id=\"_x0000_s{shape_id}\"");
341 let _ = write!(out, " type=\"#{FORM_CONTROL_SHAPETYPE_ID}\"");
342 let _ = write!(
343 out,
344 " style=\"position:absolute;z-index:{z_index};visibility:visible\""
345 );
346
347 match config.control_type {
348 FormControlType::Button => {
349 out.push_str(" fillcolor=\"buttonFace\" o:insetmode=\"auto\">\n");
350 out.push_str(" <v:fill color2=\"buttonFace\" o:detectmouseclick=\"t\"/>\n");
351 out.push_str(" <o:lock v:ext=\"edit\" rotation=\"t\"/>\n");
352 if let Some(ref text) = config.text {
353 let _ = write!(
354 out,
355 " <v:textbox>\n\
356 \x20 <div style=\"text-align:center\">\
357 <font face=\"Calibri\" size=\"220\" color=\"#000000\">{text}</font>\
358 </div>\n\
359 \x20 </v:textbox>\n"
360 );
361 }
362 }
363 FormControlType::CheckBox | FormControlType::OptionButton => {
364 out.push_str(" fillcolor=\"window\" o:insetmode=\"auto\">\n");
365 out.push_str(" <v:fill color2=\"window\"/>\n");
366 if let Some(ref text) = config.text {
367 let _ = write!(
368 out,
369 " <v:textbox>\n\
370 \x20 <div>{text}</div>\n\
371 \x20 </v:textbox>\n"
372 );
373 }
374 }
375 FormControlType::SpinButton | FormControlType::ScrollBar => {
376 out.push_str(" fillcolor=\"buttonFace\" o:insetmode=\"auto\">\n");
377 out.push_str(" <v:fill color2=\"buttonFace\"/>\n");
378 }
379 FormControlType::GroupBox => {
380 out.push_str(" filled=\"f\" stroked=\"f\" o:insetmode=\"auto\">\n");
381 if let Some(ref text) = config.text {
382 let _ = write!(
383 out,
384 " <v:textbox>\n\
385 \x20 <div>{text}</div>\n\
386 \x20 </v:textbox>\n"
387 );
388 }
389 }
390 FormControlType::Label => {
391 out.push_str(" filled=\"f\" stroked=\"f\" o:insetmode=\"auto\">\n");
392 if let Some(ref text) = config.text {
393 let _ = write!(
394 out,
395 " <v:textbox>\n\
396 \x20 <div>{text}</div>\n\
397 \x20 </v:textbox>\n"
398 );
399 }
400 }
401 }
402
403 let object_type = config.control_type.object_type();
405 let _ = writeln!(out, " <x:ClientData ObjectType=\"{object_type}\">");
406 let _ = writeln!(out, " <x:Anchor>{anchor}</x:Anchor>");
407 out.push_str(" <x:PrintObject>False</x:PrintObject>\n");
408 out.push_str(" <x:AutoFill>False</x:AutoFill>\n");
409
410 if let Some(ref macro_name) = config.macro_name {
411 let _ = writeln!(out, " <x:FmlaMacro>{macro_name}</x:FmlaMacro>");
412 }
413
414 if let Some(ref cell_link) = config.cell_link {
415 let _ = writeln!(out, " <x:FmlaLink>{cell_link}</x:FmlaLink>");
416 }
417
418 if let Some(checked) = config.checked {
419 let val = if checked { 1 } else { 0 };
420 let _ = writeln!(out, " <x:Checked>{val}</x:Checked>");
421 }
422
423 if let Some(val) = config.current_value {
424 let _ = writeln!(out, " <x:Val>{val}</x:Val>");
425 }
426
427 if let Some(min) = config.min_value {
428 let _ = writeln!(out, " <x:Min>{min}</x:Min>");
429 }
430
431 if let Some(max) = config.max_value {
432 let _ = writeln!(out, " <x:Max>{max}</x:Max>");
433 }
434
435 if let Some(inc) = config.increment {
436 let _ = writeln!(out, " <x:Inc>{inc}</x:Inc>");
437 }
438
439 if let Some(page_inc) = config.page_increment {
440 let _ = writeln!(out, " <x:Page>{page_inc}</x:Page>");
441 }
442
443 let three_d = config.three_d.unwrap_or(true);
445 if !three_d {
446 out.push_str(" <x:NoThreeD/>\n");
447 }
448
449 out.push_str(" </x:ClientData>\n");
450 out.push_str(" </v:shape>\n");
451}
452
453pub fn parse_form_controls(vml_xml: &str) -> Vec<FormControlInfo> {
458 let mut controls = Vec::new();
459
460 let mut search_from = 0;
461 while let Some(shape_start) = vml_xml[search_from..].find("<v:shape ") {
462 let abs_start = search_from + shape_start;
463 let shape_end = match vml_xml[abs_start..].find("</v:shape>") {
464 Some(pos) => abs_start + pos + "</v:shape>".len(),
465 None => break,
466 };
467 let shape_xml = &vml_xml[abs_start..shape_end];
468
469 if let Some(info) = parse_single_control(shape_xml) {
471 controls.push(info);
472 }
473 search_from = shape_end;
474 }
475
476 controls
477}
478
479fn parse_single_control(shape_xml: &str) -> Option<FormControlInfo> {
481 let cd_start = shape_xml.find("<x:ClientData ")?;
483 let cd_end = shape_xml
484 .find("</x:ClientData>")
485 .map(|p| p + "</x:ClientData>".len())?;
486 let cd_xml = &shape_xml[cd_start..cd_end];
487
488 let obj_type = extract_attr(cd_xml, "ObjectType")?;
490 let control_type = FormControlType::from_object_type(&obj_type)?;
491
492 if obj_type == "Note" {
494 return None;
495 }
496
497 let cell = extract_anchor_cell(cd_xml).unwrap_or_default();
498 let text = extract_textbox_text(shape_xml);
499 let macro_name = extract_element(cd_xml, "x:FmlaMacro");
500 let cell_link = extract_element(cd_xml, "x:FmlaLink");
501 let checked = extract_element(cd_xml, "x:Checked").and_then(|v| match v.as_str() {
502 "1" => Some(true),
503 "0" => Some(false),
504 _ => None,
505 });
506 let current_value = extract_element(cd_xml, "x:Val").and_then(|v| v.parse().ok());
507 let min_value = extract_element(cd_xml, "x:Min").and_then(|v| v.parse().ok());
508 let max_value = extract_element(cd_xml, "x:Max").and_then(|v| v.parse().ok());
509 let increment = extract_element(cd_xml, "x:Inc").and_then(|v| v.parse().ok());
510 let page_increment = extract_element(cd_xml, "x:Page").and_then(|v| v.parse().ok());
511
512 Some(FormControlInfo {
513 control_type,
514 cell,
515 text,
516 macro_name,
517 cell_link,
518 checked,
519 current_value,
520 min_value,
521 max_value,
522 increment,
523 page_increment,
524 })
525}
526
527fn extract_attr(xml: &str, attr: &str) -> Option<String> {
529 let pattern = format!("{attr}=\"");
530 let start = xml.find(&pattern)?;
531 let val_start = start + pattern.len();
532 let end = xml[val_start..].find('"')?;
533 Some(xml[val_start..val_start + end].to_string())
534}
535
536fn extract_element(xml: &str, tag: &str) -> Option<String> {
538 let open = format!("<{tag}>");
539 let close = format!("</{tag}>");
540 let start = xml.find(&open)?;
541 let content_start = start + open.len();
542 let end = xml[content_start..].find(&close)?;
543 let text = xml[content_start..content_start + end].trim().to_string();
544 if text.is_empty() {
545 None
546 } else {
547 Some(text)
548 }
549}
550
551fn extract_textbox_text(shape_xml: &str) -> Option<String> {
553 let tb_start = shape_xml.find("<v:textbox>")?;
554 let tb_end = shape_xml.find("</v:textbox>")?;
555 let tb_content = &shape_xml[tb_start + "<v:textbox>".len()..tb_end];
556
557 let mut text = String::new();
559 let mut in_tag = false;
560 for ch in tb_content.chars() {
561 match ch {
562 '<' => in_tag = true,
563 '>' => in_tag = false,
564 _ if !in_tag => text.push(ch),
565 _ => {}
566 }
567 }
568 let trimmed = text.trim().to_string();
569 if trimmed.is_empty() {
570 None
571 } else {
572 Some(trimmed)
573 }
574}
575
576fn extract_anchor_cell(cd_xml: &str) -> Option<String> {
581 let anchor_text = extract_element(cd_xml, "x:Anchor")?;
582 let parts: Vec<&str> = anchor_text.split(',').map(|s| s.trim()).collect();
583 if parts.len() < 4 {
584 return None;
585 }
586 let col0: u32 = parts[0].parse().ok()?;
587 let row0: u32 = parts[2].parse().ok()?;
588 crate::utils::cell_ref::coordinates_to_cell_name(col0 + 1, row0 + 1).ok()
589}
590
591pub fn merge_vml_controls(
597 existing_vml: &[u8],
598 controls: &[FormControlConfig],
599 start_shape_id: usize,
600) -> Vec<u8> {
601 let existing_str = String::from_utf8_lossy(existing_vml);
602
603 let mut shapes = String::new();
605 for (i, config) in controls.iter().enumerate() {
606 let shape_id = start_shape_id + i;
607 let (default_w, default_h) = default_dimensions(&config.control_type);
608 let width = config.width.unwrap_or(default_w);
609 let height = config.height.unwrap_or(default_h);
610
611 if let Ok(anchor) = build_control_anchor(&config.cell, width, height) {
612 write_form_control_shape(&mut shapes, shape_id, shape_id, &anchor, config);
613 }
614 }
615
616 let shapetype_exists = existing_str.contains(FORM_CONTROL_SHAPETYPE_ID);
618
619 let shapetype_xml = if !shapetype_exists {
620 format!(
621 " <v:shapetype id=\"{FORM_CONTROL_SHAPETYPE_ID}\" coordsize=\"21600,21600\" \
622 o:spt=\"201\" path=\"m,l,21600r21600,l21600,xe\">\n\
623 \x20 <v:stroke joinstyle=\"miter\"/>\n\
624 \x20 <v:path gradientshapeok=\"t\" o:connecttype=\"rect\"/>\n\
625 </v:shapetype>\n"
626 )
627 } else {
628 String::new()
629 };
630
631 if let Some(close_pos) = existing_str.rfind("</xml>") {
633 let mut result =
634 String::with_capacity(existing_str.len() + shapetype_xml.len() + shapes.len());
635 result.push_str(&existing_str[..close_pos]);
636 result.push_str(&shapetype_xml);
637 result.push_str(&shapes);
638 result.push_str("</xml>\n");
639 result.into_bytes()
640 } else {
641 build_form_control_vml(controls, start_shape_id).into_bytes()
643 }
644}
645
646impl FormControlInfo {
647 pub fn to_config(&self) -> FormControlConfig {
652 FormControlConfig {
653 control_type: self.control_type.clone(),
654 cell: self.cell.clone(),
655 width: None,
656 height: None,
657 text: self.text.clone(),
658 macro_name: self.macro_name.clone(),
659 cell_link: self.cell_link.clone(),
660 checked: self.checked,
661 min_value: self.min_value,
662 max_value: self.max_value,
663 increment: self.increment,
664 page_increment: self.page_increment,
665 current_value: self.current_value,
666 three_d: None,
667 }
668 }
669}
670
671pub fn count_vml_shapes(vml_bytes: &[u8]) -> usize {
673 let vml_str = String::from_utf8_lossy(vml_bytes);
674 vml_str.matches("<v:shape ").count()
675}
676
677pub fn strip_form_control_shapes_from_vml(vml_bytes: &[u8]) -> Option<Vec<u8>> {
682 let vml_str = String::from_utf8_lossy(vml_bytes);
683
684 let mut keep_shapes = Vec::new();
686 let mut has_comment_shapes = false;
687 let mut search_from = 0;
688
689 while let Some(shape_start) = vml_str[search_from..].find("<v:shape ") {
690 let abs_start = search_from + shape_start;
691 let shape_end = match vml_str[abs_start..].find("</v:shape>") {
692 Some(pos) => abs_start + pos + "</v:shape>".len(),
693 None => break,
694 };
695 let shape_xml = &vml_str[abs_start..shape_end];
696
697 if shape_xml.contains("ObjectType=\"Note\"") {
699 keep_shapes.push((abs_start, shape_end));
700 has_comment_shapes = true;
701 }
702 search_from = shape_end;
704 }
705
706 if !has_comment_shapes {
707 return None;
708 }
709
710 let first_shape_pos = vml_str.find("<v:shape ").unwrap_or(vml_str.len());
713 let header = &vml_str[..first_shape_pos];
714
715 let header = remove_shapetype_block(header, FORM_CONTROL_SHAPETYPE_ID);
718
719 let mut result = String::with_capacity(vml_str.len());
720 result.push_str(&header);
721
722 for (start, end) in &keep_shapes {
723 result.push_str(&vml_str[*start..*end]);
724 result.push('\n');
725 }
726
727 result.push_str("</xml>\n");
728 Some(result.into_bytes())
729}
730
731fn remove_shapetype_block(header: &str, shapetype_id: &str) -> String {
733 if let Some(st_start) = header.find(&format!("<v:shapetype id=\"{shapetype_id}\"")) {
734 let st_end_tag = "</v:shapetype>";
735 if let Some(rel_end) = header[st_start..].find(st_end_tag) {
736 let st_end = st_start + rel_end + st_end_tag.len();
737 let st_end = if header.as_bytes().get(st_end) == Some(&b'\n') {
739 st_end + 1
740 } else {
741 st_end
742 };
743 let mut cleaned = String::with_capacity(header.len());
744 cleaned.push_str(&header[..st_start]);
745 cleaned.push_str(&header[st_end..]);
746 return cleaned;
747 }
748 }
749 header.to_string()
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 #[test]
757 fn test_form_control_type_parse() {
758 assert_eq!(
759 FormControlType::parse("button").unwrap(),
760 FormControlType::Button
761 );
762 assert_eq!(
763 FormControlType::parse("Button").unwrap(),
764 FormControlType::Button
765 );
766 assert_eq!(
767 FormControlType::parse("checkbox").unwrap(),
768 FormControlType::CheckBox
769 );
770 assert_eq!(
771 FormControlType::parse("check_box").unwrap(),
772 FormControlType::CheckBox
773 );
774 assert_eq!(
775 FormControlType::parse("radio").unwrap(),
776 FormControlType::OptionButton
777 );
778 assert_eq!(
779 FormControlType::parse("optionButton").unwrap(),
780 FormControlType::OptionButton
781 );
782 assert_eq!(
783 FormControlType::parse("spin").unwrap(),
784 FormControlType::SpinButton
785 );
786 assert_eq!(
787 FormControlType::parse("spinner").unwrap(),
788 FormControlType::SpinButton
789 );
790 assert_eq!(
791 FormControlType::parse("scroll").unwrap(),
792 FormControlType::ScrollBar
793 );
794 assert_eq!(
795 FormControlType::parse("scrollbar").unwrap(),
796 FormControlType::ScrollBar
797 );
798 assert_eq!(
799 FormControlType::parse("group").unwrap(),
800 FormControlType::GroupBox
801 );
802 assert_eq!(
803 FormControlType::parse("groupbox").unwrap(),
804 FormControlType::GroupBox
805 );
806 assert_eq!(
807 FormControlType::parse("label").unwrap(),
808 FormControlType::Label
809 );
810 assert!(FormControlType::parse("unknown").is_err());
811 }
812
813 #[test]
814 fn test_form_control_type_object_type() {
815 assert_eq!(FormControlType::Button.object_type(), "Button");
816 assert_eq!(FormControlType::CheckBox.object_type(), "Checkbox");
817 assert_eq!(FormControlType::OptionButton.object_type(), "Radio");
818 assert_eq!(FormControlType::SpinButton.object_type(), "Spin");
819 assert_eq!(FormControlType::ScrollBar.object_type(), "Scroll");
820 assert_eq!(FormControlType::GroupBox.object_type(), "GBox");
821 assert_eq!(FormControlType::Label.object_type(), "Label");
822 }
823
824 #[test]
825 fn test_form_control_type_roundtrip() {
826 let types = vec![
827 FormControlType::Button,
828 FormControlType::CheckBox,
829 FormControlType::OptionButton,
830 FormControlType::SpinButton,
831 FormControlType::ScrollBar,
832 FormControlType::GroupBox,
833 FormControlType::Label,
834 ];
835 for ct in types {
836 let obj_type = ct.object_type();
837 let parsed = FormControlType::from_object_type(obj_type).unwrap();
838 assert_eq!(parsed, ct);
839 }
840 }
841
842 #[test]
843 fn test_validate_config_valid() {
844 let config = FormControlConfig::button("A1", "Click Me");
845 assert!(config.validate().is_ok());
846 }
847
848 #[test]
849 fn test_validate_config_invalid_cell() {
850 let mut config = FormControlConfig::button("INVALID", "Click Me");
851 config.cell = "ZZZZZ".to_string();
852 assert!(config.validate().is_err());
853 }
854
855 #[test]
856 fn test_validate_config_min_exceeds_max() {
857 let mut config = FormControlConfig::spin_button("A1", 0, 100);
858 config.min_value = Some(200);
859 config.max_value = Some(100);
860 assert!(config.validate().is_err());
861 }
862
863 #[test]
864 fn test_validate_config_zero_increment() {
865 let mut config = FormControlConfig::spin_button("A1", 0, 100);
866 config.increment = Some(0);
867 assert!(config.validate().is_err());
868 }
869
870 #[test]
871 fn test_validate_config_zero_page_increment() {
872 let mut config = FormControlConfig::scroll_bar("A1", 0, 100);
873 config.page_increment = Some(0);
874 assert!(config.validate().is_err());
875 }
876
877 #[test]
878 fn test_validate_config_invalid_cell_link() {
879 let mut config = FormControlConfig::checkbox("A1", "Check");
880 config.cell_link = Some("NOT_A_CELL".to_string());
881 assert!(config.validate().is_err());
882 }
883
884 #[test]
885 fn test_build_button_vml() {
886 let config = FormControlConfig::button("B2", "Click Me");
887 let vml = build_form_control_vml(&[config], 1025);
888
889 assert!(vml.contains("xmlns:v=\"urn:schemas-microsoft-com:vml\""));
890 assert!(vml.contains("xmlns:o=\"urn:schemas-microsoft-com:office:office\""));
891 assert!(vml.contains("xmlns:x=\"urn:schemas-microsoft-com:office:excel\""));
892 assert!(vml.contains("ObjectType=\"Button\""));
893 assert!(vml.contains("Click Me"));
894 assert!(vml.contains("_x0000_s1025"));
895 assert!(vml.contains("_x0000_t201"));
896 assert!(vml.contains("fillcolor=\"buttonFace\""));
897 }
898
899 #[test]
900 fn test_build_checkbox_vml() {
901 let mut config = FormControlConfig::checkbox("A1", "Enable Feature");
902 config.cell_link = Some("$C$1".to_string());
903 config.checked = Some(true);
904
905 let vml = build_form_control_vml(&[config], 1025);
906 assert!(vml.contains("ObjectType=\"Checkbox\""));
907 assert!(vml.contains("Enable Feature"));
908 assert!(vml.contains("<x:FmlaLink>$C$1</x:FmlaLink>"));
909 assert!(vml.contains("<x:Checked>1</x:Checked>"));
910 }
911
912 #[test]
913 fn test_build_option_button_vml() {
914 let config = FormControlConfig {
915 control_type: FormControlType::OptionButton,
916 cell: "A3".to_string(),
917 width: None,
918 height: None,
919 text: Some("Option A".to_string()),
920 macro_name: None,
921 cell_link: Some("$D$1".to_string()),
922 checked: Some(false),
923 min_value: None,
924 max_value: None,
925 increment: None,
926 page_increment: None,
927 current_value: None,
928 three_d: None,
929 };
930
931 let vml = build_form_control_vml(&[config], 1025);
932 assert!(vml.contains("ObjectType=\"Radio\""));
933 assert!(vml.contains("Option A"));
934 assert!(vml.contains("<x:FmlaLink>$D$1</x:FmlaLink>"));
935 assert!(vml.contains("<x:Checked>0</x:Checked>"));
936 }
937
938 #[test]
939 fn test_build_spin_button_vml() {
940 let config = FormControlConfig::spin_button("E1", 0, 100);
941 let vml = build_form_control_vml(&[config], 1025);
942
943 assert!(vml.contains("ObjectType=\"Spin\""));
944 assert!(vml.contains("<x:Min>0</x:Min>"));
945 assert!(vml.contains("<x:Max>100</x:Max>"));
946 assert!(vml.contains("<x:Inc>1</x:Inc>"));
947 assert!(vml.contains("<x:Val>0</x:Val>"));
948 }
949
950 #[test]
951 fn test_build_scroll_bar_vml() {
952 let config = FormControlConfig::scroll_bar("F1", 10, 200);
953 let vml = build_form_control_vml(&[config], 1025);
954
955 assert!(vml.contains("ObjectType=\"Scroll\""));
956 assert!(vml.contains("<x:Min>10</x:Min>"));
957 assert!(vml.contains("<x:Max>200</x:Max>"));
958 assert!(vml.contains("<x:Inc>1</x:Inc>"));
959 assert!(vml.contains("<x:Page>10</x:Page>"));
960 }
961
962 #[test]
963 fn test_build_group_box_vml() {
964 let config = FormControlConfig {
965 control_type: FormControlType::GroupBox,
966 cell: "A1".to_string(),
967 width: None,
968 height: None,
969 text: Some("Options".to_string()),
970 macro_name: None,
971 cell_link: None,
972 checked: None,
973 min_value: None,
974 max_value: None,
975 increment: None,
976 page_increment: None,
977 current_value: None,
978 three_d: None,
979 };
980
981 let vml = build_form_control_vml(&[config], 1025);
982 assert!(vml.contains("ObjectType=\"GBox\""));
983 assert!(vml.contains("Options"));
984 assert!(vml.contains("filled=\"f\""));
985 }
986
987 #[test]
988 fn test_build_label_vml() {
989 let config = FormControlConfig {
990 control_type: FormControlType::Label,
991 cell: "A1".to_string(),
992 width: None,
993 height: None,
994 text: Some("Status:".to_string()),
995 macro_name: None,
996 cell_link: None,
997 checked: None,
998 min_value: None,
999 max_value: None,
1000 increment: None,
1001 page_increment: None,
1002 current_value: None,
1003 three_d: None,
1004 };
1005
1006 let vml = build_form_control_vml(&[config], 1025);
1007 assert!(vml.contains("ObjectType=\"Label\""));
1008 assert!(vml.contains("Status:"));
1009 }
1010
1011 #[test]
1012 fn test_build_button_with_macro() {
1013 let mut config = FormControlConfig::button("A1", "Run Macro");
1014 config.macro_name = Some("Sheet1.MyMacro".to_string());
1015
1016 let vml = build_form_control_vml(&[config], 1025);
1017 assert!(vml.contains("<x:FmlaMacro>Sheet1.MyMacro</x:FmlaMacro>"));
1018 }
1019
1020 #[test]
1021 fn test_build_control_no_three_d() {
1022 let mut config = FormControlConfig::checkbox("A1", "Flat");
1023 config.three_d = Some(false);
1024
1025 let vml = build_form_control_vml(&[config], 1025);
1026 assert!(vml.contains("<x:NoThreeD/>"));
1027 }
1028
1029 #[test]
1030 fn test_build_multiple_controls() {
1031 let controls = vec![
1032 FormControlConfig::button("A1", "Button 1"),
1033 FormControlConfig::checkbox("A3", "Check 1"),
1034 FormControlConfig::spin_button("C1", 0, 50),
1035 ];
1036
1037 let vml = build_form_control_vml(&controls, 1025);
1038 assert!(vml.contains("_x0000_s1025"));
1039 assert!(vml.contains("_x0000_s1026"));
1040 assert!(vml.contains("_x0000_s1027"));
1041 assert!(vml.contains("ObjectType=\"Button\""));
1042 assert!(vml.contains("ObjectType=\"Checkbox\""));
1043 assert!(vml.contains("ObjectType=\"Spin\""));
1044 }
1045
1046 #[test]
1047 fn test_parse_form_controls_button() {
1048 let config = FormControlConfig::button("B2", "Click Me");
1049 let vml = build_form_control_vml(&[config], 1025);
1050
1051 let controls = parse_form_controls(&vml);
1052 assert_eq!(controls.len(), 1);
1053 assert_eq!(controls[0].control_type, FormControlType::Button);
1054 assert_eq!(controls[0].text.as_deref(), Some("Click Me"));
1055 }
1056
1057 #[test]
1058 fn test_parse_form_controls_checkbox_with_link() {
1059 let mut config = FormControlConfig::checkbox("A1", "Toggle");
1060 config.cell_link = Some("$D$1".to_string());
1061 config.checked = Some(true);
1062 let vml = build_form_control_vml(&[config], 1025);
1063
1064 let controls = parse_form_controls(&vml);
1065 assert_eq!(controls.len(), 1);
1066 assert_eq!(controls[0].control_type, FormControlType::CheckBox);
1067 assert_eq!(controls[0].text.as_deref(), Some("Toggle"));
1068 assert_eq!(controls[0].cell_link.as_deref(), Some("$D$1"));
1069 assert_eq!(controls[0].checked, Some(true));
1070 }
1071
1072 #[test]
1073 fn test_parse_form_controls_spin_button() {
1074 let config = FormControlConfig::spin_button("C1", 5, 50);
1075 let vml = build_form_control_vml(&[config], 1025);
1076
1077 let controls = parse_form_controls(&vml);
1078 assert_eq!(controls.len(), 1);
1079 assert_eq!(controls[0].control_type, FormControlType::SpinButton);
1080 assert_eq!(controls[0].min_value, Some(5));
1081 assert_eq!(controls[0].max_value, Some(50));
1082 assert_eq!(controls[0].increment, Some(1));
1083 assert_eq!(controls[0].current_value, Some(5));
1084 }
1085
1086 #[test]
1087 fn test_parse_form_controls_scroll_bar() {
1088 let config = FormControlConfig::scroll_bar("E1", 0, 100);
1089 let vml = build_form_control_vml(&[config], 1025);
1090
1091 let controls = parse_form_controls(&vml);
1092 assert_eq!(controls.len(), 1);
1093 assert_eq!(controls[0].control_type, FormControlType::ScrollBar);
1094 assert_eq!(controls[0].page_increment, Some(10));
1095 }
1096
1097 #[test]
1098 fn test_parse_multiple_controls() {
1099 let controls = vec![
1100 FormControlConfig::button("A1", "Btn"),
1101 FormControlConfig::checkbox("A3", "Chk"),
1102 FormControlConfig::spin_button("C1", 0, 10),
1103 ];
1104 let vml = build_form_control_vml(&controls, 1025);
1105
1106 let parsed = parse_form_controls(&vml);
1107 assert_eq!(parsed.len(), 3);
1108 assert_eq!(parsed[0].control_type, FormControlType::Button);
1109 assert_eq!(parsed[1].control_type, FormControlType::CheckBox);
1110 assert_eq!(parsed[2].control_type, FormControlType::SpinButton);
1111 }
1112
1113 #[test]
1114 fn test_parse_ignores_comment_shapes() {
1115 let comment_vml = crate::vml::build_vml_drawing(&["A1"]);
1117 let controls = parse_form_controls(&comment_vml);
1118 assert!(controls.is_empty(), "comment shapes should be ignored");
1119 }
1120
1121 #[test]
1122 fn test_count_vml_shapes() {
1123 let vml = build_form_control_vml(
1124 &[
1125 FormControlConfig::button("A1", "B1"),
1126 FormControlConfig::checkbox("A3", "C1"),
1127 ],
1128 1025,
1129 );
1130 assert_eq!(count_vml_shapes(vml.as_bytes()), 2);
1131 }
1132
1133 #[test]
1134 fn test_merge_vml_controls() {
1135 let existing = build_form_control_vml(&[FormControlConfig::button("A1", "First")], 1025);
1136 let new_controls = vec![FormControlConfig::checkbox("A3", "Second")];
1137 let merged = merge_vml_controls(existing.as_bytes(), &new_controls, 1026);
1138 let merged_str = String::from_utf8(merged).unwrap();
1139
1140 assert!(merged_str.contains("_x0000_s1025"));
1141 assert!(merged_str.contains("_x0000_s1026"));
1142 assert!(merged_str.contains("ObjectType=\"Button\""));
1143 assert!(merged_str.contains("ObjectType=\"Checkbox\""));
1144 let shapetype_count = merged_str.matches(FORM_CONTROL_SHAPETYPE_ID).count();
1146 assert!(shapetype_count >= 2);
1148 }
1149
1150 #[test]
1151 fn test_build_control_anchor_basic() {
1152 let anchor = build_control_anchor("A1", 72.0, 24.0).unwrap();
1153 let parts: Vec<&str> = anchor.split(", ").collect();
1154 assert_eq!(parts.len(), 8);
1155 assert_eq!(parts[0], "0"); assert_eq!(parts[2], "0"); }
1158
1159 #[test]
1160 fn test_build_control_anchor_offset_cell() {
1161 let anchor = build_control_anchor("C5", 72.0, 30.0).unwrap();
1162 let parts: Vec<&str> = anchor.split(", ").collect();
1163 assert_eq!(parts[0], "2"); assert_eq!(parts[2], "4"); }
1166
1167 #[test]
1168 fn test_build_control_anchor_invalid_cell() {
1169 assert!(build_control_anchor("INVALID", 72.0, 24.0).is_err());
1170 }
1171
1172 #[test]
1173 fn test_default_dimensions() {
1174 let (w, h) = default_dimensions(&FormControlType::Button);
1175 assert_eq!(w, 72.0);
1176 assert_eq!(h, 24.0);
1177
1178 let (w, h) = default_dimensions(&FormControlType::ScrollBar);
1179 assert_eq!(w, 15.75);
1180 assert_eq!(h, 60.0);
1181 }
1182
1183 #[test]
1184 fn test_extract_anchor_cell() {
1185 let cd = "<x:ClientData ObjectType=\"Button\"><x:Anchor>1, 15, 0, 10, 3, 63, 2, 24</x:Anchor></x:ClientData>";
1186 let cell = extract_anchor_cell(cd).unwrap();
1187 assert_eq!(cell, "B1");
1188 }
1189
1190 #[test]
1191 fn test_custom_dimensions() {
1192 let mut config = FormControlConfig::button("A1", "Wide");
1193 config.width = Some(200.0);
1194 config.height = Some(50.0);
1195 let vml = build_form_control_vml(&[config], 1025);
1196 assert!(vml.contains("_x0000_s1025"));
1197 }
1198
1199 #[test]
1200 fn test_workbook_add_form_control() {
1201 use crate::workbook::Workbook;
1202
1203 let mut wb = Workbook::new();
1204 let config = FormControlConfig::button("B2", "Click Me");
1205 wb.add_form_control("Sheet1", config).unwrap();
1206
1207 let controls = wb.get_form_controls("Sheet1").unwrap();
1208 assert_eq!(controls.len(), 1);
1209 assert_eq!(controls[0].control_type, FormControlType::Button);
1210 assert_eq!(controls[0].text.as_deref(), Some("Click Me"));
1211 }
1212
1213 #[test]
1214 fn test_workbook_add_multiple_form_controls() {
1215 use crate::workbook::Workbook;
1216
1217 let mut wb = Workbook::new();
1218 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Button 1"))
1219 .unwrap();
1220 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Check 1"))
1221 .unwrap();
1222 wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 100))
1223 .unwrap();
1224
1225 let controls = wb.get_form_controls("Sheet1").unwrap();
1226 assert_eq!(controls.len(), 3);
1227 assert_eq!(controls[0].control_type, FormControlType::Button);
1228 assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1229 assert_eq!(controls[2].control_type, FormControlType::SpinButton);
1230 }
1231
1232 #[test]
1233 fn test_workbook_add_form_control_sheet_not_found() {
1234 use crate::workbook::Workbook;
1235
1236 let mut wb = Workbook::new();
1237 let config = FormControlConfig::button("A1", "Test");
1238 let result = wb.add_form_control("NoSheet", config);
1239 assert!(result.is_err());
1240 }
1241
1242 #[test]
1243 fn test_workbook_add_form_control_invalid_cell() {
1244 use crate::workbook::Workbook;
1245
1246 let mut wb = Workbook::new();
1247 let config = FormControlConfig {
1248 control_type: FormControlType::Button,
1249 cell: "INVALID".to_string(),
1250 width: None,
1251 height: None,
1252 text: Some("Test".to_string()),
1253 macro_name: None,
1254 cell_link: None,
1255 checked: None,
1256 min_value: None,
1257 max_value: None,
1258 increment: None,
1259 page_increment: None,
1260 current_value: None,
1261 three_d: None,
1262 };
1263 let result = wb.add_form_control("Sheet1", config);
1264 assert!(result.is_err());
1265 }
1266
1267 #[test]
1268 fn test_workbook_delete_form_control() {
1269 use crate::workbook::Workbook;
1270
1271 let mut wb = Workbook::new();
1272 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn 1"))
1273 .unwrap();
1274 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Chk 1"))
1275 .unwrap();
1276
1277 wb.delete_form_control("Sheet1", 0).unwrap();
1278
1279 let controls = wb.get_form_controls("Sheet1").unwrap();
1280 assert_eq!(controls.len(), 1);
1281 assert_eq!(controls[0].control_type, FormControlType::CheckBox);
1282 }
1283
1284 #[test]
1285 fn test_workbook_delete_form_control_out_of_bounds() {
1286 use crate::workbook::Workbook;
1287
1288 let mut wb = Workbook::new();
1289 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1290 .unwrap();
1291 let result = wb.delete_form_control("Sheet1", 5);
1292 assert!(result.is_err());
1293 }
1294
1295 #[test]
1296 fn test_workbook_form_control_save_roundtrip() {
1297 use crate::workbook::Workbook;
1298 use tempfile::TempDir;
1299
1300 let dir = TempDir::new().unwrap();
1301 let path = dir.path().join("form_controls.xlsx");
1302
1303 let mut wb = Workbook::new();
1304 let mut btn = FormControlConfig::button("B2", "Submit");
1305 btn.macro_name = Some("Sheet1.OnSubmit".to_string());
1306 wb.add_form_control("Sheet1", btn).unwrap();
1307
1308 let mut chk = FormControlConfig::checkbox("B4", "Agree");
1309 chk.cell_link = Some("$D$4".to_string());
1310 chk.checked = Some(true);
1311 wb.add_form_control("Sheet1", chk).unwrap();
1312
1313 wb.add_form_control("Sheet1", FormControlConfig::spin_button("E2", 0, 100))
1314 .unwrap();
1315
1316 wb.save(&path).unwrap();
1317
1318 let file = std::fs::File::open(&path).unwrap();
1320 let mut archive = zip::ZipArchive::new(file).unwrap();
1321
1322 let has_vml = (1..=10).any(|i| {
1323 archive
1324 .by_name(&format!("xl/drawings/vmlDrawing{i}.vml"))
1325 .is_ok()
1326 });
1327 assert!(has_vml, "should have a vmlDrawing file in the ZIP");
1328
1329 let mut wb2 = Workbook::open(&path).unwrap();
1331 let controls = wb2.get_form_controls("Sheet1").unwrap();
1332 assert_eq!(controls.len(), 3);
1333 assert_eq!(controls[0].control_type, FormControlType::Button);
1334 assert_eq!(controls[0].text.as_deref(), Some("Submit"));
1335 assert_eq!(controls[0].macro_name.as_deref(), Some("Sheet1.OnSubmit"));
1336 assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1337 assert_eq!(controls[1].cell_link.as_deref(), Some("$D$4"));
1338 assert_eq!(controls[1].checked, Some(true));
1339 assert_eq!(controls[2].control_type, FormControlType::SpinButton);
1340 assert_eq!(controls[2].min_value, Some(0));
1341 assert_eq!(controls[2].max_value, Some(100));
1342 }
1343
1344 #[test]
1345 fn test_workbook_form_control_all_7_types() {
1346 use crate::workbook::Workbook;
1347
1348 let mut wb = Workbook::new();
1349 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Button"))
1350 .unwrap();
1351 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Checkbox"))
1352 .unwrap();
1353 wb.add_form_control(
1354 "Sheet1",
1355 FormControlConfig {
1356 control_type: FormControlType::OptionButton,
1357 cell: "A5".to_string(),
1358 width: None,
1359 height: None,
1360 text: Some("Option".to_string()),
1361 macro_name: None,
1362 cell_link: None,
1363 checked: None,
1364 min_value: None,
1365 max_value: None,
1366 increment: None,
1367 page_increment: None,
1368 current_value: None,
1369 three_d: None,
1370 },
1371 )
1372 .unwrap();
1373 wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 10))
1374 .unwrap();
1375 wb.add_form_control("Sheet1", FormControlConfig::scroll_bar("E1", 0, 100))
1376 .unwrap();
1377 wb.add_form_control(
1378 "Sheet1",
1379 FormControlConfig {
1380 control_type: FormControlType::GroupBox,
1381 cell: "G1".to_string(),
1382 width: None,
1383 height: None,
1384 text: Some("Group".to_string()),
1385 macro_name: None,
1386 cell_link: None,
1387 checked: None,
1388 min_value: None,
1389 max_value: None,
1390 increment: None,
1391 page_increment: None,
1392 current_value: None,
1393 three_d: None,
1394 },
1395 )
1396 .unwrap();
1397 wb.add_form_control(
1398 "Sheet1",
1399 FormControlConfig {
1400 control_type: FormControlType::Label,
1401 cell: "I1".to_string(),
1402 width: None,
1403 height: None,
1404 text: Some("Label Text".to_string()),
1405 macro_name: None,
1406 cell_link: None,
1407 checked: None,
1408 min_value: None,
1409 max_value: None,
1410 increment: None,
1411 page_increment: None,
1412 current_value: None,
1413 three_d: None,
1414 },
1415 )
1416 .unwrap();
1417
1418 let controls = wb.get_form_controls("Sheet1").unwrap();
1419 assert_eq!(controls.len(), 7);
1420 assert_eq!(controls[0].control_type, FormControlType::Button);
1421 assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1422 assert_eq!(controls[2].control_type, FormControlType::OptionButton);
1423 assert_eq!(controls[3].control_type, FormControlType::SpinButton);
1424 assert_eq!(controls[4].control_type, FormControlType::ScrollBar);
1425 assert_eq!(controls[5].control_type, FormControlType::GroupBox);
1426 assert_eq!(controls[6].control_type, FormControlType::Label);
1427 }
1428
1429 #[test]
1430 fn test_workbook_form_control_with_comments() {
1431 use crate::workbook::Workbook;
1432 use tempfile::TempDir;
1433
1434 let dir = TempDir::new().unwrap();
1435 let path = dir.path().join("controls_and_comments.xlsx");
1436
1437 let mut wb = Workbook::new();
1438 wb.add_comment(
1439 "Sheet1",
1440 &crate::comment::CommentConfig {
1441 cell: "A1".to_string(),
1442 author: "Author".to_string(),
1443 text: "A comment".to_string(),
1444 },
1445 )
1446 .unwrap();
1447 wb.add_form_control("Sheet1", FormControlConfig::button("C1", "Button"))
1448 .unwrap();
1449 wb.save(&path).unwrap();
1450
1451 let mut wb2 = Workbook::open(&path).unwrap();
1452 let comments = wb2.get_comments("Sheet1").unwrap();
1453 assert_eq!(comments.len(), 1);
1454 let controls = wb2.get_form_controls("Sheet1").unwrap();
1455 assert_eq!(controls.len(), 1);
1456 assert_eq!(controls[0].control_type, FormControlType::Button);
1457 }
1458
1459 #[test]
1460 fn test_workbook_get_form_controls_empty() {
1461 use crate::workbook::Workbook;
1462
1463 let mut wb = Workbook::new();
1464 let controls = wb.get_form_controls("Sheet1").unwrap();
1465 assert!(controls.is_empty());
1466 }
1467
1468 #[test]
1469 fn test_workbook_get_form_controls_sheet_not_found() {
1470 use crate::workbook::Workbook;
1471
1472 let mut wb = Workbook::new();
1473 let result = wb.get_form_controls("NoSheet");
1474 assert!(result.is_err());
1475 }
1476
1477 #[test]
1478 fn test_open_file_get_form_controls_returns_existing() {
1479 use crate::workbook::Workbook;
1480 use tempfile::TempDir;
1481
1482 let dir = TempDir::new().unwrap();
1483 let path = dir.path().join("get_existing.xlsx");
1484
1485 let mut wb = Workbook::new();
1486 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Existing"))
1487 .unwrap();
1488 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Check"))
1489 .unwrap();
1490 wb.save(&path).unwrap();
1491
1492 let mut wb2 = Workbook::open(&path).unwrap();
1493 let controls = wb2.get_form_controls("Sheet1").unwrap();
1494 assert_eq!(controls.len(), 2);
1495 assert_eq!(controls[0].control_type, FormControlType::Button);
1496 assert_eq!(controls[0].text.as_deref(), Some("Existing"));
1497 assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1498 assert_eq!(controls[1].text.as_deref(), Some("Check"));
1499 }
1500
1501 #[test]
1502 fn test_open_file_add_form_control_preserves_existing() {
1503 use crate::workbook::Workbook;
1504 use tempfile::TempDir;
1505
1506 let dir = TempDir::new().unwrap();
1507 let path = dir.path().join("add_preserves.xlsx");
1508
1509 let mut wb = Workbook::new();
1510 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "First"))
1511 .unwrap();
1512 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Second"))
1513 .unwrap();
1514 wb.save(&path).unwrap();
1515
1516 let mut wb2 = Workbook::open(&path).unwrap();
1517 wb2.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 50))
1518 .unwrap();
1519
1520 let controls = wb2.get_form_controls("Sheet1").unwrap();
1521 assert_eq!(
1522 controls.len(),
1523 3,
1524 "old + new controls should all be present"
1525 );
1526 assert_eq!(controls[0].control_type, FormControlType::Button);
1527 assert_eq!(controls[0].text.as_deref(), Some("First"));
1528 assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1529 assert_eq!(controls[1].text.as_deref(), Some("Second"));
1530 assert_eq!(controls[2].control_type, FormControlType::SpinButton);
1531 assert_eq!(controls[2].min_value, Some(0));
1532 assert_eq!(controls[2].max_value, Some(50));
1533 }
1534
1535 #[test]
1536 fn test_open_file_delete_form_control_works() {
1537 use crate::workbook::Workbook;
1538 use tempfile::TempDir;
1539
1540 let dir = TempDir::new().unwrap();
1541 let path = dir.path().join("delete_works.xlsx");
1542
1543 let mut wb = Workbook::new();
1544 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "First"))
1545 .unwrap();
1546 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Second"))
1547 .unwrap();
1548 wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 10))
1549 .unwrap();
1550 wb.save(&path).unwrap();
1551
1552 let mut wb2 = Workbook::open(&path).unwrap();
1553 wb2.delete_form_control("Sheet1", 1).unwrap();
1554
1555 let controls = wb2.get_form_controls("Sheet1").unwrap();
1556 assert_eq!(controls.len(), 2);
1557 assert_eq!(controls[0].control_type, FormControlType::Button);
1558 assert_eq!(controls[1].control_type, FormControlType::SpinButton);
1559 }
1560
1561 #[test]
1562 fn test_open_file_modify_save_reopen_persistence() {
1563 use crate::workbook::Workbook;
1564 use tempfile::TempDir;
1565
1566 let dir = TempDir::new().unwrap();
1567 let path1 = dir.path().join("persistence_step1.xlsx");
1568 let path2 = dir.path().join("persistence_step2.xlsx");
1569
1570 let mut wb = Workbook::new();
1572 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Button"))
1573 .unwrap();
1574 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Check"))
1575 .unwrap();
1576 wb.save(&path1).unwrap();
1577
1578 let mut wb2 = Workbook::open(&path1).unwrap();
1580 wb2.add_form_control("Sheet1", FormControlConfig::scroll_bar("E1", 0, 100))
1581 .unwrap();
1582 wb2.delete_form_control("Sheet1", 0).unwrap();
1583 wb2.save(&path2).unwrap();
1584
1585 let mut wb3 = Workbook::open(&path2).unwrap();
1587 let controls = wb3.get_form_controls("Sheet1").unwrap();
1588 assert_eq!(controls.len(), 2);
1589 assert_eq!(controls[0].control_type, FormControlType::CheckBox);
1590 assert_eq!(controls[0].text.as_deref(), Some("Check"));
1591 assert_eq!(controls[1].control_type, FormControlType::ScrollBar);
1592 assert_eq!(controls[1].min_value, Some(0));
1593 assert_eq!(controls[1].max_value, Some(100));
1594 }
1595
1596 #[test]
1597 fn test_info_to_config_roundtrip() {
1598 let config = FormControlConfig {
1599 control_type: FormControlType::CheckBox,
1600 cell: "B2".to_string(),
1601 width: None,
1602 height: None,
1603 text: Some("Toggle".to_string()),
1604 macro_name: Some("MyMacro".to_string()),
1605 cell_link: Some("$D$1".to_string()),
1606 checked: Some(true),
1607 min_value: None,
1608 max_value: None,
1609 increment: None,
1610 page_increment: None,
1611 current_value: None,
1612 three_d: None,
1613 };
1614
1615 let vml = build_form_control_vml(&[config.clone()], 1025);
1616 let parsed = parse_form_controls(&vml);
1617 assert_eq!(parsed.len(), 1);
1618 let roundtripped = parsed[0].to_config();
1619 assert_eq!(roundtripped.control_type, config.control_type);
1620 assert_eq!(roundtripped.text, config.text);
1621 assert_eq!(roundtripped.macro_name, config.macro_name);
1622 assert_eq!(roundtripped.cell_link, config.cell_link);
1623 assert_eq!(roundtripped.checked, config.checked);
1624 }
1625
1626 #[test]
1627 fn test_strip_form_control_shapes_controls_only() {
1628 let vml = build_form_control_vml(
1629 &[
1630 FormControlConfig::button("A1", "Btn"),
1631 FormControlConfig::checkbox("A3", "Chk"),
1632 ],
1633 1025,
1634 );
1635 let result = strip_form_control_shapes_from_vml(vml.as_bytes());
1636 assert!(
1637 result.is_none(),
1638 "should return None when no comment shapes remain"
1639 );
1640 }
1641
1642 #[test]
1643 fn test_strip_form_control_shapes_mixed() {
1644 let comment_vml = crate::vml::build_vml_drawing(&["A1"]);
1646 let controls = vec![FormControlConfig::button("C1", "Click")];
1647 let mixed = merge_vml_controls(comment_vml.as_bytes(), &controls, 1026);
1648
1649 let mixed_str = String::from_utf8_lossy(&mixed);
1650 assert!(mixed_str.contains("ObjectType=\"Note\""));
1651 assert!(mixed_str.contains("ObjectType=\"Button\""));
1652
1653 let stripped = strip_form_control_shapes_from_vml(&mixed).unwrap();
1654 let stripped_str = String::from_utf8(stripped).unwrap();
1655 assert!(
1656 stripped_str.contains("ObjectType=\"Note\""),
1657 "comment shapes should be preserved"
1658 );
1659 assert!(
1660 !stripped_str.contains("ObjectType=\"Button\""),
1661 "form control shapes should be removed"
1662 );
1663 }
1664
1665 #[test]
1666 fn test_hydration_does_not_duplicate_on_save() {
1667 use crate::workbook::Workbook;
1668 use tempfile::TempDir;
1669
1670 let dir = TempDir::new().unwrap();
1671 let path1 = dir.path().join("no_dup_step1.xlsx");
1672 let path2 = dir.path().join("no_dup_step2.xlsx");
1673
1674 let mut wb = Workbook::new();
1675 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1676 .unwrap();
1677 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Chk"))
1678 .unwrap();
1679 wb.save(&path1).unwrap();
1680
1681 let mut wb2 = Workbook::open(&path1).unwrap();
1683 let controls = wb2.get_form_controls("Sheet1").unwrap();
1684 assert_eq!(controls.len(), 2);
1685
1686 wb2.save(&path2).unwrap();
1688
1689 let mut wb3 = Workbook::open(&path2).unwrap();
1691 let controls3 = wb3.get_form_controls("Sheet1").unwrap();
1692 assert_eq!(
1693 controls3.len(),
1694 2,
1695 "control count must be stable after hydrate+save cycle"
1696 );
1697 assert_eq!(controls3[0].control_type, FormControlType::Button);
1698 assert_eq!(controls3[1].control_type, FormControlType::CheckBox);
1699 }
1700
1701 #[test]
1702 fn test_hydration_then_add_no_duplication() {
1703 use crate::workbook::Workbook;
1704 use tempfile::TempDir;
1705
1706 let dir = TempDir::new().unwrap();
1707 let path1 = dir.path().join("add_no_dup_step1.xlsx");
1708 let path2 = dir.path().join("add_no_dup_step2.xlsx");
1709
1710 let mut wb = Workbook::new();
1711 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Existing"))
1712 .unwrap();
1713 wb.save(&path1).unwrap();
1714
1715 let mut wb2 = Workbook::open(&path1).unwrap();
1717 wb2.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "New"))
1718 .unwrap();
1719 wb2.save(&path2).unwrap();
1720
1721 let mut wb3 = Workbook::open(&path2).unwrap();
1723 let controls = wb3.get_form_controls("Sheet1").unwrap();
1724 assert_eq!(controls.len(), 2, "should have exactly 1 existing + 1 new");
1725 assert_eq!(controls[0].control_type, FormControlType::Button);
1726 assert_eq!(controls[0].text.as_deref(), Some("Existing"));
1727 assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1728 assert_eq!(controls[1].text.as_deref(), Some("New"));
1729 }
1730
1731 #[test]
1732 fn test_hydration_with_comments_no_duplication() {
1733 use crate::workbook::Workbook;
1734 use tempfile::TempDir;
1735
1736 let dir = TempDir::new().unwrap();
1737 let path1 = dir.path().join("comments_no_dup_step1.xlsx");
1738 let path2 = dir.path().join("comments_no_dup_step2.xlsx");
1739
1740 let mut wb = Workbook::new();
1741 wb.add_comment(
1742 "Sheet1",
1743 &crate::comment::CommentConfig {
1744 cell: "A1".to_string(),
1745 author: "Author".to_string(),
1746 text: "A comment".to_string(),
1747 },
1748 )
1749 .unwrap();
1750 wb.add_form_control("Sheet1", FormControlConfig::button("C1", "Btn"))
1751 .unwrap();
1752 wb.save(&path1).unwrap();
1753
1754 let mut wb2 = Workbook::open(&path1).unwrap();
1756 let controls = wb2.get_form_controls("Sheet1").unwrap();
1757 assert_eq!(controls.len(), 1);
1758 let comments = wb2.get_comments("Sheet1").unwrap();
1759 assert_eq!(comments.len(), 1);
1760 wb2.save(&path2).unwrap();
1761
1762 let mut wb3 = Workbook::open(&path2).unwrap();
1764 let controls3 = wb3.get_form_controls("Sheet1").unwrap();
1765 assert_eq!(
1766 controls3.len(),
1767 1,
1768 "form controls must not be duplicated when mixed with comments"
1769 );
1770 assert_eq!(controls3[0].control_type, FormControlType::Button);
1771 let comments3 = wb3.get_comments("Sheet1").unwrap();
1772 assert_eq!(comments3.len(), 1);
1773 assert_eq!(comments3[0].text, "A comment");
1774 }
1775
1776 #[test]
1777 fn test_multiple_hydrate_save_cycles_stable_count() {
1778 use crate::workbook::Workbook;
1779 use tempfile::TempDir;
1780
1781 let dir = TempDir::new().unwrap();
1782 let mut prev_path = dir.path().join("cycle_0.xlsx");
1783
1784 let mut wb = Workbook::new();
1785 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1786 .unwrap();
1787 wb.add_form_control("Sheet1", FormControlConfig::spin_button("C1", 0, 50))
1788 .unwrap();
1789 wb.save(&prev_path).unwrap();
1790
1791 for i in 1..=3 {
1793 let next_path = dir.path().join(format!("cycle_{i}.xlsx"));
1794 let mut wb_n = Workbook::open(&prev_path).unwrap();
1795 let controls = wb_n.get_form_controls("Sheet1").unwrap();
1796 assert_eq!(
1797 controls.len(),
1798 2,
1799 "cycle {i}: control count should remain 2"
1800 );
1801 wb_n.save(&next_path).unwrap();
1802 prev_path = next_path;
1803 }
1804 }
1805
1806 #[test]
1807 fn test_save_without_get_preserves_controls() {
1808 use crate::workbook::Workbook;
1809 use tempfile::TempDir;
1810
1811 let dir = TempDir::new().unwrap();
1812 let path1 = dir.path().join("no_get_step1.xlsx");
1813 let path2 = dir.path().join("no_get_step2.xlsx");
1814
1815 let mut wb = Workbook::new();
1816 wb.add_form_control("Sheet1", FormControlConfig::button("A1", "Btn"))
1817 .unwrap();
1818 wb.add_form_control("Sheet1", FormControlConfig::checkbox("A3", "Chk"))
1819 .unwrap();
1820 wb.save(&path1).unwrap();
1821
1822 let wb2 = Workbook::open(&path1).unwrap();
1824 wb2.save(&path2).unwrap();
1825
1826 let mut wb3 = Workbook::open(&path2).unwrap();
1828 let controls = wb3.get_form_controls("Sheet1").unwrap();
1829 assert_eq!(
1830 controls.len(),
1831 2,
1832 "controls must be preserved when saving without hydration"
1833 );
1834 assert_eq!(controls[0].control_type, FormControlType::Button);
1835 assert_eq!(controls[1].control_type, FormControlType::CheckBox);
1836 }
1837}