Skip to main content

ppt_rs/generator/layouts/
title_content.rs

1//! Title and content slide layouts
2
3use super::common::{SlideXmlBuilder, generate_text_props, escape_xml, ShapePosition, TextContent};
4use crate::generator::slide_content::SlideContent;
5use crate::generator::shapes_xml::generate_shape_xml;
6use crate::generator::constants::{
7    TITLE_X, TITLE_Y, TITLE_WIDTH, TITLE_HEIGHT, TITLE_HEIGHT_BIG,
8    CONTENT_X, CONTENT_Y_START, CONTENT_Y_START_BIG,
9    CONTENT_WIDTH, CONTENT_HEIGHT, CONTENT_HEIGHT_BIG,
10    TITLE_FONT_SIZE, CONTENT_FONT_SIZE,
11};
12
13/// Standard title and content layout
14pub struct TitleContentLayout;
15
16impl TitleContentLayout {
17    /// Generate title and content slide XML
18    pub fn generate(content: &SlideContent) -> String {
19        let title_size = content.title_size.unwrap_or(TITLE_FONT_SIZE / 100 * 100);
20        let content_size = content.content_size.unwrap_or(CONTENT_FONT_SIZE / 100 * 100);
21
22        let title_props = generate_text_props(
23            title_size,
24            content.title_bold,
25            content.title_italic,
26            content.title_underline,
27            content.title_color.as_deref(),
28        );
29
30        let content_props = generate_text_props(
31            content_size,
32            content.content_bold,
33            content.content_italic,
34            content.content_underline,
35            content.content_color.as_deref(),
36        );
37
38        let mut builder = SlideXmlBuilder::new()
39            .start_slide_with_bg()
40            .start_sp_tree()
41            .add_title(2, ShapePosition::new(TITLE_X, TITLE_Y, TITLE_WIDTH, TITLE_HEIGHT), TextContent::new(&content.title, &title_props), "title");
42
43        // Add table or bullets
44        if let Some(ref table) = content.table {
45            builder = builder.raw(&crate::generator::table::generate_table_xml(table, 3));
46        } else if !content.bullets.is_empty() {
47            // Use bullets with styles
48            builder = builder.start_content_body(3, CONTENT_X, CONTENT_Y_START, CONTENT_WIDTH, CONTENT_HEIGHT);
49            for bullet in &content.bullets {
50                builder = builder.add_bullet_with_style(&bullet.text, &content_props, bullet.level, bullet.style);
51            }
52            builder = builder.end_content_body();
53        } else if !content.content.is_empty() {
54            // Fallback to plain content strings
55            builder = builder.start_content_body(3, CONTENT_X, CONTENT_Y_START, CONTENT_WIDTH, CONTENT_HEIGHT);
56            for bullet in &content.content {
57                builder = builder.add_bullet_with_style(bullet, &content_props, 0, content.bullet_style);
58            }
59            builder = builder.end_content_body();
60        }
61
62        // Add shapes
63        for (i, shape) in content.shapes.iter().enumerate() {
64            builder = builder.raw("\n").raw(&generate_shape_xml(shape, (i + 10) as u32));
65        }
66
67        // Add image placeholders
68        let image_start_id = 20 + content.shapes.len();
69        for (i, image) in content.images.iter().enumerate() {
70            builder = builder.raw(&format!(
71                r#"
72<p:sp>
73<p:nvSpPr>
74<p:cNvPr id="{}" name="Image Placeholder: {}"/>
75<p:cNvSpPr/>
76<p:nvPr/>
77</p:nvSpPr>
78<p:spPr>
79<a:xfrm>
80<a:off x="{}" y="{}"/>
81<a:ext cx="{}" cy="{}"/>
82</a:xfrm>
83<a:prstGeom prst="rect"><a:avLst/></a:prstGeom>
84<a:solidFill><a:srgbClr val="E0E0E0"/></a:solidFill>
85<a:ln w="12700"><a:solidFill><a:srgbClr val="808080"/></a:solidFill></a:ln>
86</p:spPr>
87<p:txBody>
88<a:bodyPr wrap="square" rtlCol="0" anchor="ctr"/>
89<a:lstStyle/>
90<a:p>
91<a:pPr algn="ctr"/>
92<a:r>
93<a:rPr lang="en-US" sz="1400"/>
94<a:t>📷 {}</a:t>
95</a:r>
96</a:p>
97</p:txBody>
98</p:sp>"#,
99                image_start_id + i,
100                escape_xml(&image.filename),
101                image.x,
102                image.y,
103                image.width,
104                image.height,
105                escape_xml(&image.filename)
106            ));
107        }
108
109        builder
110            .end_sp_tree()
111            .end_slide()
112            .build()
113    }
114}
115
116/// Title and big content layout (smaller title, larger content area)
117pub struct TitleBigContentLayout;
118
119impl TitleBigContentLayout {
120    /// Generate title and big content slide XML
121    pub fn generate(content: &SlideContent) -> String {
122        let title_size = content.title_size.unwrap_or(TITLE_FONT_SIZE / 100 * 100);
123        let content_size = content.content_size.unwrap_or(CONTENT_FONT_SIZE / 100 * 100);
124
125        let title_props = generate_text_props(
126            title_size,
127            content.title_bold,
128            content.title_italic,
129            content.title_underline,
130            content.title_color.as_deref(),
131        );
132
133        let content_props = generate_text_props(
134            content_size,
135            content.content_bold,
136            content.content_italic,
137            content.content_underline,
138            content.content_color.as_deref(),
139        );
140
141        let mut builder = SlideXmlBuilder::new()
142            .start_slide_with_bg()
143            .start_sp_tree()
144            .add_title(2, ShapePosition::new(TITLE_X, TITLE_Y, TITLE_WIDTH, TITLE_HEIGHT_BIG), TextContent::new(&content.title, &title_props), "title");
145
146        if !content.bullets.is_empty() {
147            builder = builder.start_content_body(3, CONTENT_X, CONTENT_Y_START_BIG, CONTENT_WIDTH, CONTENT_HEIGHT_BIG);
148            for bullet in &content.bullets {
149                builder = builder.add_bullet_with_style(&bullet.text, &content_props, bullet.level, bullet.style);
150            }
151            builder = builder.end_content_body();
152        } else if !content.content.is_empty() {
153            builder = builder.start_content_body(3, CONTENT_X, CONTENT_Y_START_BIG, CONTENT_WIDTH, CONTENT_HEIGHT_BIG);
154            for bullet in &content.content {
155                builder = builder.add_bullet_with_style(bullet, &content_props, 0, content.bullet_style);
156            }
157            builder = builder.end_content_body();
158        }
159
160        builder
161            .end_sp_tree()
162            .end_slide()
163            .build()
164    }
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_title_content_layout() {
173        let content = SlideContent::new("Test")
174            .add_bullet("Point 1")
175            .add_bullet("Point 2");
176        let xml = TitleContentLayout::generate(&content);
177        
178        assert!(xml.contains("Test"));
179        assert!(xml.contains("Point 1"));
180        assert!(xml.contains("Point 2"));
181    }
182
183    #[test]
184    fn test_title_big_content_layout() {
185        let content = SlideContent::new("Big Content")
186            .add_bullet("Item");
187        let xml = TitleBigContentLayout::generate(&content);
188        
189        assert!(xml.contains("Big Content"));
190        assert!(xml.contains("5668800")); // Larger content height
191    }
192}