Skip to main content

fop_layout/area/
types.rs

1//! Area types and traits
2//!
3//! Defines the different types of areas and their associated traits.
4
5use crate::layout::{properties::OverflowBehavior, TextAlign};
6use fop_types::{Color, Length, Rect};
7
8/// Content stored in an area
9#[derive(Debug, Clone)]
10pub enum AreaContent {
11    /// Text content
12    Text(String),
13
14    /// Binary image data (raw bytes)
15    ImageData(Vec<u8>),
16}
17
18/// An area represents a rectangular region on a page
19#[derive(Debug, Clone)]
20pub struct Area {
21    /// Area type
22    pub area_type: AreaType,
23
24    /// Position and size
25    pub geometry: Rect,
26
27    /// Rendering traits (color, background, borders, etc.)
28    pub traits: TraitSet,
29
30    /// Content (text or image data)
31    pub content: Option<AreaContent>,
32
33    /// Keep constraints for page breaking
34    pub keep_constraint: Option<crate::layout::KeepConstraint>,
35
36    /// Break-before property value
37    pub break_before: Option<crate::layout::BreakValue>,
38
39    /// Break-after property value
40    pub break_after: Option<crate::layout::BreakValue>,
41
42    /// Widows constraint - minimum lines at top of page after break
43    pub widows: i32,
44
45    /// Orphans constraint - minimum lines at bottom of page before break
46    pub orphans: i32,
47}
48
49impl Area {
50    /// Create a new area
51    pub fn new(area_type: AreaType, geometry: Rect) -> Self {
52        Self {
53            area_type,
54            geometry,
55            traits: TraitSet::default(),
56            content: None,
57            keep_constraint: None,
58            break_before: None,
59            break_after: None,
60            widows: 2,
61            orphans: 2,
62        }
63    }
64
65    /// Create a text area
66    pub fn text(geometry: Rect, content: String) -> Self {
67        Self {
68            area_type: AreaType::Text,
69            geometry,
70            traits: TraitSet::default(),
71            content: Some(AreaContent::Text(content)),
72            keep_constraint: None,
73            break_before: None,
74            break_after: None,
75            widows: 2,
76            orphans: 2,
77        }
78    }
79
80    /// Create a viewport area for an image
81    pub fn viewport_with_image(geometry: Rect, image_data: Vec<u8>) -> Self {
82        Self {
83            area_type: AreaType::Viewport,
84            geometry,
85            traits: TraitSet::default(),
86            content: Some(AreaContent::ImageData(image_data)),
87            keep_constraint: None,
88            break_before: None,
89            break_after: None,
90            widows: 2,
91            orphans: 2,
92        }
93    }
94
95    /// Set the area's traits
96    pub fn with_traits(mut self, traits: TraitSet) -> Self {
97        self.traits = traits;
98        self
99    }
100
101    /// Set the area's keep constraint
102    pub fn with_keep_constraint(mut self, keep_constraint: crate::layout::KeepConstraint) -> Self {
103        self.keep_constraint = Some(keep_constraint);
104        self
105    }
106
107    /// Set the area's break-before value
108    pub fn with_break_before(mut self, break_before: crate::layout::BreakValue) -> Self {
109        self.break_before = Some(break_before);
110        self
111    }
112
113    /// Set the area's break-after value
114    pub fn with_break_after(mut self, break_after: crate::layout::BreakValue) -> Self {
115        self.break_after = Some(break_after);
116        self
117    }
118
119    /// Set the area's widows constraint
120    pub fn with_widows(mut self, widows: i32) -> Self {
121        self.widows = widows;
122        self
123    }
124
125    /// Set the area's orphans constraint
126    pub fn with_orphans(mut self, orphans: i32) -> Self {
127        self.orphans = orphans;
128        self
129    }
130
131    /// Check if this area contains text
132    pub fn has_text(&self) -> bool {
133        matches!(self.content, Some(AreaContent::Text(_)))
134    }
135
136    /// Check if this area contains image data
137    pub fn has_image_data(&self) -> bool {
138        matches!(self.content, Some(AreaContent::ImageData(_)))
139    }
140
141    /// Get text content if this is a text area
142    pub fn text_content(&self) -> Option<&str> {
143        match &self.content {
144            Some(AreaContent::Text(s)) => Some(s),
145            _ => None,
146        }
147    }
148
149    /// Get image data if this is an image area
150    pub fn image_data(&self) -> Option<&[u8]> {
151        match &self.content {
152            Some(AreaContent::ImageData(data)) => Some(data),
153            _ => None,
154        }
155    }
156
157    /// Get the area's width
158    pub fn width(&self) -> Length {
159        self.geometry.width
160    }
161
162    /// Get the area's height
163    pub fn height(&self) -> Length {
164        self.geometry.height
165    }
166}
167
168/// Types of areas in the area tree
169#[derive(Debug, Clone, Copy, PartialEq, Eq)]
170pub enum AreaType {
171    /// Page area - represents a physical page
172    Page,
173
174    /// Region area - represents a page region (body, before, after, start, end)
175    Region,
176
177    /// Header area - static content at top of page
178    Header,
179
180    /// Footer area - static content at bottom of page
181    Footer,
182
183    /// Block area - block-level formatting context
184    Block,
185
186    /// Line area - contains inline areas
187    Line,
188
189    /// Inline area - inline-level content
190    Inline,
191
192    /// Text area - actual text content
193    Text,
194
195    /// Space area - whitespace
196    Space,
197
198    /// Viewport area - for images, SVG, etc.
199    Viewport,
200
201    /// Footnote area - footnote content
202    Footnote,
203
204    /// Footnote separator area - line above footnotes
205    FootnoteSeparator,
206
207    /// Column area - represents a column in multi-column layout
208    Column,
209
210    /// Float area - floating element (like CSS floats)
211    FloatArea,
212
213    /// Sidebar start area - static content on the start (left) side of the page
214    SidebarStart,
215
216    /// Sidebar end area - static content on the end (right) side of the page
217    SidebarEnd,
218}
219
220/// Rendering traits for an area
221///
222/// These are the properties that affect rendering (color, background, borders, etc.)
223#[derive(Debug, Clone, Default)]
224pub struct TraitSet {
225    /// Text color
226    pub color: Option<Color>,
227
228    /// Background color
229    pub background_color: Option<Color>,
230
231    /// Font family
232    pub font_family: Option<String>,
233
234    /// Font size
235    pub font_size: Option<Length>,
236
237    /// Font weight (100-900, 400 = normal, 700 = bold)
238    pub font_weight: Option<u16>,
239
240    /// Font style (normal, italic, oblique)
241    pub font_style: Option<FontStyle>,
242
243    /// Text decoration
244    pub text_decoration: Option<TextDecoration>,
245
246    /// Border widths (top, right, bottom, left)
247    pub border_width: Option<[Length; 4]>,
248
249    /// Border colors (top, right, bottom, left)
250    pub border_color: Option<[Color; 4]>,
251
252    /// Border styles (top, right, bottom, left)
253    pub border_style: Option<[BorderStyle; 4]>,
254
255    /// Padding (top, right, bottom, left)
256    pub padding: Option<[Length; 4]>,
257
258    /// Text alignment
259    pub text_align: Option<TextAlign>,
260
261    /// Link destination (for hyperlinks)
262    pub link_destination: Option<String>,
263
264    /// Leader pattern (dots, rule, space, use-content)
265    pub is_leader: Option<String>,
266
267    /// Rule thickness (for rule leaders)
268    pub rule_thickness: Option<Length>,
269
270    /// Rule style (for rule leaders: solid, dashed, dotted)
271    pub rule_style: Option<String>,
272
273    /// Line height
274    pub line_height: Option<Length>,
275
276    /// Letter spacing (extra space between characters)
277    pub letter_spacing: Option<Length>,
278
279    /// Word spacing (extra space between words)
280    pub word_spacing: Option<Length>,
281
282    /// Border radius for rounded corners (top-left, top-right, bottom-right, bottom-left)
283    /// Each corner can have independent radius values
284    pub border_radius: Option<[Length; 4]>,
285
286    /// Overflow behavior - controls clipping of content
287    pub overflow: Option<OverflowBehavior>,
288
289    /// Opacity - transparency level (0.0 = transparent, 1.0 = opaque)
290    pub opacity: Option<f64>,
291
292    /// Text transformation
293    pub text_transform: Option<TextTransform>,
294
295    /// Font variant
296    pub font_variant: Option<FontVariant>,
297
298    /// Display alignment (vertical alignment)
299    pub display_align: Option<DisplayAlign>,
300
301    /// Baseline shift for inline positioning (positive = up, negative = down, as fraction of font-size)
302    pub baseline_shift: Option<f64>,
303
304    /// Whether hyphenation is enabled
305    pub hyphenate: Option<bool>,
306
307    /// Minimum word length before hyphenation (hyphenation-minimum-word-count)
308    pub hyphenation_min_word_chars: Option<u32>,
309
310    /// Characters before hyphen (hyphenation-push-character-count)
311    pub hyphenation_push_chars: Option<u32>,
312
313    /// Characters after hyphen (hyphenation-remain-character-count)
314    pub hyphenation_remain_chars: Option<u32>,
315
316    /// Font stretch (condensed/expanded)
317    pub font_stretch: Option<FontStretch>,
318
319    /// Text alignment for last line (used with justify)
320    pub text_align_last: Option<TextAlign>,
321
322    /// Change bar color (for margin rule rendering)
323    pub change_bar_color: Option<fop_types::Color>,
324
325    /// Span property (none or all columns)
326    pub span: Span,
327
328    /// Role attribute for accessibility tagging (PDF/UA)
329    pub role: Option<String>,
330
331    /// Language attribute (xml:lang) for language tagging
332    pub xml_lang: Option<String>,
333
334    /// Writing mode (lr-tb, rl-tb, tb-rl, tb-lr)
335    pub writing_mode: WritingMode,
336
337    /// Text direction (ltr, rtl)
338    pub direction: Direction,
339}
340
341/// Writing mode values for XSL-FO
342#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
343pub enum WritingMode {
344    /// Left-to-right, top-to-bottom (default Western text)
345    #[default]
346    LrTb,
347    /// Right-to-left, top-to-bottom (Arabic/Hebrew)
348    RlTb,
349    /// Top-to-bottom, right-to-left (Traditional CJK vertical)
350    TbRl,
351    /// Top-to-bottom, left-to-right (Modern CJK vertical)
352    TbLr,
353}
354
355/// Text direction values
356#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
357pub enum Direction {
358    /// Left-to-right (default)
359    #[default]
360    Ltr,
361    /// Right-to-left (Arabic/Hebrew)
362    Rtl,
363}
364
365/// Span values for fo:block column spanning
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
367pub enum Span {
368    /// Block stays in current column (default)
369    #[default]
370    None,
371    /// Block spans all columns
372    All,
373}
374
375/// Font style values
376#[derive(Debug, Clone, Copy, PartialEq, Eq)]
377pub enum FontStyle {
378    Normal,
379    Italic,
380    Oblique,
381}
382
383/// Border style values (XSL-FO and CSS compatible)
384#[derive(Debug, Clone, Copy, PartialEq, Eq)]
385pub enum BorderStyle {
386    /// No border
387    None,
388    /// Solid line
389    Solid,
390    /// Dashed line
391    Dashed,
392    /// Dotted line
393    Dotted,
394    /// Double line
395    Double,
396    /// 3D grooved border
397    Groove,
398    /// 3D ridged border
399    Ridge,
400    /// 3D inset border
401    Inset,
402    /// 3D outset border
403    Outset,
404    /// Hidden border (same as none, but for border conflict resolution)
405    Hidden,
406}
407
408/// Text decoration values
409#[derive(Debug, Clone, Copy, PartialEq, Eq)]
410pub struct TextDecoration {
411    pub underline: bool,
412    pub overline: bool,
413    pub line_through: bool,
414}
415
416impl TextDecoration {
417    pub const NONE: Self = Self {
418        underline: false,
419        overline: false,
420        line_through: false,
421    };
422
423    pub const UNDERLINE: Self = Self {
424        underline: true,
425        overline: false,
426        line_through: false,
427    };
428}
429
430/// Text transformation mode
431#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
432pub enum TextTransform {
433    #[default]
434    None,
435    Uppercase,
436    Lowercase,
437    Capitalize,
438}
439
440/// Font variant
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
442pub enum FontVariant {
443    #[default]
444    Normal,
445    SmallCaps,
446}
447
448/// Font stretch values
449#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
450pub enum FontStretch {
451    UltraCondensed,
452    ExtraCondensed,
453    Condensed,
454    SemiCondensed,
455    #[default]
456    Normal,
457    SemiExpanded,
458    Expanded,
459    ExtraExpanded,
460    UltraExpanded,
461}
462
463/// Display alignment (vertical alignment within a block area)
464#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
465pub enum DisplayAlign {
466    #[default]
467    Before,
468    Center,
469    After,
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475    use fop_types::{Length, Point, Size};
476
477    #[test]
478    fn test_area_creation() {
479        let rect = Rect::from_point_size(
480            Point::new(Length::from_pt(10.0), Length::from_pt(20.0)),
481            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
482        );
483
484        let area = Area::new(AreaType::Block, rect);
485
486        assert_eq!(area.area_type, AreaType::Block);
487        assert_eq!(area.width(), Length::from_pt(100.0));
488        assert_eq!(area.height(), Length::from_pt(50.0));
489        assert!(!area.has_text());
490    }
491
492    #[test]
493    fn test_text_area() {
494        let rect = Rect::from_point_size(
495            Point::ZERO,
496            Size::new(Length::from_pt(50.0), Length::from_pt(12.0)),
497        );
498
499        let area = Area::text(rect, "Hello".to_string());
500
501        assert_eq!(area.area_type, AreaType::Text);
502        assert!(area.has_text());
503        assert_eq!(area.text_content().expect("test: should succeed"), "Hello");
504    }
505
506    #[test]
507    fn test_traits() {
508        let traits = TraitSet {
509            color: Some(Color::RED),
510            font_size: Some(Length::from_pt(12.0)),
511            ..Default::default()
512        };
513
514        assert_eq!(traits.color, Some(Color::RED));
515        assert_eq!(traits.font_size, Some(Length::from_pt(12.0)));
516    }
517}
518
519#[cfg(test)]
520mod extended_tests {
521    use super::*;
522    use fop_types::{Length, Point, Rect, Size};
523
524    // ---- Area builder method tests ----
525
526    #[test]
527    fn test_area_with_traits() {
528        let rect = Rect::from_point_size(
529            Point::ZERO,
530            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
531        );
532        let traits = TraitSet {
533            font_size: Some(Length::from_pt(14.0)),
534            ..Default::default()
535        };
536        let area = Area::new(AreaType::Block, rect).with_traits(traits);
537        assert_eq!(area.traits.font_size, Some(Length::from_pt(14.0)));
538    }
539
540    #[test]
541    fn test_area_widows_orphans_defaults() {
542        let rect = Rect::from_point_size(
543            Point::ZERO,
544            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
545        );
546        let area = Area::new(AreaType::Block, rect);
547        assert_eq!(area.widows, 2);
548        assert_eq!(area.orphans, 2);
549    }
550
551    #[test]
552    fn test_area_with_widows_and_orphans() {
553        let rect = Rect::from_point_size(
554            Point::ZERO,
555            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
556        );
557        let area = Area::new(AreaType::Block, rect)
558            .with_widows(4)
559            .with_orphans(3);
560        assert_eq!(area.widows, 4);
561        assert_eq!(area.orphans, 3);
562    }
563
564    #[test]
565    fn test_area_break_before_none_by_default() {
566        let rect = Rect::from_point_size(
567            Point::ZERO,
568            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
569        );
570        let area = Area::new(AreaType::Block, rect);
571        assert!(area.break_before.is_none());
572        assert!(area.break_after.is_none());
573    }
574
575    #[test]
576    fn test_area_with_break_before() {
577        use crate::layout::BreakValue;
578        let rect = Rect::from_point_size(
579            Point::ZERO,
580            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
581        );
582        let area = Area::new(AreaType::Block, rect).with_break_before(BreakValue::Page);
583        assert!(area.break_before.is_some());
584    }
585
586    #[test]
587    fn test_area_with_break_after() {
588        use crate::layout::BreakValue;
589        let rect = Rect::from_point_size(
590            Point::ZERO,
591            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
592        );
593        let area = Area::new(AreaType::Block, rect).with_break_after(BreakValue::Page);
594        assert!(area.break_after.is_some());
595    }
596
597    #[test]
598    fn test_area_viewport_with_image() {
599        let rect = Rect::from_point_size(
600            Point::ZERO,
601            Size::new(Length::from_pt(50.0), Length::from_pt(50.0)),
602        );
603        let image_data = vec![0u8, 1, 2, 3, 4];
604        let area = Area::viewport_with_image(rect, image_data.clone());
605        assert_eq!(area.area_type, AreaType::Viewport);
606        assert!(area.has_image_data());
607        assert_eq!(
608            area.image_data().expect("test: should succeed"),
609            image_data.as_slice()
610        );
611    }
612
613    #[test]
614    fn test_area_text_content_none_for_non_text() {
615        let rect = Rect::from_point_size(
616            Point::ZERO,
617            Size::new(Length::from_pt(100.0), Length::from_pt(50.0)),
618        );
619        let area = Area::new(AreaType::Block, rect);
620        assert!(area.text_content().is_none());
621        assert!(!area.has_text());
622        assert!(!area.has_image_data());
623    }
624
625    #[test]
626    fn test_area_image_data_none_for_text_area() {
627        let rect = Rect::from_point_size(
628            Point::ZERO,
629            Size::new(Length::from_pt(50.0), Length::from_pt(12.0)),
630        );
631        let area = Area::text(rect, "Test".to_string());
632        assert!(area.image_data().is_none());
633    }
634
635    // ---- TraitSet field tests ----
636
637    #[test]
638    fn test_traitset_default_all_none() {
639        let traits = TraitSet::default();
640        assert!(traits.color.is_none());
641        assert!(traits.background_color.is_none());
642        assert!(traits.font_family.is_none());
643        assert!(traits.font_size.is_none());
644        assert!(traits.font_weight.is_none());
645        assert!(traits.font_style.is_none());
646        assert!(traits.text_decoration.is_none());
647        assert!(traits.border_width.is_none());
648        assert!(traits.padding.is_none());
649        assert!(traits.text_align.is_none());
650        assert!(traits.line_height.is_none());
651        assert!(traits.letter_spacing.is_none());
652        assert!(traits.word_spacing.is_none());
653    }
654
655    #[test]
656    fn test_traitset_writing_mode_default_lr_tb() {
657        let traits = TraitSet::default();
658        assert_eq!(traits.writing_mode, WritingMode::LrTb);
659    }
660
661    #[test]
662    fn test_traitset_direction_default_ltr() {
663        let traits = TraitSet::default();
664        assert_eq!(traits.direction, Direction::Ltr);
665    }
666
667    #[test]
668    fn test_traitset_span_default_none() {
669        let traits = TraitSet::default();
670        assert_eq!(traits.span, Span::None);
671    }
672
673    #[test]
674    fn test_traitset_display_align_default_before() {
675        let traits = TraitSet::default();
676        // display_align is Option<DisplayAlign>, defaults to None
677        assert_eq!(traits.display_align, None);
678    }
679
680    #[test]
681    fn test_traitset_font_variant_default_normal() {
682        let traits = TraitSet::default();
683        // font_variant is Option<FontVariant>, defaults to None
684        assert_eq!(traits.font_variant, None);
685    }
686
687    #[test]
688    fn test_traitset_text_transform_default_none() {
689        let traits = TraitSet::default();
690        // text_transform is Option<TextTransform>, defaults to None
691        assert_eq!(traits.text_transform, None);
692    }
693
694    #[test]
695    fn test_traitset_font_stretch_default_normal() {
696        let traits = TraitSet::default();
697        // font_stretch is Option<FontStretch>, defaults to None
698        assert_eq!(traits.font_stretch, None);
699    }
700
701    // ---- AreaType coverage tests ----
702
703    #[test]
704    fn test_area_types_are_distinct() {
705        assert_ne!(AreaType::Page, AreaType::Region);
706        assert_ne!(AreaType::Block, AreaType::Line);
707        assert_ne!(AreaType::Inline, AreaType::Text);
708        assert_ne!(AreaType::Header, AreaType::Footer);
709        assert_ne!(AreaType::Column, AreaType::Footnote);
710    }
711
712    #[test]
713    fn test_area_width_and_height() {
714        let rect = Rect::from_point_size(
715            Point::new(Length::from_pt(5.0), Length::from_pt(10.0)),
716            Size::new(Length::from_pt(200.0), Length::from_pt(100.0)),
717        );
718        let area = Area::new(AreaType::Page, rect);
719        assert_eq!(area.width(), Length::from_pt(200.0));
720        assert_eq!(area.height(), Length::from_pt(100.0));
721    }
722
723    // ---- TextDecoration tests ----
724
725    #[test]
726    fn test_text_decoration_none() {
727        let td = TextDecoration::NONE;
728        assert!(!td.underline);
729        assert!(!td.overline);
730        assert!(!td.line_through);
731    }
732
733    #[test]
734    fn test_text_decoration_underline() {
735        let td = TextDecoration::UNDERLINE;
736        assert!(td.underline);
737        assert!(!td.overline);
738        assert!(!td.line_through);
739    }
740
741    #[test]
742    fn test_text_decoration_custom() {
743        let td = TextDecoration {
744            underline: false,
745            overline: true,
746            line_through: true,
747        };
748        assert!(!td.underline);
749        assert!(td.overline);
750        assert!(td.line_through);
751    }
752
753    // ---- FontStyle / BorderStyle enum tests ----
754
755    #[test]
756    fn test_font_style_variants() {
757        assert_ne!(FontStyle::Normal, FontStyle::Italic);
758        assert_ne!(FontStyle::Italic, FontStyle::Oblique);
759        assert_ne!(FontStyle::Normal, FontStyle::Oblique);
760    }
761
762    #[test]
763    fn test_border_style_variants() {
764        assert_ne!(BorderStyle::None, BorderStyle::Solid);
765        assert_ne!(BorderStyle::Dashed, BorderStyle::Dotted);
766        assert_ne!(BorderStyle::Hidden, BorderStyle::None);
767    }
768}