Skip to main content

oxidize_pdf/forms/
field.rs

1//! Form field and widget definitions
2
3use crate::geometry::Rectangle;
4use crate::graphics::Color;
5use crate::objects::{Dictionary, Object};
6use std::collections::{HashMap, HashSet};
7
8/// Field flags according to ISO 32000-1 Table 221
9#[derive(Debug, Clone, Copy, Default)]
10pub struct FieldFlags {
11    /// Field is read-only
12    pub read_only: bool,
13    /// Field is required
14    pub required: bool,
15    /// Field should not be exported
16    pub no_export: bool,
17}
18
19impl FieldFlags {
20    /// Convert to PDF flags integer
21    pub fn to_flags(&self) -> u32 {
22        let mut flags = 0u32;
23        if self.read_only {
24            flags |= 1 << 0;
25        }
26        if self.required {
27            flags |= 1 << 1;
28        }
29        if self.no_export {
30            flags |= 1 << 2;
31        }
32        flags
33    }
34}
35
36/// Field options
37#[derive(Debug, Clone, Default)]
38pub struct FieldOptions {
39    /// Field flags
40    pub flags: FieldFlags,
41    /// Default appearance string
42    pub default_appearance: Option<String>,
43    /// Quadding (justification): 0=left, 1=center, 2=right
44    pub quadding: Option<i32>,
45}
46
47/// Widget appearance settings
48#[derive(Debug, Clone)]
49pub struct WidgetAppearance {
50    /// Border color
51    pub border_color: Option<Color>,
52    /// Background color
53    pub background_color: Option<Color>,
54    /// Border width
55    pub border_width: f64,
56    /// Border style: S (solid), D (dashed), B (beveled), I (inset), U (underline)
57    pub border_style: BorderStyle,
58}
59
60/// Border style for widgets
61#[derive(Debug, Clone, Copy)]
62pub enum BorderStyle {
63    /// Solid border
64    Solid,
65    /// Dashed border
66    Dashed,
67    /// Beveled border
68    Beveled,
69    /// Inset border
70    Inset,
71    /// Underline only
72    Underline,
73}
74
75impl BorderStyle {
76    /// Get PDF name
77    pub fn pdf_name(&self) -> &'static str {
78        match self {
79            BorderStyle::Solid => "S",
80            BorderStyle::Dashed => "D",
81            BorderStyle::Beveled => "B",
82            BorderStyle::Inset => "I",
83            BorderStyle::Underline => "U",
84        }
85    }
86}
87
88impl Default for WidgetAppearance {
89    fn default() -> Self {
90        Self {
91            border_color: Some(Color::black()),
92            background_color: None,
93            border_width: 1.0,
94            border_style: BorderStyle::Solid,
95        }
96    }
97}
98
99/// Widget annotation for form field
100#[derive(Debug, Clone)]
101pub struct Widget {
102    /// Rectangle for widget position
103    pub rect: Rectangle,
104    /// Appearance settings
105    pub appearance: WidgetAppearance,
106    /// Parent field reference (will be set by FormManager)
107    pub parent: Option<String>,
108    /// Appearance streams (Normal, Rollover, Down)
109    pub appearance_streams: Option<crate::forms::AppearanceDictionary>,
110}
111
112impl Widget {
113    /// Create a new widget
114    pub fn new(rect: Rectangle) -> Self {
115        Self {
116            rect,
117            appearance: WidgetAppearance::default(),
118            parent: None,
119            appearance_streams: None,
120        }
121    }
122
123    /// Set appearance
124    pub fn with_appearance(mut self, appearance: WidgetAppearance) -> Self {
125        self.appearance = appearance;
126        self
127    }
128
129    /// Set appearance streams
130    pub fn with_appearance_streams(mut self, streams: crate::forms::AppearanceDictionary) -> Self {
131        self.appearance_streams = Some(streams);
132        self
133    }
134
135    /// Generate default appearance streams based on field type.
136    ///
137    /// Backwards-compatible entry point — delegates to
138    /// [`generate_appearance_with_font`] without any custom-font context.
139    /// Emits Helvetica + WinAnsi by default, so non-WinAnsi values fail with
140    /// `PdfError::EncodingError`. Callers that need CJK, Arabic, or other
141    /// scripts must use the `_with_font` variant (typically via
142    /// `Document::fill_field`, which routes through it automatically).
143    pub fn generate_appearance(
144        &mut self,
145        field_type: crate::forms::FieldType,
146        value: Option<&str>,
147    ) -> crate::error::Result<()> {
148        let _ = self.generate_appearance_with_font(field_type, value, None, None)?;
149        Ok(())
150    }
151
152    /// Generate appearance streams honouring an optional `/DA` and an
153    /// optional pre-resolved custom (Type0) font.
154    ///
155    /// Returns the per-font character accumulator produced by the Type0 path
156    /// — an empty map for the built-in path — so the caller can merge it
157    /// into `Document::used_characters_by_font` (subsetter invariant, issue
158    /// #204).
159    pub fn generate_appearance_with_font(
160        &mut self,
161        field_type: crate::forms::FieldType,
162        value: Option<&str>,
163        default_appearance: Option<&crate::forms::DefaultAppearance>,
164        custom_font: Option<&crate::fonts::Font>,
165    ) -> crate::error::Result<HashMap<String, HashSet<char>>> {
166        use crate::forms::{generate_field_appearance, AppearanceDictionary, AppearanceState};
167
168        let mut app_dict = AppearanceDictionary::new();
169        let mut merged: HashMap<String, HashSet<char>> = HashMap::new();
170
171        // Normal appearance
172        let normal =
173            generate_field_appearance(field_type, self, value, default_appearance, custom_font)?;
174        app_dict.set_appearance(AppearanceState::Normal, normal.stream);
175        for (font_name, chars) in normal.used_chars_by_font {
176            merged.entry(font_name).or_default().extend(chars);
177        }
178
179        // Buttons also get rollover + down states. Per-state duplication is
180        // intentional — different viewers rely on distinct streams even when
181        // the content is visually identical.
182        if field_type == crate::forms::FieldType::Button {
183            let rollover = generate_field_appearance(
184                field_type,
185                self,
186                value,
187                default_appearance,
188                custom_font,
189            )?;
190            app_dict.set_appearance(AppearanceState::Rollover, rollover.stream);
191            for (font_name, chars) in rollover.used_chars_by_font {
192                merged.entry(font_name).or_default().extend(chars);
193            }
194
195            let down = generate_field_appearance(
196                field_type,
197                self,
198                value,
199                default_appearance,
200                custom_font,
201            )?;
202            app_dict.set_appearance(AppearanceState::Down, down.stream);
203            for (font_name, chars) in down.used_chars_by_font {
204                merged.entry(font_name).or_default().extend(chars);
205            }
206        }
207
208        self.appearance_streams = Some(app_dict);
209        Ok(merged)
210    }
211
212    /// Convert to annotation dictionary
213    pub fn to_annotation_dict(&self) -> Dictionary {
214        let mut dict = Dictionary::new();
215
216        // Annotation type
217        dict.set("Type", Object::Name("Annot".to_string()));
218        dict.set("Subtype", Object::Name("Widget".to_string()));
219
220        // Rectangle
221        let rect_array = vec![
222            Object::Real(self.rect.lower_left.x),
223            Object::Real(self.rect.lower_left.y),
224            Object::Real(self.rect.upper_right.x),
225            Object::Real(self.rect.upper_right.y),
226        ];
227        dict.set("Rect", Object::Array(rect_array));
228
229        // Border style
230        let mut bs_dict = Dictionary::new();
231        bs_dict.set("W", Object::Real(self.appearance.border_width));
232        bs_dict.set(
233            "S",
234            Object::Name(self.appearance.border_style.pdf_name().to_string()),
235        );
236        dict.set("BS", Object::Dictionary(bs_dict));
237
238        // Appearance characteristics
239        let mut mk_dict = Dictionary::new();
240
241        if let Some(border_color) = &self.appearance.border_color {
242            let bc = match border_color {
243                Color::Rgb(r, g, b) => vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)],
244                Color::Gray(g) => vec![Object::Real(*g)],
245                Color::Cmyk(c, m, y, k) => vec![
246                    Object::Real(*c),
247                    Object::Real(*m),
248                    Object::Real(*y),
249                    Object::Real(*k),
250                ],
251            };
252            mk_dict.set("BC", Object::Array(bc));
253        }
254
255        if let Some(bg_color) = &self.appearance.background_color {
256            let bg = match bg_color {
257                Color::Rgb(r, g, b) => vec![Object::Real(*r), Object::Real(*g), Object::Real(*b)],
258                Color::Gray(g) => vec![Object::Real(*g)],
259                Color::Cmyk(c, m, y, k) => vec![
260                    Object::Real(*c),
261                    Object::Real(*m),
262                    Object::Real(*y),
263                    Object::Real(*k),
264                ],
265            };
266            mk_dict.set("BG", Object::Array(bg));
267        }
268
269        dict.set("MK", Object::Dictionary(mk_dict));
270
271        // Flags - print flag
272        dict.set("F", Object::Integer(4));
273
274        // Add appearance dictionary if present
275        if let Some(ref app_streams) = self.appearance_streams {
276            dict.set("AP", Object::Dictionary(app_streams.to_dict()));
277        }
278
279        dict
280    }
281}
282
283/// Base field trait
284pub trait Field {
285    /// Get field name
286    fn name(&self) -> &str;
287
288    /// Get field type
289    fn field_type(&self) -> &'static str;
290
291    /// Convert to dictionary
292    fn to_dict(&self) -> Dictionary;
293}
294
295/// Form field with widget
296#[derive(Debug, Clone)]
297pub struct FormField {
298    /// Field dictionary
299    pub field_dict: Dictionary,
300    /// Associated widgets
301    pub widgets: Vec<Widget>,
302    /// Typed `/DA` kept alongside the serialised form in `field_dict` so
303    /// `Document::fill_field` can pick the right font + size + colour when
304    /// regenerating the appearance stream without re-parsing the /DA PDF
305    /// string. `None` falls back to Helvetica + WinAnsi.
306    pub default_appearance: Option<crate::forms::DefaultAppearance>,
307}
308
309impl FormField {
310    /// Create from field dictionary
311    pub fn new(field_dict: Dictionary) -> Self {
312        Self {
313            field_dict,
314            widgets: Vec::new(),
315            default_appearance: None,
316        }
317    }
318
319    /// Add a widget
320    pub fn add_widget(&mut self, widget: Widget) {
321        self.widgets.push(widget);
322    }
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328    use crate::geometry::Point;
329
330    #[test]
331    fn test_field_flags() {
332        let flags = FieldFlags {
333            read_only: true,
334            required: true,
335            no_export: false,
336        };
337
338        assert_eq!(flags.to_flags(), 3); // bits 0 and 1 set
339    }
340
341    #[test]
342    fn test_border_style() {
343        assert_eq!(BorderStyle::Solid.pdf_name(), "S");
344        assert_eq!(BorderStyle::Dashed.pdf_name(), "D");
345        assert_eq!(BorderStyle::Beveled.pdf_name(), "B");
346        assert_eq!(BorderStyle::Inset.pdf_name(), "I");
347        assert_eq!(BorderStyle::Underline.pdf_name(), "U");
348    }
349
350    #[test]
351    fn test_widget_creation() {
352        let rect = Rectangle::new(Point::new(100.0, 100.0), Point::new(200.0, 120.0));
353
354        let widget = Widget::new(rect);
355        assert_eq!(widget.rect.lower_left.x, 100.0);
356        assert_eq!(widget.appearance.border_width, 1.0);
357    }
358
359    #[test]
360    fn test_widget_annotation_dict() {
361        let rect = Rectangle::new(Point::new(50.0, 50.0), Point::new(150.0, 70.0));
362
363        let appearance = WidgetAppearance {
364            border_color: Some(Color::Rgb(0.0, 0.0, 1.0)),
365            background_color: Some(Color::Gray(0.9)),
366            border_width: 2.0,
367            border_style: BorderStyle::Solid,
368        };
369
370        let widget = Widget::new(rect).with_appearance(appearance);
371        let dict = widget.to_annotation_dict();
372
373        assert_eq!(dict.get("Type"), Some(&Object::Name("Annot".to_string())));
374        assert_eq!(
375            dict.get("Subtype"),
376            Some(&Object::Name("Widget".to_string()))
377        );
378        assert!(dict.get("Rect").is_some());
379        assert!(dict.get("BS").is_some());
380        assert!(dict.get("MK").is_some());
381    }
382
383    #[test]
384    fn test_field_options_default() {
385        let options = FieldOptions::default();
386        assert!(!options.flags.read_only);
387        assert!(!options.flags.required);
388        assert!(!options.flags.no_export);
389        assert!(options.default_appearance.is_none());
390        assert!(options.quadding.is_none());
391    }
392
393    #[test]
394    fn test_field_flags_all_combinations() {
395        // Test all flag combinations
396        let flag_combos = [
397            (false, false, false, 0),
398            (true, false, false, 1),
399            (false, true, false, 2),
400            (false, false, true, 4),
401            (true, true, false, 3),
402            (true, false, true, 5),
403            (false, true, true, 6),
404            (true, true, true, 7),
405        ];
406
407        for (read_only, required, no_export, expected) in flag_combos {
408            let flags = FieldFlags {
409                read_only,
410                required,
411                no_export,
412            };
413            assert_eq!(
414                flags.to_flags(),
415                expected,
416                "Failed for read_only={read_only}, required={required}, no_export={no_export}"
417            );
418        }
419    }
420
421    #[test]
422    fn test_field_flags_debug_clone_default() {
423        let flags = FieldFlags::default();
424        let debug_str = format!("{flags:?}");
425        assert!(debug_str.contains("FieldFlags"));
426
427        let cloned = flags;
428        assert_eq!(flags.read_only, cloned.read_only);
429        assert_eq!(flags.required, cloned.required);
430        assert_eq!(flags.no_export, cloned.no_export);
431
432        // Test that Copy trait works
433        let copied = flags;
434        assert_eq!(flags.read_only, copied.read_only);
435    }
436
437    #[test]
438    fn test_border_style_debug_clone_copy() {
439        let style = BorderStyle::Solid;
440        let debug_str = format!("{style:?}");
441        assert!(debug_str.contains("Solid"));
442
443        let cloned = style;
444        assert_eq!(style.pdf_name(), cloned.pdf_name());
445
446        // Test Copy trait
447        let copied = style;
448        assert_eq!(style.pdf_name(), copied.pdf_name());
449    }
450
451    #[test]
452    fn test_all_border_styles() {
453        let styles = [
454            (BorderStyle::Solid, "S"),
455            (BorderStyle::Dashed, "D"),
456            (BorderStyle::Beveled, "B"),
457            (BorderStyle::Inset, "I"),
458            (BorderStyle::Underline, "U"),
459        ];
460
461        for (style, expected) in styles {
462            assert_eq!(style.pdf_name(), expected);
463        }
464    }
465
466    #[test]
467    fn test_widget_appearance_default() {
468        let appearance = WidgetAppearance::default();
469        assert_eq!(appearance.border_color, Some(Color::black()));
470        assert_eq!(appearance.background_color, None);
471        assert_eq!(appearance.border_width, 1.0);
472        match appearance.border_style {
473            BorderStyle::Solid => {}
474            _ => panic!("Expected solid border style"),
475        }
476    }
477
478    #[test]
479    fn test_widget_appearance_debug_clone() {
480        let appearance = WidgetAppearance {
481            border_color: Some(Color::red()),
482            background_color: Some(Color::gray(0.5)),
483            border_width: 2.5,
484            border_style: BorderStyle::Dashed,
485        };
486
487        let debug_str = format!("{appearance:?}");
488        assert!(debug_str.contains("WidgetAppearance"));
489
490        let cloned = appearance.clone();
491        assert_eq!(appearance.border_color, cloned.border_color);
492        assert_eq!(appearance.background_color, cloned.background_color);
493        assert_eq!(appearance.border_width, cloned.border_width);
494    }
495
496    #[test]
497    fn test_widget_debug_clone() {
498        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
499        let widget = Widget::new(rect);
500
501        let debug_str = format!("{widget:?}");
502        assert!(debug_str.contains("Widget"));
503
504        let cloned = widget.clone();
505        assert_eq!(widget.rect.lower_left.x, cloned.rect.lower_left.x);
506        assert_eq!(widget.parent, cloned.parent);
507    }
508
509    #[test]
510    fn test_widget_with_parent() {
511        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
512        let mut widget = Widget::new(rect);
513        widget.parent = Some("TextField1".to_string());
514
515        assert_eq!(widget.parent, Some("TextField1".to_string()));
516    }
517
518    #[test]
519    fn test_widget_annotation_dict_rgb_colors() {
520        let rect = Rectangle::new(Point::new(10.0, 20.0), Point::new(110.0, 40.0));
521        let appearance = WidgetAppearance {
522            border_color: Some(Color::rgb(1.0, 0.0, 0.0)),
523            background_color: Some(Color::rgb(0.0, 1.0, 0.0)),
524            border_width: 1.5,
525            border_style: BorderStyle::Dashed,
526        };
527
528        let widget = Widget::new(rect).with_appearance(appearance);
529        let dict = widget.to_annotation_dict();
530
531        // Check rectangle array
532        if let Some(Object::Array(rect_array)) = dict.get("Rect") {
533            assert_eq!(rect_array.len(), 4);
534            assert_eq!(rect_array[0], Object::Real(10.0));
535            assert_eq!(rect_array[1], Object::Real(20.0));
536            assert_eq!(rect_array[2], Object::Real(110.0));
537            assert_eq!(rect_array[3], Object::Real(40.0));
538        } else {
539            panic!("Expected Rect array");
540        }
541
542        // Check border style
543        if let Some(Object::Dictionary(bs_dict)) = dict.get("BS") {
544            assert_eq!(bs_dict.get("W"), Some(&Object::Real(1.5)));
545            assert_eq!(bs_dict.get("S"), Some(&Object::Name("D".to_string())));
546        } else {
547            panic!("Expected BS dictionary");
548        }
549
550        // Check appearance characteristics
551        if let Some(Object::Dictionary(mk_dict)) = dict.get("MK") {
552            // Check border color
553            if let Some(Object::Array(bc_array)) = mk_dict.get("BC") {
554                assert_eq!(bc_array.len(), 3);
555                assert_eq!(bc_array[0], Object::Real(1.0));
556                assert_eq!(bc_array[1], Object::Real(0.0));
557                assert_eq!(bc_array[2], Object::Real(0.0));
558            } else {
559                panic!("Expected BC array");
560            }
561
562            // Check background color
563            if let Some(Object::Array(bg_array)) = mk_dict.get("BG") {
564                assert_eq!(bg_array.len(), 3);
565                assert_eq!(bg_array[0], Object::Real(0.0));
566                assert_eq!(bg_array[1], Object::Real(1.0));
567                assert_eq!(bg_array[2], Object::Real(0.0));
568            } else {
569                panic!("Expected BG array");
570            }
571        } else {
572            panic!("Expected MK dictionary");
573        }
574
575        // Check flags
576        assert_eq!(dict.get("F"), Some(&Object::Integer(4)));
577    }
578
579    #[test]
580    fn test_widget_annotation_dict_gray_colors() {
581        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(50.0, 25.0));
582        let appearance = WidgetAppearance {
583            border_color: Some(Color::gray(0.3)),
584            background_color: Some(Color::gray(0.9)),
585            border_width: 0.5,
586            border_style: BorderStyle::Beveled,
587        };
588
589        let widget = Widget::new(rect).with_appearance(appearance);
590        let dict = widget.to_annotation_dict();
591
592        if let Some(Object::Dictionary(mk_dict)) = dict.get("MK") {
593            // Check border color (gray)
594            if let Some(Object::Array(bc_array)) = mk_dict.get("BC") {
595                assert_eq!(bc_array.len(), 1);
596                assert_eq!(bc_array[0], Object::Real(0.3));
597            } else {
598                panic!("Expected BC array");
599            }
600
601            // Check background color (gray)
602            if let Some(Object::Array(bg_array)) = mk_dict.get("BG") {
603                assert_eq!(bg_array.len(), 1);
604                assert_eq!(bg_array[0], Object::Real(0.9));
605            } else {
606                panic!("Expected BG array");
607            }
608        } else {
609            panic!("Expected MK dictionary");
610        }
611    }
612
613    #[test]
614    fn test_widget_annotation_dict_cmyk_colors() {
615        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(50.0, 25.0));
616        let appearance = WidgetAppearance {
617            border_color: Some(Color::cmyk(0.1, 0.2, 0.3, 0.4)),
618            background_color: Some(Color::cmyk(0.5, 0.6, 0.7, 0.8)),
619            border_width: 3.0,
620            border_style: BorderStyle::Inset,
621        };
622
623        let widget = Widget::new(rect).with_appearance(appearance);
624        let dict = widget.to_annotation_dict();
625
626        if let Some(Object::Dictionary(mk_dict)) = dict.get("MK") {
627            // Check border color (CMYK)
628            if let Some(Object::Array(bc_array)) = mk_dict.get("BC") {
629                assert_eq!(bc_array.len(), 4);
630                assert_eq!(bc_array[0], Object::Real(0.1));
631                assert_eq!(bc_array[1], Object::Real(0.2));
632                assert_eq!(bc_array[2], Object::Real(0.3));
633                assert_eq!(bc_array[3], Object::Real(0.4));
634            } else {
635                panic!("Expected BC array");
636            }
637
638            // Check background color (CMYK)
639            if let Some(Object::Array(bg_array)) = mk_dict.get("BG") {
640                assert_eq!(bg_array.len(), 4);
641                assert_eq!(bg_array[0], Object::Real(0.5));
642                assert_eq!(bg_array[1], Object::Real(0.6));
643                assert_eq!(bg_array[2], Object::Real(0.7));
644                assert_eq!(bg_array[3], Object::Real(0.8));
645            } else {
646                panic!("Expected BG array");
647            }
648        } else {
649            panic!("Expected MK dictionary");
650        }
651    }
652
653    #[test]
654    fn test_widget_annotation_dict_no_colors() {
655        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(50.0, 25.0));
656        let appearance = WidgetAppearance {
657            border_color: None,
658            background_color: None,
659            border_width: 2.0,
660            border_style: BorderStyle::Underline,
661        };
662
663        let widget = Widget::new(rect).with_appearance(appearance);
664        let dict = widget.to_annotation_dict();
665
666        if let Some(Object::Dictionary(mk_dict)) = dict.get("MK") {
667            assert!(mk_dict.get("BC").is_none());
668            assert!(mk_dict.get("BG").is_none());
669        } else {
670            panic!("Expected MK dictionary");
671        }
672    }
673
674    #[test]
675    fn test_field_options_with_values() {
676        let flags = FieldFlags {
677            read_only: true,
678            required: false,
679            no_export: true,
680        };
681
682        let options = FieldOptions {
683            flags,
684            default_appearance: Some("/Helv 12 Tf 0 g".to_string()),
685            quadding: Some(1), // Center alignment
686        };
687
688        assert!(options.flags.read_only);
689        assert!(!options.flags.required);
690        assert!(options.flags.no_export);
691        assert_eq!(
692            options.default_appearance,
693            Some("/Helv 12 Tf 0 g".to_string())
694        );
695        assert_eq!(options.quadding, Some(1));
696    }
697
698    #[test]
699    fn test_field_options_debug_clone_default() {
700        let options = FieldOptions::default();
701        let debug_str = format!("{options:?}");
702        assert!(debug_str.contains("FieldOptions"));
703
704        let cloned = options.clone();
705        assert_eq!(options.flags.read_only, cloned.flags.read_only);
706        assert_eq!(options.default_appearance, cloned.default_appearance);
707        assert_eq!(options.quadding, cloned.quadding);
708    }
709
710    #[test]
711    fn test_form_field_creation() {
712        let mut field_dict = Dictionary::new();
713        field_dict.set("T", Object::String("TestField".to_string()));
714        field_dict.set("FT", Object::Name("Tx".to_string()));
715
716        let form_field = FormField::new(field_dict);
717        assert_eq!(form_field.widgets.len(), 0);
718        assert_eq!(
719            form_field.field_dict.get("T"),
720            Some(&Object::String("TestField".to_string()))
721        );
722    }
723
724    #[test]
725    fn test_form_field_add_widget() {
726        let mut field_dict = Dictionary::new();
727        field_dict.set("T", Object::String("TestField".to_string()));
728
729        let mut form_field = FormField::new(field_dict);
730
731        let rect1 = Rectangle::new(Point::new(10.0, 10.0), Point::new(110.0, 30.0));
732        let widget1 = Widget::new(rect1);
733
734        let rect2 = Rectangle::new(Point::new(10.0, 50.0), Point::new(110.0, 70.0));
735        let widget2 = Widget::new(rect2);
736
737        form_field.add_widget(widget1);
738        form_field.add_widget(widget2);
739
740        assert_eq!(form_field.widgets.len(), 2);
741        assert_eq!(form_field.widgets[0].rect.lower_left.x, 10.0);
742        assert_eq!(form_field.widgets[1].rect.lower_left.y, 50.0);
743    }
744
745    #[test]
746    fn test_form_field_debug_clone() {
747        let field_dict = Dictionary::new();
748        let form_field = FormField::new(field_dict);
749
750        let debug_str = format!("{form_field:?}");
751        assert!(debug_str.contains("FormField"));
752
753        let cloned = form_field.clone();
754        assert_eq!(form_field.widgets.len(), cloned.widgets.len());
755    }
756
757    #[test]
758    fn test_widget_rect_boundary_values() {
759        // Test with various rectangle configurations
760        let test_rects = [
761            // Normal rectangle
762            Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0)),
763            // Small rectangle
764            Rectangle::new(Point::new(5.0, 5.0), Point::new(6.0, 6.0)),
765            // Large rectangle
766            Rectangle::new(Point::new(0.0, 0.0), Point::new(1000.0, 800.0)),
767            // Negative coordinates
768            Rectangle::new(Point::new(-50.0, -25.0), Point::new(50.0, 25.0)),
769        ];
770
771        for rect in test_rects {
772            let widget = Widget::new(rect);
773            let dict = widget.to_annotation_dict();
774
775            if let Some(Object::Array(rect_array)) = dict.get("Rect") {
776                assert_eq!(rect_array.len(), 4);
777                assert_eq!(rect_array[0], Object::Real(rect.lower_left.x));
778                assert_eq!(rect_array[1], Object::Real(rect.lower_left.y));
779                assert_eq!(rect_array[2], Object::Real(rect.upper_right.x));
780                assert_eq!(rect_array[3], Object::Real(rect.upper_right.y));
781            } else {
782                panic!("Expected Rect array");
783            }
784        }
785    }
786
787    #[test]
788    fn test_border_width_variations() {
789        let rect = Rectangle::new(Point::new(0.0, 0.0), Point::new(100.0, 50.0));
790        let border_widths = [0.0, 0.5, 1.0, 2.5, 10.0];
791
792        for width in border_widths {
793            let appearance = WidgetAppearance {
794                border_color: Some(Color::black()),
795                background_color: None,
796                border_width: width,
797                border_style: BorderStyle::Solid,
798            };
799
800            let widget = Widget::new(rect).with_appearance(appearance);
801            let dict = widget.to_annotation_dict();
802
803            if let Some(Object::Dictionary(bs_dict)) = dict.get("BS") {
804                assert_eq!(bs_dict.get("W"), Some(&Object::Real(width)));
805            } else {
806                panic!("Expected BS dictionary");
807            }
808        }
809    }
810
811    #[test]
812    fn test_quadding_values() {
813        let test_quadding = [
814            (None, "no quadding"),
815            (Some(0), "left alignment"),
816            (Some(1), "center alignment"),
817            (Some(2), "right alignment"),
818        ];
819
820        for (quadding, description) in test_quadding {
821            let options = FieldOptions {
822                flags: FieldFlags::default(),
823                default_appearance: None,
824                quadding,
825            };
826
827            assert_eq!(options.quadding, quadding, "Failed for {description}");
828        }
829    }
830}