Skip to main content

pptx_grouping_and_connectors/
pptx_grouping_and_connectors.rs

1//! PowerPoint shape grouping and connectors: a group of shapes moving as
2//! one, a rotated/flipped group, and a connector line attached to two
3//! shapes. One slide per feature.
4//!
5//! Run with: `cargo run -p office-toolkit --example pptx_grouping_and_connectors`
6
7use std::path::{Path, PathBuf};
8
9use office_toolkit::SaveToFile;
10use office_toolkit::drawing::{
11    Color, Fill, Line, ShapeProperties, TextBody, TextParagraph, TextRun, Transform2D,
12};
13use office_toolkit::powerpoint::{AutoShape, Connector, Shape, ShapeGroup};
14use office_toolkit::prelude::*;
15
16fn main() -> office_toolkit::Result<()> {
17    let path = output_path("pptx_grouping_and_connectors.pptx");
18
19    // Every shape below sets an explicit fill (and the connector an
20    // explicit line): with neither a fill/line nor a `<p:style>` theme
21    // reference, a shape has nothing to paint and renders as fully
22    // invisible — the slide then looks blank even though the shapes are
23    // there. `AutoShape`/`Connector` have no `<p:style>` support yet, so an
24    // explicit `Fill`/`Line` is the only way to make a shape actually show.
25    let circle_fill = || Fill::Solid(Color::Rgb("4472C4".to_string()));
26
27    let circle = AutoShape::new(11, "Circle").with_properties(
28        ShapeProperties::new()
29            .with_transform(
30                Transform2D::new()
31                    .with_offset(0, 0)
32                    .with_extent(1_000_000, 1_000_000),
33            )
34            .with_fill(circle_fill()),
35    );
36    let label = AutoShape::new(12, "Label")
37        .with_properties(
38            ShapeProperties::new()
39                .with_transform(
40                    Transform2D::new()
41                        .with_offset(1_200_000, 300_000)
42                        .with_extent(1_500_000, 400_000),
43                )
44                .with_fill(Fill::Solid(Color::Rgb("E8622C".to_string()))),
45        )
46        .with_text_body(
47            TextBody::new()
48                .with_paragraph(TextParagraph::new().with_run(TextRun::text("Grouped label"))),
49        );
50
51    let group = ShapeGroup::new(2, "Group")
52        .with_transform(
53            (838_200, 838_200),
54            (2_700_000, 1_000_000),
55            (0, 0),
56            (2_700_000, 1_000_000),
57        )
58        .with_shape(Shape::AutoShape(circle))
59        .with_shape(Shape::AutoShape(label));
60
61    let rotated_circle = AutoShape::new(11, "Circle").with_properties(
62        ShapeProperties::new()
63            .with_transform(
64                Transform2D::new()
65                    .with_offset(0, 0)
66                    .with_extent(1_000_000, 1_000_000),
67            )
68            .with_fill(circle_fill()),
69    );
70    let rotated_group = ShapeGroup::new(2, "Rotated and flipped group")
71        .with_transform(
72            (838_200, 838_200),
73            (1_000_000, 1_000_000),
74            (0, 0),
75            (1_000_000, 1_000_000),
76        )
77        .with_rotation_degrees(45.0)
78        .with_flip_horizontal(true)
79        .with_shape(Shape::AutoShape(rotated_circle));
80
81    let box_a = AutoShape::new(2, "Box A").with_properties(
82        ShapeProperties::new()
83            .with_transform(
84                Transform2D::new()
85                    .with_offset(838_200, 838_200)
86                    .with_extent(1_000_000, 1_000_000),
87            )
88            .with_fill(Fill::Solid(Color::Rgb("2E86AB".to_string()))),
89    );
90    let box_b = AutoShape::new(3, "Box B").with_properties(
91        ShapeProperties::new()
92            .with_transform(
93                Transform2D::new()
94                    .with_offset(3_500_000, 838_200)
95                    .with_extent(1_000_000, 1_000_000),
96            )
97            .with_fill(Fill::Solid(Color::Rgb("6FB98F".to_string()))),
98    );
99    // `Connector::new` leaves position/size unset, which the writer then
100    // emits as a zero-size `<a:xfrm>` (off 0,0 / ext 0,0) — a real point
101    // with no length, invisible regardless of line/fill. `stCxn`/`endCxn`
102    // alone don't supply geometry; PowerPoint only recomputes it once you
103    // drag one of the attached shapes. So this also sets an explicit
104    // transform spanning Box A's right edge to Box B's left edge.
105    let connector = Connector::new(4, "Connector")
106        .with_properties(
107            ShapeProperties::new()
108                .with_transform(
109                    Transform2D::new()
110                        .with_offset(1_838_200, 1_338_200)
111                        .with_extent(1_662_000, 0),
112                )
113                .with_line(
114                    Line::new()
115                        .with_width_emu(25_400)
116                        .with_fill(Fill::Solid(Color::Rgb("000000".to_string()))),
117                ),
118        )
119        .with_start_connection(2, 0)
120        .with_end_connection(3, 0);
121
122    let presentation = Presentation::new()
123        .with_slide(Slide::new().with_shape(Shape::Group(group)))
124        .with_slide(Slide::new().with_shape(Shape::Group(rotated_group)))
125        .with_slide(
126            Slide::new()
127                .with_shape(Shape::AutoShape(box_a))
128                .with_shape(Shape::AutoShape(box_b))
129                .with_shape(Shape::Connector(connector)),
130        );
131
132    presentation.save_to_file(&path)?;
133    println!("Wrote {}", path.display());
134    Ok(())
135}
136
137/// Every example in this directory writes its output under
138/// `tests-data/output/`, resolved relative to this crate's own manifest so
139/// it works no matter what directory `cargo run` was invoked from.
140fn output_path(filename: &str) -> PathBuf {
141    Path::new(env!("CARGO_MANIFEST_DIR"))
142        .join("../../tests-data/output")
143        .join(filename)
144}