Skip to main content

ppt_rs/generator/
connectors.rs

1//! Connector support for linking shapes in PPTX
2//!
3//! Provides connector types and XML generation for connecting shapes.
4
5use crate::core::escape_xml;
6
7/// Connector types available in PPTX
8#[derive(Clone, Debug, Copy, PartialEq, Eq)]
9pub enum ConnectorType {
10    /// Straight line connector
11    Straight,
12    /// Elbow (bent) connector
13    Elbow,
14    /// Curved connector
15    Curved,
16}
17
18impl ConnectorType {
19    /// Get the preset geometry name for the connector
20    pub fn preset_name(&self) -> &'static str {
21        match self {
22            ConnectorType::Straight => "straightConnector1",
23            ConnectorType::Elbow => "bentConnector3",
24            ConnectorType::Curved => "curvedConnector3",
25        }
26    }
27
28    /// Get display name
29    pub fn display_name(&self) -> &'static str {
30        match self {
31            ConnectorType::Straight => "Straight Connector",
32            ConnectorType::Elbow => "Elbow Connector",
33            ConnectorType::Curved => "Curved Connector",
34        }
35    }
36}
37
38/// Arrow head types for connectors
39#[derive(Clone, Debug, Copy, PartialEq, Eq)]
40pub enum ArrowType {
41    /// No arrow
42    None,
43    /// Triangle arrow
44    Triangle,
45    /// Stealth arrow
46    Stealth,
47    /// Diamond arrow
48    Diamond,
49    /// Oval arrow
50    Oval,
51    /// Open arrow
52    Open,
53}
54
55impl ArrowType {
56    /// Get OOXML arrow type value
57    pub fn xml_value(&self) -> &'static str {
58        match self {
59            ArrowType::None => "none",
60            ArrowType::Triangle => "triangle",
61            ArrowType::Stealth => "stealth",
62            ArrowType::Diamond => "diamond",
63            ArrowType::Oval => "oval",
64            ArrowType::Open => "arrow",
65        }
66    }
67}
68
69/// Arrow size
70#[derive(Clone, Debug, Copy, PartialEq, Eq)]
71pub enum ArrowSize {
72    Small,
73    Medium,
74    Large,
75}
76
77impl ArrowSize {
78    /// Get OOXML size value
79    pub fn xml_value(&self) -> &'static str {
80        match self {
81            ArrowSize::Small => "sm",
82            ArrowSize::Medium => "med",
83            ArrowSize::Large => "lg",
84        }
85    }
86}
87
88/// Connection point on a shape
89#[derive(Clone, Debug, Copy, PartialEq, Eq)]
90pub enum ConnectionSite {
91    /// Top center
92    Top,
93    /// Bottom center
94    Bottom,
95    /// Left center
96    Left,
97    /// Right center
98    Right,
99    /// Top left corner
100    TopLeft,
101    /// Top right corner
102    TopRight,
103    /// Bottom left corner
104    BottomLeft,
105    /// Bottom right corner
106    BottomRight,
107    /// Center
108    Center,
109}
110
111impl ConnectionSite {
112    /// Get connection site index (0-based)
113    pub fn index(&self) -> u32 {
114        match self {
115            ConnectionSite::Top => 0,
116            ConnectionSite::Right => 1,
117            ConnectionSite::Bottom => 2,
118            ConnectionSite::Left => 3,
119            ConnectionSite::TopLeft => 4,
120            ConnectionSite::TopRight => 5,
121            ConnectionSite::BottomRight => 6,
122            ConnectionSite::BottomLeft => 7,
123            ConnectionSite::Center => 8,
124        }
125    }
126}
127
128/// Connector line style
129#[derive(Clone, Debug)]
130pub struct ConnectorLine {
131    /// Line color (RGB hex)
132    pub color: String,
133    /// Line width in EMU
134    pub width: u32,
135    /// Dash style
136    pub dash: LineDash,
137}
138
139impl Default for ConnectorLine {
140    fn default() -> Self {
141        ConnectorLine {
142            color: "000000".to_string(),
143            width: 12700, // 1pt
144            dash: LineDash::Solid,
145        }
146    }
147}
148
149impl ConnectorLine {
150    /// Create new connector line
151    pub fn new(color: &str, width: u32) -> Self {
152        ConnectorLine {
153            color: color.trim_start_matches('#').to_uppercase(),
154            width,
155            dash: LineDash::Solid,
156        }
157    }
158
159    /// Set dash style
160    pub fn with_dash(mut self, dash: LineDash) -> Self {
161        self.dash = dash;
162        self
163    }
164}
165
166/// Line dash styles
167#[derive(Clone, Debug, Copy, PartialEq, Eq)]
168pub enum LineDash {
169    Solid,
170    Dash,
171    Dot,
172    DashDot,
173    DashDotDot,
174    LongDash,
175    LongDashDot,
176}
177
178impl LineDash {
179    /// Get OOXML dash value
180    pub fn xml_value(&self) -> &'static str {
181        match self {
182            LineDash::Solid => "solid",
183            LineDash::Dash => "dash",
184            LineDash::Dot => "dot",
185            LineDash::DashDot => "dashDot",
186            LineDash::DashDotDot => "lgDashDotDot",
187            LineDash::LongDash => "lgDash",
188            LineDash::LongDashDot => "lgDashDot",
189        }
190    }
191}
192
193/// Connector definition
194#[derive(Clone, Debug)]
195pub struct Connector {
196    /// Connector type
197    pub connector_type: ConnectorType,
198    /// Start X position in EMU
199    pub start_x: u32,
200    /// Start Y position in EMU
201    pub start_y: u32,
202    /// End X position in EMU
203    pub end_x: u32,
204    /// End Y position in EMU
205    pub end_y: u32,
206    /// Line style
207    pub line: ConnectorLine,
208    /// Start arrow
209    pub start_arrow: ArrowType,
210    /// End arrow
211    pub end_arrow: ArrowType,
212    /// Arrow size
213    pub arrow_size: ArrowSize,
214    /// Connected shape ID at start (optional)
215    pub start_shape_id: Option<u32>,
216    /// Connection site at start shape
217    pub start_site: Option<ConnectionSite>,
218    /// Connected shape ID at end (optional)
219    pub end_shape_id: Option<u32>,
220    /// Connection site at end shape
221    pub end_site: Option<ConnectionSite>,
222    /// Optional label text
223    pub label: Option<String>,
224}
225
226impl Connector {
227    /// Create a new connector
228    pub fn new(
229        connector_type: ConnectorType,
230        start_x: u32,
231        start_y: u32,
232        end_x: u32,
233        end_y: u32,
234    ) -> Self {
235        Connector {
236            connector_type,
237            start_x,
238            start_y,
239            end_x,
240            end_y,
241            line: ConnectorLine::default(),
242            start_arrow: ArrowType::None,
243            end_arrow: ArrowType::None,
244            arrow_size: ArrowSize::Medium,
245            start_shape_id: None,
246            start_site: None,
247            end_shape_id: None,
248            end_site: None,
249            label: None,
250        }
251    }
252
253    /// Create a straight connector
254    pub fn straight(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
255        Self::new(ConnectorType::Straight, start_x, start_y, end_x, end_y)
256    }
257
258    /// Create an elbow connector
259    pub fn elbow(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
260        Self::new(ConnectorType::Elbow, start_x, start_y, end_x, end_y)
261    }
262
263    /// Create a curved connector
264    pub fn curved(start_x: u32, start_y: u32, end_x: u32, end_y: u32) -> Self {
265        Self::new(ConnectorType::Curved, start_x, start_y, end_x, end_y)
266    }
267
268    /// Set line style
269    pub fn with_line(mut self, line: ConnectorLine) -> Self {
270        self.line = line;
271        self
272    }
273
274    /// Set line color
275    pub fn with_color(mut self, color: &str) -> Self {
276        self.line.color = color.trim_start_matches('#').to_uppercase();
277        self
278    }
279
280    /// Set line width in EMU
281    pub fn with_width(mut self, width: u32) -> Self {
282        self.line.width = width;
283        self
284    }
285
286    /// Set start arrow
287    pub fn with_start_arrow(mut self, arrow: ArrowType) -> Self {
288        self.start_arrow = arrow;
289        self
290    }
291
292    /// Set end arrow
293    pub fn with_end_arrow(mut self, arrow: ArrowType) -> Self {
294        self.end_arrow = arrow;
295        self
296    }
297
298    /// Set both arrows
299    pub fn with_arrows(mut self, start: ArrowType, end: ArrowType) -> Self {
300        self.start_arrow = start;
301        self.end_arrow = end;
302        self
303    }
304
305    /// Set arrow size
306    pub fn with_arrow_size(mut self, size: ArrowSize) -> Self {
307        self.arrow_size = size;
308        self
309    }
310
311    /// Connect to start shape
312    pub fn connect_start(mut self, shape_id: u32, site: ConnectionSite) -> Self {
313        self.start_shape_id = Some(shape_id);
314        self.start_site = Some(site);
315        self
316    }
317
318    /// Connect to end shape
319    pub fn connect_end(mut self, shape_id: u32, site: ConnectionSite) -> Self {
320        self.end_shape_id = Some(shape_id);
321        self.end_site = Some(site);
322        self
323    }
324
325    /// Add label text
326    pub fn with_label(mut self, label: &str) -> Self {
327        self.label = Some(label.to_string());
328        self
329    }
330
331    /// Calculate width for XML
332    fn width(&self) -> u32 {
333        self.end_x.abs_diff(self.start_x)
334    }
335
336    /// Calculate height for XML
337    fn height(&self) -> u32 {
338        self.end_y.abs_diff(self.start_y)
339    }
340
341    /// Check if connector is flipped horizontally
342    fn flip_h(&self) -> bool {
343        self.end_x < self.start_x
344    }
345
346    /// Check if connector is flipped vertically
347    fn flip_v(&self) -> bool {
348        self.end_y < self.start_y
349    }
350}
351
352/// Generate connector XML for a slide
353pub fn generate_connector_xml(connector: &Connector, shape_id: usize) -> String {
354    let x = connector.start_x.min(connector.end_x);
355    let y = connector.start_y.min(connector.end_y);
356    let cx = connector.width();
357    let cy = connector.height();
358
359    let flip_h = if connector.flip_h() { " flipH=\"1\"" } else { "" };
360    let flip_v = if connector.flip_v() { " flipV=\"1\"" } else { "" };
361
362    let mut xml = format!(
363        r#"<p:cxnSp>
364<p:nvCxnSpPr>
365<p:cNvPr id="{}" name="Connector {}"/>
366<p:cNvCxnSpPr>"#,
367        shape_id, shape_id
368    );
369
370    // Add connection references if connected to shapes
371    if let (Some(start_id), Some(start_site)) = (connector.start_shape_id, connector.start_site) {
372        xml.push_str(&format!(
373            r#"
374<a:stCxn id="{}" idx="{}"/>"#,
375            start_id, start_site.index()
376        ));
377    }
378
379    if let (Some(end_id), Some(end_site)) = (connector.end_shape_id, connector.end_site) {
380        xml.push_str(&format!(
381            r#"
382<a:endCxn id="{}" idx="{}"/>"#,
383            end_id, end_site.index()
384        ));
385    }
386
387    xml.push_str(&format!(
388        r#"
389</p:cNvCxnSpPr>
390<p:nvPr/>
391</p:nvCxnSpPr>
392<p:spPr>
393<a:xfrm{}{}>
394<a:off x="{}" y="{}"/>
395<a:ext cx="{}" cy="{}"/>
396</a:xfrm>
397<a:prstGeom prst="{}">
398<a:avLst/>
399</a:prstGeom>
400<a:ln w="{}">
401<a:solidFill>
402<a:srgbClr val="{}"/>
403</a:solidFill>
404<a:prstDash val="{}"/>"#,
405        flip_h, flip_v,
406        x, y, cx, cy,
407        connector.connector_type.preset_name(),
408        connector.line.width,
409        connector.line.color,
410        connector.line.dash.xml_value()
411    ));
412
413    // Add arrow heads
414    if connector.start_arrow != ArrowType::None {
415        xml.push_str(&format!(
416            r#"
417<a:headEnd type="{}" w="{}" len="{}"/>"#,
418            connector.start_arrow.xml_value(),
419            connector.arrow_size.xml_value(),
420            connector.arrow_size.xml_value()
421        ));
422    }
423
424    if connector.end_arrow != ArrowType::None {
425        xml.push_str(&format!(
426            r#"
427<a:tailEnd type="{}" w="{}" len="{}"/>"#,
428            connector.end_arrow.xml_value(),
429            connector.arrow_size.xml_value(),
430            connector.arrow_size.xml_value()
431        ));
432    }
433
434    xml.push_str(r#"
435</a:ln>
436</p:spPr>"#);
437
438    // Add label if present
439    if let Some(label) = &connector.label {
440        xml.push_str(&format!(
441            r#"
442<p:txBody>
443<a:bodyPr/>
444<a:lstStyle/>
445<a:p>
446<a:r>
447<a:rPr lang="en-US" sz="1000"/>
448<a:t>{}</a:t>
449</a:r>
450</a:p>
451</p:txBody>"#,
452            escape_xml(label)
453        ));
454    }
455
456    xml.push_str(r#"
457</p:cxnSp>"#);
458
459    xml
460}
461
462#[cfg(test)]
463mod tests {
464    use super::*;
465
466    #[test]
467    fn test_connector_type_preset() {
468        assert_eq!(ConnectorType::Straight.preset_name(), "straightConnector1");
469        assert_eq!(ConnectorType::Elbow.preset_name(), "bentConnector3");
470        assert_eq!(ConnectorType::Curved.preset_name(), "curvedConnector3");
471    }
472
473    #[test]
474    fn test_arrow_type_xml() {
475        assert_eq!(ArrowType::None.xml_value(), "none");
476        assert_eq!(ArrowType::Triangle.xml_value(), "triangle");
477        assert_eq!(ArrowType::Stealth.xml_value(), "stealth");
478    }
479
480    #[test]
481    fn test_connector_builder() {
482        let conn = Connector::straight(0, 0, 1000000, 500000)
483            .with_color("FF0000")
484            .with_end_arrow(ArrowType::Triangle);
485
486        assert_eq!(conn.line.color, "FF0000");
487        assert_eq!(conn.end_arrow, ArrowType::Triangle);
488    }
489
490    #[test]
491    fn test_connector_with_connections() {
492        let conn = Connector::elbow(0, 0, 1000000, 500000)
493            .connect_start(1, ConnectionSite::Right)
494            .connect_end(2, ConnectionSite::Left);
495
496        assert_eq!(conn.start_shape_id, Some(1));
497        assert_eq!(conn.start_site, Some(ConnectionSite::Right));
498        assert_eq!(conn.end_shape_id, Some(2));
499        assert_eq!(conn.end_site, Some(ConnectionSite::Left));
500    }
501
502    #[test]
503    fn test_generate_connector_xml() {
504        let conn = Connector::straight(0, 0, 1000000, 500000)
505            .with_end_arrow(ArrowType::Triangle);
506
507        let xml = generate_connector_xml(&conn, 1);
508        assert!(xml.contains("p:cxnSp"));
509        assert!(xml.contains("straightConnector1"));
510        assert!(xml.contains("tailEnd"));
511    }
512
513    #[test]
514    fn test_connector_with_label() {
515        let conn = Connector::straight(0, 0, 1000000, 500000)
516            .with_label("Connection");
517
518        let xml = generate_connector_xml(&conn, 1);
519        assert!(xml.contains("Connection"));
520        assert!(xml.contains("p:txBody"));
521    }
522
523    #[test]
524    fn test_line_dash_styles() {
525        assert_eq!(LineDash::Solid.xml_value(), "solid");
526        assert_eq!(LineDash::Dash.xml_value(), "dash");
527        assert_eq!(LineDash::Dot.xml_value(), "dot");
528        assert_eq!(LineDash::DashDot.xml_value(), "dashDot");
529    }
530
531    #[test]
532    fn test_connection_site_index() {
533        assert_eq!(ConnectionSite::Top.index(), 0);
534        assert_eq!(ConnectionSite::Right.index(), 1);
535        assert_eq!(ConnectionSite::Bottom.index(), 2);
536        assert_eq!(ConnectionSite::Left.index(), 3);
537    }
538}