Skip to main content

sheetkit_core/
control.rs

1//! Form control support for Excel worksheets.
2//!
3//! Excel uses legacy VML (Vector Markup Language) drawing parts for form
4//! controls such as buttons, check boxes, option buttons, spin buttons,
5//! scroll bars, group boxes, and labels. This module generates the VML
6//! markup needed to add form controls and parses existing VML to read
7//! them back.
8
9use crate::error::{Error, Result};
10use crate::utils::cell_ref::cell_name_to_coordinates;
11
12/// Form control types.
13#[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    /// Return the VML ObjectType string for x:ClientData.
26    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    /// Parse an ObjectType string back into a FormControlType.
39    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    /// Parse a user-facing type string into a FormControlType.
53    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/// Configuration for adding a form control to a worksheet.
72#[derive(Debug, Clone)]
73pub struct FormControlConfig {
74    /// The type of form control.
75    pub control_type: FormControlType,
76    /// Anchor cell (top-left corner), e.g. "B2".
77    pub cell: String,
78    /// Width in points. Uses a sensible default per control type if None.
79    pub width: Option<f64>,
80    /// Height in points. Uses a sensible default per control type if None.
81    pub height: Option<f64>,
82    /// Display text (Button, CheckBox, OptionButton, GroupBox, Label).
83    pub text: Option<String>,
84    /// VBA macro name (Button only).
85    pub macro_name: Option<String>,
86    /// Linked cell reference for value binding (CheckBox, OptionButton, SpinButton, ScrollBar).
87    pub cell_link: Option<String>,
88    /// Initial checked state (CheckBox, OptionButton).
89    pub checked: Option<bool>,
90    /// Minimum value (SpinButton, ScrollBar).
91    pub min_value: Option<u32>,
92    /// Maximum value (SpinButton, ScrollBar).
93    pub max_value: Option<u32>,
94    /// Step increment (SpinButton, ScrollBar).
95    pub increment: Option<u32>,
96    /// Page increment (ScrollBar only).
97    pub page_increment: Option<u32>,
98    /// Current value (SpinButton, ScrollBar).
99    pub current_value: Option<u32>,
100    /// Enable 3D shading (default true for most controls).
101    pub three_d: Option<bool>,
102}
103
104impl FormControlConfig {
105    /// Create a Button configuration.
106    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    /// Create a CheckBox configuration.
126    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    /// Create a SpinButton configuration.
146    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    /// Create a ScrollBar configuration.
166    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    /// Validate the configuration for correctness.
186    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/// Information about an existing form control, returned when querying.
222#[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
237/// Default dimensions (width, height) in points for each control type.
238fn 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
250/// VML shapetype id for form controls (differs from comments which use t202).
251const FORM_CONTROL_SHAPETYPE_ID: &str = "_x0000_t201";
252
253/// Build the VML anchor string from a cell reference and optional dimensions.
254///
255/// The anchor format is: col1, col1Off, row1, row1Off, col2, col2Off, row2, row2Off.
256/// Offsets are in units of 1/1024 column width or 1/256 row height.
257fn 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    // Approximate column and row span from point dimensions.
263    // Standard column width is ~64 pixels (~48pt), standard row height ~15pt.
264    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
273/// Build a complete VML drawing document containing form control shapes.
274///
275/// `controls` is a list of FormControlConfig entries that have been validated.
276/// `start_shape_id` is the starting shape ID (usually 1025).
277/// Returns the VML XML string.
278pub 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        // Skip controls whose cell reference cannot be resolved (e.g. data
289        // from a corrupted file). Validated controls added via
290        // add_form_control will never hit this branch.
291        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    // Form control shapetype (t201).
315    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
330/// Write a single VML form control shape element.
331fn 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    // x:ClientData element with control-specific properties.
404    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    // 3D shading: default is true for most controls. Write NoThreeD only when explicitly false.
444    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
453/// Parse form controls from a VML drawing XML string.
454///
455/// Scans for `<x:ClientData ObjectType="...">` elements and extracts
456/// control properties. Returns a list of FormControlInfo.
457pub 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        // Skip non-form-control shapes (e.g. comment shapes use ObjectType="Note").
470        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
479/// Parse a single v:shape element into a FormControlInfo, if it is a form control.
480fn parse_single_control(shape_xml: &str) -> Option<FormControlInfo> {
481    // Find the ClientData element.
482    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    // Extract ObjectType attribute.
489    let obj_type = extract_attr(cd_xml, "ObjectType")?;
490    let control_type = FormControlType::from_object_type(&obj_type)?;
491
492    // Skip Note types (comments).
493    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
527/// Extract an XML attribute value from a tag.
528fn 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
536/// Extract text content of an XML element like `<tag>content</tag>`.
537fn 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
551/// Extract textbox text from a v:shape element.
552fn 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    // Text is inside a <div> or <font> element; extract plain text.
558    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
576/// Extract the anchor cell reference from x:Anchor element.
577///
578/// The anchor format is "col1, col1Off, row1, row1Off, col2, col2Off, row2, row2Off".
579/// We derive the cell from col1 (0-based) and row1 (0-based).
580fn 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
591/// Merge new form control VML into existing VML bytes (for sheets that
592/// already have VML content from comments or other controls).
593///
594/// This appends new shape elements before the closing `</xml>` tag and
595/// updates the shapetype if needed.
596pub 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    // Generate shape elements for the new controls.
604    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    // Check if the form control shapetype already exists.
617    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    // Insert before closing </xml>.
632    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        // Malformed VML; return new VML document.
642        build_form_control_vml(controls, start_shape_id).into_bytes()
643    }
644}
645
646impl FormControlInfo {
647    /// Convert a parsed `FormControlInfo` back to a `FormControlConfig`.
648    ///
649    /// This is used during VML hydration to reconstruct the config list from
650    /// existing VML data so that subsequent add/delete operations work correctly.
651    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
671/// Count existing VML shapes in VML bytes to determine the next shape ID.
672pub 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
677/// Strip form control shapes from VML bytes, keeping only comment (Note) shapes.
678///
679/// Returns `None` if no comment shapes remain after stripping, or `Some(bytes)`
680/// with the cleaned VML that contains only comment shapes.
681pub 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    // Collect ranges of form control shapes to remove.
685    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        // Check if this is a comment shape (ObjectType="Note").
698        if shape_xml.contains("ObjectType=\"Note\"") {
699            keep_shapes.push((abs_start, shape_end));
700            has_comment_shapes = true;
701        }
702        // Form control shapes are silently dropped.
703        search_from = shape_end;
704    }
705
706    if !has_comment_shapes {
707        return None;
708    }
709
710    // Rebuild VML with only the comment shapes preserved.
711    // Find the header portion (everything before the first <v:shape).
712    let first_shape_pos = vml_str.find("<v:shape ").unwrap_or(vml_str.len());
713    let header = &vml_str[..first_shape_pos];
714
715    // Also strip the form control shapetype (_x0000_t201) if present, keeping
716    // only the comment shapetype (_x0000_t202).
717    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
731/// Remove a <v:shapetype id="..."> ... </v:shapetype> block from the header.
732fn 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            // Also consume a trailing newline if present.
738            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        // Build a VML that has both a comment (Note) and a form control.
1116        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        // Should not duplicate shapetype.
1145        let shapetype_count = merged_str.matches(FORM_CONTROL_SHAPETYPE_ID).count();
1146        // One in the shapetype definition and references in each shape = 1 (def) + 2 (refs)
1147        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"); // col0
1156        assert_eq!(parts[2], "0"); // row0
1157    }
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"); // col0 (C = 3, 0-based = 2)
1164        assert_eq!(parts[2], "4"); // row0 (5, 0-based = 4)
1165    }
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        // Verify VML part exists in the ZIP.
1319        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        // Re-open and verify controls are preserved.
1330        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        // Step 1: Create with 2 controls.
1571        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        // Step 2: Open, add one, delete one, save.
1579        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        // Step 3: Re-open and verify.
1586        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        // Build VML with both a comment shape and a form control shape.
1645        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        // Open and trigger hydration via get_form_controls (read-only).
1682        let mut wb2 = Workbook::open(&path1).unwrap();
1683        let controls = wb2.get_form_controls("Sheet1").unwrap();
1684        assert_eq!(controls.len(), 2);
1685
1686        // Save without adding or removing anything.
1687        wb2.save(&path2).unwrap();
1688
1689        // Re-open and verify no duplication.
1690        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        // Open, add a new control, save.
1716        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        // Re-open and verify exact expected count.
1722        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        // Open, hydrate via get_form_controls, save.
1755        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        // Re-open and verify no duplication of either comments or controls.
1763        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        // Run 3 open-hydrate-save cycles.
1792        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        // Open and save immediately without calling get_form_controls.
1823        let wb2 = Workbook::open(&path1).unwrap();
1824        wb2.save(&path2).unwrap();
1825
1826        // Re-open and verify controls are preserved without duplication.
1827        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}