Skip to main content

ppt_rs/core/
traits.rs

1//! Core traits for PPTX elements
2//!
3//! These traits provide a consistent interface for XML generation
4//! and element manipulation across the library.
5
6/// Trait for types that can be converted to XML
7pub trait ToXml {
8    /// Generate XML representation of this element
9    fn to_xml(&self) -> String;
10    
11    /// Write XML to a string buffer (more efficient for large documents)
12    fn write_xml(&self, writer: &mut String) {
13        writer.push_str(&self.to_xml());
14    }
15}
16
17/// Trait for XML elements with a tag name
18pub trait XmlElement: ToXml {
19    /// Get the XML tag name for this element
20    fn tag_name(&self) -> &'static str;
21    
22    /// Get XML namespace prefix (e.g., "a", "p", "r")
23    fn namespace_prefix(&self) -> &'static str {
24        ""
25    }
26    
27    /// Get the fully qualified tag name
28    fn qualified_name(&self) -> String {
29        let prefix = self.namespace_prefix();
30        if prefix.is_empty() {
31            self.tag_name().to_string()
32        } else {
33            format!("{}:{}", prefix, self.tag_name())
34        }
35    }
36}
37
38/// Trait for positioned elements (x, y coordinates)
39pub trait Positioned {
40    /// Get X position in EMU
41    fn x(&self) -> u32;
42    
43    /// Get Y position in EMU
44    fn y(&self) -> u32;
45    
46    /// Set position
47    fn set_position(&mut self, x: u32, y: u32);
48}
49
50/// Trait for sized elements (width, height)
51pub trait Sized {
52    /// Get width in EMU
53    fn width(&self) -> u32;
54    
55    /// Get height in EMU
56    fn height(&self) -> u32;
57    
58    /// Set size
59    fn set_size(&mut self, width: u32, height: u32);
60}
61
62/// Trait for styled elements (color, formatting)
63pub trait Styled {
64    /// Get the primary color (if any)
65    fn color(&self) -> Option<&str>;
66    
67    /// Set the primary color
68    fn set_color(&mut self, color: &str);
69}
70
71/// RGB color representation
72#[derive(Clone, Debug, PartialEq, Eq)]
73#[allow(dead_code)]
74pub struct RgbColor {
75    pub r: u8,
76    pub g: u8,
77    pub b: u8,
78}
79
80#[allow(dead_code)]
81impl RgbColor {
82    pub fn new(r: u8, g: u8, b: u8) -> Self {
83        Self { r, g, b }
84    }
85    
86    /// Parse from hex string (e.g., "FF0000" or "#FF0000")
87    pub fn from_hex(hex: &str) -> Option<Self> {
88        let hex = hex.trim_start_matches('#');
89        if hex.len() != 6 {
90            return None;
91        }
92        let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
93        let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
94        let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
95        Some(Self { r, g, b })
96    }
97    
98    /// Convert to hex string (uppercase, no #)
99    pub fn to_hex(&self) -> String {
100        format!("{:02X}{:02X}{:02X}", self.r, self.g, self.b)
101    }
102}
103
104impl ToXml for RgbColor {
105    fn to_xml(&self) -> String {
106        format!(r#"<a:srgbClr val="{}"/>"#, self.to_hex())
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn test_rgb_color_from_hex() {
116        let color = RgbColor::from_hex("FF0000").unwrap();
117        assert_eq!(color.r, 255);
118        assert_eq!(color.g, 0);
119        assert_eq!(color.b, 0);
120        
121        let color = RgbColor::from_hex("#00FF00").unwrap();
122        assert_eq!(color.to_hex(), "00FF00");
123    }
124
125    #[test]
126    fn test_rgb_color_to_xml() {
127        let color = RgbColor::new(255, 0, 0);
128        assert_eq!(color.to_xml(), r#"<a:srgbClr val="FF0000"/>"#);
129    }
130
131    /// Verify generic dispatch works via ToXml trait objects
132    #[test]
133    fn test_to_xml_trait_dispatch() {
134        use crate::generator::text::{Run, Paragraph, TextFrame};
135
136        let items: Vec<Box<dyn ToXml>> = vec![
137            Box::new(Run::new("hello")),
138            Box::new(Paragraph::with_text("world")),
139            Box::new(TextFrame::with_text("frame")),
140            Box::new(RgbColor::new(255, 0, 0)),
141        ];
142
143        for item in &items {
144            let xml = item.to_xml();
145            assert!(!xml.is_empty(), "ToXml dispatch should produce non-empty XML");
146        }
147
148        assert!(items[0].to_xml().contains("hello"));
149        assert!(items[1].to_xml().contains("world"));
150        assert!(items[2].to_xml().contains("frame"));
151        assert!(items[3].to_xml().contains("FF0000"));
152    }
153
154    /// Verify Positioned trait works generically
155    #[test]
156    fn test_positioned_trait_dispatch() {
157        use crate::generator::shapes::{Shape, ShapeType};
158        use crate::generator::images::Image;
159
160        fn move_element(elem: &mut dyn Positioned, x: u32, y: u32) {
161            elem.set_position(x, y);
162        }
163
164        let mut shape = Shape::new(ShapeType::Rectangle, 0, 0, 1000, 1000);
165        let mut image = Image::new("test.png", 500, 500, "PNG");
166
167        move_element(&mut shape, 100, 200);
168        move_element(&mut image, 300, 400);
169
170        assert_eq!(shape.x(), 100);
171        assert_eq!(shape.y(), 200);
172        assert_eq!(image.x(), 300);
173        assert_eq!(image.y(), 400);
174    }
175
176    /// Verify ElementSized trait works generically
177    #[test]
178    fn test_element_sized_trait_dispatch() {
179        use crate::generator::shapes::{Shape, ShapeType};
180        use crate::generator::images::Image;
181
182        fn resize(elem: &mut dyn Sized, w: u32, h: u32) {
183            elem.set_size(w, h);
184        }
185
186        let mut shape = Shape::new(ShapeType::Rectangle, 0, 0, 1000, 1000);
187        let mut image = Image::new("test.png", 500, 500, "PNG");
188
189        resize(&mut shape, 2000, 3000);
190        resize(&mut image, 4000, 5000);
191
192        assert_eq!(shape.width(), 2000);
193        assert_eq!(shape.height(), 3000);
194        assert_eq!(image.width(), 4000);
195        assert_eq!(image.height(), 5000);
196    }
197}