Skip to main content

dimension_demo/
dimension_demo.rs

1//! Dimension API Demo — Flexible Positioning & Sizing
2//!
3//! Showcases all Dimension unit types and fluent APIs:
4//! - EMU, Inches, Cm, Pt, Ratio, Percent
5//! - Shape::from_dimensions(), .at(), .with_dimensions()
6//! - Image::at(), .with_dimensions()
7//! - Prelude helpers: shapes::dim(), shapes::rect_ratio(), shapes::text_box_ratio()
8//! - FlexPosition / FlexSize structs
9//! - Mixed-unit positioning
10//!
11//! Run with: cargo run --example dimension_demo
12
13use ppt_rs::generator::{
14    create_pptx_with_content, SlideContent, SlideLayout,
15    Shape, ShapeType, ShapeFill, ShapeLine,
16};
17use ppt_rs::core::{Dimension, FlexPosition, FlexSize, SLIDE_WIDTH_EMU, SLIDE_HEIGHT_EMU};
18use ppt_rs::prelude::shapes;
19use std::fs;
20
21fn main() -> Result<(), Box<dyn std::error::Error>> {
22    println!("╔══════════════════════════════════════════════════════════════╗");
23    println!("║       Dimension API Demo — Flexible Positioning & Sizing     ║");
24    println!("╚══════════════════════════════════════════════════════════════╝\n");
25
26    let mut slides = Vec::new();
27
28    // =========================================================================
29    // SLIDE 1: Title
30    // =========================================================================
31    slides.push(
32        SlideContent::new("Dimension API — Flexible Positioning & Sizing")
33            .layout(SlideLayout::CenteredTitle)
34            .title_size(44)
35            .title_bold(true)
36            .title_color("1F497D")
37    );
38
39    // =========================================================================
40    // SLIDE 2: All Unit Types Side-by-Side
41    // =========================================================================
42    println!("📏 Slide 2: All Unit Types");
43
44    // Each shape is 1 inch wide, positioned using a different unit type
45    let emu_shape = Shape::from_dimensions(ShapeType::Rectangle,
46        Dimension::Emu(457200), Dimension::Inches(1.5),
47        Dimension::Emu(1371600), Dimension::Inches(0.8),
48    ).with_fill(ShapeFill::new("1565C0")).with_text("EMU");
49
50    let inch_shape = Shape::from_dimensions(ShapeType::Rectangle,
51        Dimension::Inches(2.0), Dimension::Inches(1.5),
52        Dimension::Inches(1.5), Dimension::Inches(0.8),
53    ).with_fill(ShapeFill::new("2E7D32")).with_text("Inches");
54
55    let cm_shape = Shape::from_dimensions(ShapeType::Rectangle,
56        Dimension::Cm(9.0), Dimension::Inches(1.5),
57        Dimension::Cm(3.81), Dimension::Inches(0.8),
58    ).with_fill(ShapeFill::new("C62828")).with_text("Cm");
59
60    let pt_shape = Shape::from_dimensions(ShapeType::Rectangle,
61        Dimension::Pt(324.0), Dimension::Inches(1.5),
62        Dimension::Pt(108.0), Dimension::Inches(0.8),
63    ).with_fill(ShapeFill::new("7B1FA2")).with_text("Pt");
64
65    let ratio_shape = Shape::from_dimensions(ShapeType::Rectangle,
66        Dimension::Ratio(0.52), Dimension::Inches(1.5),
67        Dimension::Ratio(0.15), Dimension::Inches(0.8),
68    ).with_fill(ShapeFill::new("EF6C00")).with_text("Ratio");
69
70    let pct_shape = Shape::from_dimensions(ShapeType::Rectangle,
71        Dimension::percent(69.0), Dimension::Inches(1.5),
72        Dimension::percent(15.0), Dimension::Inches(0.8),
73    ).with_fill(ShapeFill::new("00838F")).with_text("Percent");
74
75    // Labels row
76    let label = Shape::from_dimensions(ShapeType::Rectangle,
77        Dimension::Inches(0.5), Dimension::Inches(0.8),
78        Dimension::Inches(9.0), Dimension::Inches(0.5),
79    ).with_text("Each shape below uses a different unit type for X position:");
80
81    slides.push(
82        SlideContent::new("All Dimension Unit Types")
83            .layout(SlideLayout::TitleOnly)
84            .title_color("1F497D").title_bold(true)
85            .add_shape(label)
86            .add_shape(emu_shape)
87            .add_shape(inch_shape)
88            .add_shape(cm_shape)
89            .add_shape(pt_shape)
90            .add_shape(ratio_shape)
91            .add_shape(pct_shape)
92    );
93
94    // =========================================================================
95    // SLIDE 3: Ratio-Based Grid Layout
96    // =========================================================================
97    println!("📐 Slide 3: Ratio-Based Grid (auto-adapts to slide size)");
98
99    let margin = 0.03;  // 3% margin
100    let gap = 0.02;     // 2% gap
101    let cell_w = (1.0 - 2.0 * margin - 2.0 * gap) / 3.0;
102    let cell_h = (0.7 - 2.0 * gap) / 3.0;  // 70% of slide height for grid
103    let y_start = 0.22; // below title
104
105    let colors = [
106        "1565C0", "2E7D32", "C62828",
107        "7B1FA2", "EF6C00", "00838F",
108        "AD1457", "4E342E", "37474F",
109    ];
110    let labels = [
111        "Top-Left", "Top-Center", "Top-Right",
112        "Mid-Left", "Mid-Center", "Mid-Right",
113        "Bot-Left", "Bot-Center", "Bot-Right",
114    ];
115
116    let mut grid_slide = SlideContent::new("Ratio-Based 3x3 Grid Layout")
117        .layout(SlideLayout::TitleOnly)
118        .title_color("1F497D").title_bold(true);
119
120    for row in 0..3 {
121        for col in 0..3 {
122            let idx = row * 3 + col;
123            let x = margin + col as f64 * (cell_w + gap);
124            let y = y_start + row as f64 * (cell_h + gap);
125            let shape = Shape::from_dimensions(ShapeType::RoundedRectangle,
126                Dimension::Ratio(x), Dimension::Ratio(y),
127                Dimension::Ratio(cell_w), Dimension::Ratio(cell_h),
128            ).with_fill(ShapeFill::new(colors[idx])).with_text(labels[idx]);
129            grid_slide = grid_slide.add_shape(shape);
130        }
131    }
132
133    slides.push(grid_slide);
134
135    // =========================================================================
136    // SLIDE 4: Mixed-Unit Positioning
137    // =========================================================================
138    println!("🔀 Slide 4: Mixed-Unit Positioning");
139
140    // Title area: inches for position, ratio for width
141    let title_box = Shape::from_dimensions(ShapeType::RoundedRectangle,
142        Dimension::Inches(0.5), Dimension::Inches(1.5),
143        Dimension::Ratio(0.9), Dimension::Cm(2.0),
144    ).with_fill(ShapeFill::new("1F497D")).with_text("Inches X + Ratio Width + Cm Height");
145
146    // Content area: cm for position, pt for size
147    let content_box = Shape::from_dimensions(ShapeType::Rectangle,
148        Dimension::Cm(2.0), Dimension::Cm(6.0),
149        Dimension::Pt(432.0), Dimension::Pt(108.0),  // 6in x 1.5in
150    ).with_fill(ShapeFill::new("2E7D32")).with_text("Cm position + Pt size");
151
152    // Footer area: percent for everything
153    let footer_box = Shape::from_dimensions(ShapeType::Rectangle,
154        Dimension::percent(5.0), Dimension::percent(75.0),
155        Dimension::percent(90.0), Dimension::percent(10.0),
156    ).with_fill(ShapeFill::new("C62828")).with_text("100% percent-based");
157
158    // Sidebar: EMU for position, inches for size
159    let sidebar = Shape::from_dimensions(ShapeType::Rectangle,
160        Dimension::Emu(8000000), Dimension::Inches(1.5),
161        Dimension::Inches(1.0), Dimension::Ratio(0.6),
162    ).with_fill(ShapeFill::new("7B1FA2")).with_text("EMU + Inches + Ratio");
163
164    slides.push(
165        SlideContent::new("Mixed-Unit Positioning")
166            .layout(SlideLayout::TitleOnly)
167            .title_color("1F497D").title_bold(true)
168            .add_shape(title_box)
169            .add_shape(content_box)
170            .add_shape(footer_box)
171            .add_shape(sidebar)
172    );
173
174    // =========================================================================
175    // SLIDE 5: Fluent .at() and .with_dimensions() Chaining
176    // =========================================================================
177    println!("🔗 Slide 5: Fluent Chaining API");
178
179    // Build shapes step by step with chaining
180    let shape1 = Shape::new(ShapeType::Ellipse, 0, 0, 0, 0)
181        .at(Dimension::percent(10.0), Dimension::percent(25.0))
182        .with_dimensions(Dimension::Inches(2.5), Dimension::Inches(2.5))
183        .with_fill(ShapeFill::new("1565C0"))
184        .with_text(".at() + .with_dimensions()");
185
186    let shape2 = Shape::new(ShapeType::RoundedRectangle, 0, 0, 0, 0)
187        .at(Dimension::Inches(4.0), Dimension::Cm(5.0))
188        .with_dimensions(Dimension::Ratio(0.3), Dimension::Inches(2.0))
189        .with_fill(ShapeFill::new("2E7D32"))
190        .with_line(ShapeLine::new("1B5E20", 25400))
191        .with_text("Chained with fill + line");
192
193    let shape3 = Shape::new(ShapeType::Star5, 0, 0, 0, 0)
194        .at(Dimension::percent(70.0), Dimension::percent(55.0))
195        .with_dimensions(Dimension::Inches(2.0), Dimension::Inches(2.0))
196        .with_fill(ShapeFill::new("FFC107"))
197        .with_rotation(15)
198        .with_text("+ rotation");
199
200    slides.push(
201        SlideContent::new("Fluent .at() and .with_dimensions() Chaining")
202            .layout(SlideLayout::TitleOnly)
203            .title_color("1F497D").title_bold(true)
204            .add_shape(shape1)
205            .add_shape(shape2)
206            .add_shape(shape3)
207    );
208
209    // =========================================================================
210    // SLIDE 6: Prelude Shape Builders
211    // =========================================================================
212    println!("🧰 Slide 6: Prelude Shape Builders");
213
214    // shapes::dim() — generic Dimension-based builder
215    let dim_shape = shapes::dim(ShapeType::Diamond,
216        Dimension::percent(5.0), Dimension::percent(25.0),
217        Dimension::percent(25.0), Dimension::percent(35.0),
218    ).with_fill(ShapeFill::new("7B1FA2")).with_text("shapes::dim()");
219
220    // shapes::rect_ratio() — ratio-based rectangle
221    let ratio_rect = shapes::rect_ratio(0.35, 0.25, 0.28, 0.35)
222        .with_fill(ShapeFill::new("EF6C00")).with_text("shapes::rect_ratio()");
223
224    // shapes::text_box_ratio() — ratio-based text box
225    let ratio_text = shapes::text_box_ratio(0.68, 0.25, 0.28, 0.35, "shapes::text_box_ratio()")
226        .with_fill(ShapeFill::new("00838F"));
227
228    // Traditional shapes::rect() still works (inches)
229    let inch_rect = shapes::rect(1.0, 5.0, 3.0, 1.0)
230        .with_fill(ShapeFill::new("A5A5A5")).with_text("shapes::rect() (inches)");
231
232    slides.push(
233        SlideContent::new("Prelude Shape Builders")
234            .layout(SlideLayout::TitleOnly)
235            .title_color("1F497D").title_bold(true)
236            .add_shape(dim_shape)
237            .add_shape(ratio_rect)
238            .add_shape(ratio_text)
239            .add_shape(inch_rect)
240    );
241
242    // =========================================================================
243    // SLIDE 7: FlexPosition & FlexSize Structs
244    // =========================================================================
245    println!("📦 Slide 7: FlexPosition & FlexSize");
246
247    // Demonstrate FlexPosition and FlexSize for reusable layout definitions
248    let header_pos = FlexPosition::new(Dimension::percent(5.0), Dimension::percent(20.0));
249    let header_size = FlexSize::new(Dimension::percent(90.0), Dimension::percent(12.0));
250    let (hx, hy) = header_pos.to_emu();
251    let (hw, hh) = header_size.to_emu();
252    let header = Shape::new(ShapeType::RoundedRectangle, hx, hy, hw, hh)
253        .with_fill(ShapeFill::new("1F497D"))
254        .with_text("FlexPosition + FlexSize → header");
255
256    let body_pos = FlexPosition::new(Dimension::percent(5.0), Dimension::percent(35.0));
257    let body_size = FlexSize::new(Dimension::percent(60.0), Dimension::percent(50.0));
258    let (bx, by) = body_pos.to_emu();
259    let (bw, bh) = body_size.to_emu();
260    let body = Shape::new(ShapeType::Rectangle, bx, by, bw, bh)
261        .with_fill(ShapeFill::new("E8EAF6"))
262        .with_line(ShapeLine::new("3F51B5", 12700))
263        .with_text("Body area (60% x 50%)");
264
265    let sidebar_pos = FlexPosition::new(Dimension::percent(68.0), Dimension::percent(35.0));
266    let sidebar_size = FlexSize::new(Dimension::percent(27.0), Dimension::percent(50.0));
267    let (sx, sy) = sidebar_pos.to_emu();
268    let (sw, sh) = sidebar_size.to_emu();
269    let sidebar_shape = Shape::new(ShapeType::Rectangle, sx, sy, sw, sh)
270        .with_fill(ShapeFill::new("FFF3E0"))
271        .with_line(ShapeLine::new("EF6C00", 12700))
272        .with_text("Sidebar (27% x 50%)");
273
274    slides.push(
275        SlideContent::new("FlexPosition & FlexSize — Reusable Layouts")
276            .layout(SlideLayout::TitleOnly)
277            .title_color("1F497D").title_bold(true)
278            .add_shape(header)
279            .add_shape(body)
280            .add_shape(sidebar_shape)
281    );
282
283    // =========================================================================
284    // SLIDE 8: Real-World Dashboard with Dimension API
285    // =========================================================================
286    println!("📊 Slide 8: Real-World Dashboard");
287
288    // 4 evenly-spaced KPI cards using percent
289    let kpi_colors = ["1565C0", "2E7D32", "EF6C00", "7B1FA2"];
290    let kpi_labels = [
291        "Revenue\n$2.14M\n+15%",
292        "Users\n12,450\n+22%",
293        "NPS\n72\n+8 pts",
294        "Uptime\n99.9%\n+0.1%",
295    ];
296
297    let mut dashboard = SlideContent::new("KPI Dashboard — Dimension API")
298        .layout(SlideLayout::TitleOnly)
299        .title_color("1F497D").title_bold(true);
300
301    for i in 0..4 {
302        let x_pct = 3.0 + i as f64 * 24.5;
303        let card = Shape::from_dimensions(ShapeType::RoundedRectangle,
304            Dimension::percent(x_pct), Dimension::percent(22.0),
305            Dimension::percent(22.0), Dimension::percent(30.0),
306        ).with_fill(ShapeFill::new(kpi_colors[i])).with_text(kpi_labels[i]);
307        dashboard = dashboard.add_shape(card);
308    }
309
310    // Bottom chart placeholder
311    let chart_area = Shape::from_dimensions(ShapeType::Rectangle,
312        Dimension::percent(3.0), Dimension::percent(58.0),
313        Dimension::percent(94.0), Dimension::percent(35.0),
314    ).with_fill(ShapeFill::new("ECEFF1"))
315     .with_line(ShapeLine::new("B0BEC5", 12700))
316     .with_text("Chart Area (94% x 35%)");
317    dashboard = dashboard.add_shape(chart_area);
318
319    slides.push(dashboard);
320
321    // =========================================================================
322    // SLIDE 9: Unit Equivalence Reference
323    // =========================================================================
324    println!("📖 Slide 9: Unit Equivalence Reference");
325
326    slides.push(
327        SlideContent::new("Dimension Unit Reference")
328            .layout(SlideLayout::TitleAndContent)
329            .title_color("1F497D").title_bold(true)
330            .add_bullet(&format!("1 inch = {} EMU = Dimension::Inches(1.0)", 914400))
331            .add_bullet(&format!("1 cm   = {} EMU = Dimension::Cm(1.0)", 360000))
332            .add_bullet(&format!("1 pt   = {} EMU = Dimension::Pt(1.0)", 12700))
333            .add_bullet(&format!("Slide width  = {} EMU = 10 inches", SLIDE_WIDTH_EMU))
334            .add_bullet(&format!("Slide height = {} EMU = 7.5 inches", SLIDE_HEIGHT_EMU))
335            .add_bullet("Ratio(0.1) on X = 10% of slide width = 1 inch")
336            .add_bullet("Ratio(0.5) on Y = 50% of slide height = 3.75 inches")
337            .add_bullet("percent(50.0) = Ratio(0.5)")
338            .content_size(22)
339    );
340
341    // =========================================================================
342    // Generate PPTX
343    // =========================================================================
344    fs::create_dir_all("examples/output")?;
345    let num_slides = slides.len();
346    let pptx_data = create_pptx_with_content("Dimension API Demo", slides)?;
347    fs::write("examples/output/dimension_demo.pptx", &pptx_data)?;
348
349    println!("\n╔══════════════════════════════════════════════════════════════╗");
350    println!("║                 Dimension API Demo Complete                   ║");
351    println!("╠══════════════════════════════════════════════════════════════╣");
352    println!("║  Output: examples/output/dimension_demo.pptx                 ║");
353    println!("║  Slides: {}                                                   ║", num_slides);
354    println!("║  Size:   {} KB                                               ║", pptx_data.len() / 1024);
355    println!("╠══════════════════════════════════════════════════════════════╣");
356    println!("║  Showcased:                                                  ║");
357    println!("║    ✓ All 6 unit types: EMU, Inches, Cm, Pt, Ratio, Percent   ║");
358    println!("║    ✓ Shape::from_dimensions() constructor                    ║");
359    println!("║    ✓ Fluent .at() and .with_dimensions() chaining            ║");
360    println!("║    ✓ Mixed-unit positioning                                  ║");
361    println!("║    ✓ Prelude helpers: dim(), rect_ratio(), text_box_ratio()  ║");
362    println!("║    ✓ FlexPosition & FlexSize structs                         ║");
363    println!("║    ✓ Ratio-based grid layout (auto-adapts)                   ║");
364    println!("║    ✓ Real-world KPI dashboard                                ║");
365    println!("╚══════════════════════════════════════════════════════════════╝");
366
367    Ok(())
368}