Skip to main content

rpptx_layout/
style.rs

1//! Shape-style inheritance resolution.
2
3use std::fmt;
4
5use oxml_drawing::color::ColorChoice;
6use oxml_drawing::effect::CT_EffectList;
7use oxml_drawing::fill::{Fill, GradientGeometry};
8use oxml_drawing::line::CT_LineProperties;
9use oxml_drawing::shape_props::CT_ShapeProperties;
10use oxml_drawing::style_ref::FontCollectionIndex;
11use oxml_drawing::theme::CT_EffectStyle;
12use rpptx_oxml::connector::CT_ConnectionShape;
13use rpptx_oxml::shape_tree::{CT_Shape, CT_ShapeStyle};
14
15use crate::ResolveCtx;
16
17/// The modelled shape-style properties after theme lookup and direct overlays.
18#[derive(Clone, Debug, Default, Eq, PartialEq)]
19pub struct EffectiveShapeStyle {
20    pub fill: Option<Fill>,
21    pub line: Option<CT_LineProperties>,
22    pub effects: Option<CT_EffectList>,
23    pub font_collection: Option<FontCollectionIndex>,
24}
25
26/// Errors produced while resolving a shape against its theme format scheme.
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub enum ResolveError {
29    StyleIndexOutOfRange {
30        reference: &'static str,
31        index: u32,
32        available: usize,
33    },
34    UnresolvedPlaceholderColor {
35        reference: &'static str,
36    },
37    ConcreteValue {
38        kind: &'static str,
39        detail: String,
40    },
41}
42
43impl fmt::Display for ResolveError {
44    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
45        match self {
46            Self::StyleIndexOutOfRange {
47                reference,
48                index,
49                available,
50            } => write!(
51                formatter,
52                "{reference} style index {index} is outside the {available} available entries"
53            ),
54            Self::UnresolvedPlaceholderColor { reference } => write!(
55                formatter,
56                "{reference} style retains an unresolved phClr placeholder colour"
57            ),
58            Self::ConcreteValue { kind, detail } => {
59                write!(
60                    formatter,
61                    "cannot resolve {kind} to a concrete value: {detail}"
62                )
63            }
64        }
65    }
66}
67
68impl std::error::Error for ResolveError {}
69
70impl ResolveCtx<'_> {
71    /// Resolves one ordinary shape's modelled theme and direct style properties.
72    pub fn effective_shape_style(
73        &self,
74        shape: &CT_Shape,
75    ) -> Result<EffectiveShapeStyle, ResolveError> {
76        let (mut effective, referenced_effect_style) =
77            self.shape_style_for_placement(shape, false)?;
78        effective.effects = resolve_effects(
79            referenced_effect_style,
80            shape.style(),
81            &shape.shape_properties,
82        )?;
83        Ok(effective)
84    }
85
86    // The boundsless validator cannot normalize shape-relative gradient rotation.
87    // Placed rendering retains its existing substitution and normalization policy.
88    pub(crate) fn shape_style_for_placement(
89        &self,
90        shape: &CT_Shape,
91        unplaced: bool,
92    ) -> Result<(EffectiveShapeStyle, Option<CT_EffectStyle>), ResolveError> {
93        let matrix = &self.theme.theme_elements.format_scheme;
94        let referenced_effect_style = self.referenced_effect_style(shape.style())?;
95        let mut effective = if let Some(style) = shape.style() {
96            EffectiveShapeStyle {
97                fill: referenced_fill(
98                    style.fill_reference.index,
99                    &matrix.fill_styles,
100                    &matrix.background_fill_styles,
101                )?,
102                line: matrix_entry("line", style.line_reference.index, &matrix.line_styles)?,
103                effects: None,
104                font_collection: Some(style.font_reference.index),
105            }
106        } else {
107            EffectiveShapeStyle::default()
108        };
109
110        if let Some(explicit) = &shape.shape_properties.fill {
111            effective.fill = Some(explicit.clone());
112        }
113        if let Some(explicit) = &shape.shape_properties.line {
114            if let Some(line) = effective.line.as_mut() {
115                overlay_line(line, explicit);
116            } else {
117                effective.line = Some(explicit.clone());
118            }
119        }
120
121        let fill_reference = shape
122            .style()
123            .and_then(|style| style.fill_reference.color.as_ref());
124        let line_reference = shape
125            .style()
126            .and_then(|style| style.line_reference.color.as_ref());
127        if let Some(fill) = effective.fill.as_mut()
128            && !(unplaced
129                && matches!(fill, Fill::Gradient(gradient) if gradient.rotate_with_shape == Some(false)))
130        {
131            substitute_fill(fill, fill_reference, "fill")?;
132        }
133        if let Some(line) = effective.line.as_mut()
134            && let Some(fill) = line.fill.as_mut()
135            && !(unplaced
136                && matches!(fill, Fill::Gradient(gradient) if gradient.rotate_with_shape == Some(false)))
137        {
138            substitute_fill(fill, line_reference, "line")?;
139        }
140        Ok((effective, referenced_effect_style))
141    }
142
143    /// Resolves the effects of an ordinary shape or a connector: the theme
144    /// effect its style's `a:effectRef` selects, replaced as a whole by a
145    /// direct `a:effectLst`. Index 0 and an empty direct list both leave no
146    /// effect.
147    pub(crate) fn effective_effects(
148        &self,
149        style: Option<&CT_ShapeStyle>,
150        properties: &CT_ShapeProperties,
151    ) -> Result<Option<CT_EffectList>, ResolveError> {
152        let referenced_effect_style = self.referenced_effect_style(style)?;
153        resolve_effects(referenced_effect_style, style, properties)
154    }
155
156    fn referenced_effect_style(
157        &self,
158        style: Option<&CT_ShapeStyle>,
159    ) -> Result<Option<CT_EffectStyle>, ResolveError> {
160        match style {
161            Some(style) => matrix_entry(
162                "effect",
163                style.effect_reference.index,
164                &self.theme.theme_elements.format_scheme.effect_styles,
165            ),
166            None => Ok(None),
167        }
168    }
169
170    /// Resolves one connector's line from its style's theme line reference and
171    /// its direct `a:ln`, the same way an ordinary shape's line resolves.
172    pub(crate) fn effective_connector_line(
173        &self,
174        connector: &CT_ConnectionShape,
175        unplaced: bool,
176    ) -> Result<Option<CT_LineProperties>, ResolveError> {
177        let style = connector.style();
178        let mut line = match style {
179            Some(style) => matrix_entry(
180                "line",
181                style.line_reference.index,
182                &self.theme.theme_elements.format_scheme.line_styles,
183            )?,
184            None => None,
185        };
186        if let Some(explicit) = &connector.shape_properties.line {
187            if let Some(line) = line.as_mut() {
188                overlay_line(line, explicit);
189            } else {
190                line = Some(explicit.clone());
191            }
192        }
193        if let Some(fill) = line.as_mut().and_then(|line| line.fill.as_mut())
194            && !(unplaced
195                && matches!(fill, Fill::Gradient(gradient) if gradient.rotate_with_shape == Some(false)))
196        {
197            let reference = style.and_then(|style| style.line_reference.color.as_ref());
198            substitute_fill(fill, reference, "line")?;
199        }
200        Ok(line)
201    }
202}
203
204/// Chooses the effects a shape draws from its referenced theme effect style
205/// and its direct `a:effectLst`, then resolves their placeholder colours.
206fn resolve_effects(
207    referenced_effect_style: Option<CT_EffectStyle>,
208    style: Option<&CT_ShapeStyle>,
209    properties: &CT_ShapeProperties,
210) -> Result<Option<CT_EffectList>, ResolveError> {
211    let has_explicit_unmodelled_effect = properties.has_unmodelled_effect();
212    let unmodelled_effect_has_placeholder = if has_explicit_unmodelled_effect {
213        properties.has_unmodelled_effect_placeholder_color()
214    } else {
215        referenced_effect_style
216            .as_ref()
217            .is_some_and(|style| style.has_unmodelled_effect_placeholder_color())
218    };
219    if unmodelled_effect_has_placeholder {
220        return Err(ResolveError::UnresolvedPlaceholderColor {
221            reference: "effect",
222        });
223    }
224    let mut effects = if has_explicit_unmodelled_effect {
225        None
226    } else if let Some(explicit) = &properties.effects {
227        Some(explicit.clone())
228    } else {
229        referenced_effect_style
230            .filter(|style| !style.has_unmodelled_effect())
231            .and_then(|style| style.effect_list)
232    };
233    if let Some(effects) = effects.as_mut() {
234        let effect_reference = style.and_then(|style| style.effect_reference.color.as_ref());
235        if let Some(shadow) = effects.outer_shadow.as_mut()
236            && let Some(color) = shadow.color.as_mut()
237        {
238            substitute_color(color, effect_reference, "effect")?;
239        }
240        if effects.has_unmodelled_placeholder_color() {
241            return Err(ResolveError::UnresolvedPlaceholderColor {
242                reference: "effect",
243            });
244        }
245    }
246    Ok(effects)
247}
248
249fn matrix_entry<T: Clone>(
250    reference: &'static str,
251    index: u32,
252    entries: &[T],
253) -> Result<Option<T>, ResolveError> {
254    if index == 0 {
255        return Ok(None);
256    }
257    let offset = usize::try_from(index - 1).ok();
258    offset
259        .and_then(|offset| entries.get(offset))
260        .cloned()
261        .map(Some)
262        .ok_or(ResolveError::StyleIndexOutOfRange {
263            reference,
264            index,
265            available: entries.len(),
266        })
267}
268
269pub(crate) fn referenced_fill(
270    index: u32,
271    normal: &[Fill],
272    background: &[Fill],
273) -> Result<Option<Fill>, ResolveError> {
274    if index > 1000 {
275        matrix_entry("background fill", index - 1000, background)
276    } else {
277        matrix_entry("fill", index, normal)
278    }
279}
280
281fn overlay_line(target: &mut CT_LineProperties, source: &CT_LineProperties) {
282    if source.width.is_some() {
283        target.width = source.width;
284    }
285    if source.cap.is_some() {
286        target.cap = source.cap;
287    }
288    if source.fill.is_some() {
289        target.fill.clone_from(&source.fill);
290    }
291    if source.dash.is_some() {
292        target.dash.clone_from(&source.dash);
293    }
294    if source.join.is_some() {
295        target.join.clone_from(&source.join);
296    }
297    if source.head_end.is_some() {
298        target.head_end.clone_from(&source.head_end);
299    }
300    if source.tail_end.is_some() {
301        target.tail_end.clone_from(&source.tail_end);
302    }
303}
304
305pub(crate) fn substitute_fill(
306    fill: &mut Fill,
307    reference: Option<&ColorChoice>,
308    kind: &'static str,
309) -> Result<(), ResolveError> {
310    match fill {
311        Fill::Solid(fill) => substitute_optional_color(&mut fill.color, reference, kind),
312        Fill::Gradient(fill) => {
313            // Match the paint boundary before resolving colours that cannot be drawn.
314            if matches!(fill.flip.as_deref(), Some("x" | "y" | "xy"))
315                || fill.tile_rect.is_some()
316                || matches!(fill.geometry.as_ref(), Some(GradientGeometry::Path(path)) if !path.fill_to_rect.as_ref().is_some_and(|rect| {
317                    [rect.left, rect.top, rect.right, rect.bottom].into_iter().all(|value| value.is_some_and(|value| value.0 == 50_000))
318                }))
319            {
320                return Ok(());
321            }
322            for stop in &mut fill.stops {
323                if stop.color.is_none() {
324                    break;
325                }
326                substitute_optional_color(&mut stop.color, reference, kind)?;
327            }
328            Ok(())
329        }
330        Fill::Pattern(fill) => {
331            substitute_optional_color(&mut fill.foreground, reference, kind)?;
332            substitute_optional_color(&mut fill.background, reference, kind)
333        }
334        Fill::NoFill(_) | Fill::Blip(_) => Ok(()),
335    }
336}
337
338fn substitute_optional_color(
339    color: &mut Option<ColorChoice>,
340    reference: Option<&ColorChoice>,
341    kind: &'static str,
342) -> Result<(), ResolveError> {
343    if let Some(color) = color {
344        substitute_color(color, reference, kind)?;
345    }
346    Ok(())
347}
348
349fn substitute_color(
350    color: &mut ColorChoice,
351    reference: Option<&ColorChoice>,
352    kind: &'static str,
353) -> Result<(), ResolveError> {
354    if !matches!(color, ColorChoice::Scheme { value, .. } if value == "phClr") {
355        return Ok(());
356    }
357    let placeholder_transforms = color.transforms().to_vec();
358    let Some(mut replacement) = reference.cloned() else {
359        return Err(ResolveError::UnresolvedPlaceholderColor { reference: kind });
360    };
361    if matches!(&replacement, ColorChoice::Scheme { value, .. } if value == "phClr") {
362        return Err(ResolveError::UnresolvedPlaceholderColor { reference: kind });
363    }
364    transforms_mut(&mut replacement).extend(placeholder_transforms);
365    *color = replacement;
366    Ok(())
367}
368
369fn transforms_mut(color: &mut ColorChoice) -> &mut Vec<oxml_drawing::color::ColorTransform> {
370    match color {
371        ColorChoice::Srgb { transforms, .. }
372        | ColorChoice::Scheme { transforms, .. }
373        | ColorChoice::System { transforms, .. }
374        | ColorChoice::Preset { transforms, .. } => transforms,
375    }
376}
377
378#[cfg(test)]
379mod tests {
380    use oxml_drawing::color::{ColorChoice, ColorMap, ColorTransform};
381    use oxml_drawing::fill::Fill;
382    use oxml_drawing::line::{LineCap, LineDash, ST_PresetLineDashVal};
383    use oxml_drawing::style_ref::FontCollectionIndex;
384    use oxml_drawing::text::CT_TextListStyle;
385    use oxml_drawing::theme::CT_OfficeStyleSheet;
386    use rpptx_oxml::shape_tree::{CT_Shape, ShapeTreeChild};
387    use rpptx_oxml::slide_parts::{CT_Slide, CT_SlideLayout, CT_SlideMaster};
388
389    use super::{ResolveCtx, ResolveError};
390
391    const P_NS: &str = "http://schemas.openxmlformats.org/presentationml/2006/main";
392    const A_NS: &str = "http://schemas.openxmlformats.org/drawingml/2006/main";
393
394    #[test]
395    fn shape_with_style_resolves_theme_fill_with_reference_colour_substituted() {
396        let fixture = Fixture::new(&shape("", &style_refs(0, 1, 0, "minor", "AABBCC")));
397        let resolved = fixture
398            .context()
399            .effective_shape_style(fixture.shape(0))
400            .unwrap();
401
402        let Some(Fill::Solid(fill)) = resolved.fill else {
403            panic!("expected a resolved solid fill");
404        };
405        let Some(ColorChoice::Srgb {
406            value, transforms, ..
407        }) = fill.color
408        else {
409            panic!("expected the reference sRGB colour");
410        };
411        assert_eq!(value.to_string(), "AABBCC");
412        assert!(transforms.is_empty());
413        assert_eq!(resolved.font_collection, Some(FontCollectionIndex::Minor));
414    }
415
416    #[test]
417    fn fill_ref_1001_selects_first_background_fill() {
418        let fixture = Fixture::new(&shape("", &style_refs(0, 1001, 0, "major", "102030")));
419        let resolved = fixture
420            .context()
421            .effective_shape_style(fixture.shape(0))
422            .unwrap();
423
424        let Some(Fill::Solid(fill)) = resolved.fill else {
425            panic!("expected the first background solid fill");
426        };
427        assert!(matches!(
428            fill.color,
429            Some(ColorChoice::Srgb { value, .. }) if value.to_string() == "102030"
430        ));
431    }
432
433    #[test]
434    fn style_matrix_zero_is_none_and_out_of_range_is_error() {
435        let shapes = [
436            shape("", &style_refs(0, 0, 0, "none", "112233")),
437            shape("", &style_refs(0, 4, 0, "none", "112233")),
438        ]
439        .join("");
440        let fixture = Fixture::new(&shapes);
441
442        let zero = fixture
443            .context()
444            .effective_shape_style(fixture.shape(0))
445            .unwrap();
446        assert!(zero.fill.is_none());
447        assert!(zero.line.is_none());
448        assert!(zero.effects.is_none());
449        assert_eq!(zero.font_collection, Some(FontCollectionIndex::None));
450
451        assert_eq!(
452            fixture
453                .context()
454                .effective_shape_style(fixture.shape(1))
455                .unwrap_err(),
456            ResolveError::StyleIndexOutOfRange {
457                reference: "fill",
458                index: 4,
459                available: 3,
460            }
461        );
462    }
463
464    #[test]
465    fn placeholder_colour_substitution_keeps_transform_order() {
466        let mut fixture = Fixture::new(&shape(
467            "",
468            &style_refs_with_fill_colour(
469                1,
470                r#"<a:schemeClr val="accent2"><a:shade val="80000"/></a:schemeClr>"#,
471            ),
472        ));
473        fixture.theme.theme_elements.format_scheme.fill_styles[0] = Fill::from_xml(
474            br#"<a:solidFill><a:schemeClr val="phClr"><a:tint val="30000"/></a:schemeClr></a:solidFill>"#,
475        )
476        .unwrap();
477
478        let resolved = fixture
479            .context()
480            .effective_shape_style(fixture.shape(0))
481            .unwrap();
482        let Some(Fill::Solid(fill)) = resolved.fill else {
483            panic!("expected a solid fill");
484        };
485        let Some(ColorChoice::Scheme {
486            value, transforms, ..
487        }) = fill.color
488        else {
489            panic!("expected a substituted scheme colour");
490        };
491        assert_eq!(value, "accent2");
492        assert!(matches!(
493            transforms.as_slice(),
494            [ColorTransform::Shade(shade), ColorTransform::Tint(tint)]
495                if shade.0 == 80_000 && tint.0 == 30_000
496        ));
497    }
498
499    #[test]
500    fn explicit_shape_properties_overlay_the_theme_style() {
501        let fixture = Fixture::new(&shape(
502            r#"<a:noFill/><a:ln w="25400"><a:prstDash val="dot"/></a:ln><a:effectLst/>"#,
503            &style_refs(2, 1, 3, "minor", "445566"),
504        ));
505        let resolved = fixture
506            .context()
507            .effective_shape_style(fixture.shape(0))
508            .unwrap();
509
510        assert!(matches!(resolved.fill, Some(Fill::NoFill(_))));
511        let line = resolved.line.unwrap();
512        assert_eq!(line.width, Some(25_400));
513        assert_eq!(line.cap, Some(LineCap::Flat));
514        assert!(matches!(
515            line.dash,
516            Some(LineDash::Preset(ref dash)) if dash.value == ST_PresetLineDashVal::Dot
517        ));
518        assert!(matches!(
519            line.fill,
520            Some(Fill::Solid(fill))
521                if matches!(fill.color, Some(ColorChoice::Srgb { value, .. }) if value.to_string() == "445566")
522        ));
523        assert!(resolved.effects.unwrap().outer_shadow.is_none());
524        assert_eq!(resolved.font_collection, Some(FontCollectionIndex::Minor));
525    }
526
527    #[test]
528    fn modelled_effect_placeholder_colour_is_substituted() {
529        let mut fixture = Fixture::new(&shape("", &style_refs(0, 0, 3, "none", "667788")));
530        let Fill::Solid(fill) = Fill::from_xml(
531            br#"<a:solidFill><a:schemeClr val="phClr"><a:tint val="25000"/></a:schemeClr></a:solidFill>"#,
532        )
533        .unwrap()
534        else {
535            unreachable!();
536        };
537        fixture.theme.theme_elements.format_scheme.effect_styles[2]
538            .effect_list
539            .as_mut()
540            .unwrap()
541            .outer_shadow
542            .as_mut()
543            .unwrap()
544            .color = fill.color;
545
546        let resolved = fixture
547            .context()
548            .effective_shape_style(fixture.shape(0))
549            .unwrap();
550        let color = resolved
551            .effects
552            .unwrap()
553            .outer_shadow
554            .unwrap()
555            .color
556            .unwrap();
557        assert!(matches!(
558            color,
559            ColorChoice::Srgb { value, transforms, .. }
560                if value.to_string() == "667788"
561                    && matches!(transforms.as_slice(), [ColorTransform::Tint(tint)] if tint.0 == 25_000)
562        ));
563    }
564
565    #[test]
566    fn unmodelled_effect_with_placeholder_colour_is_rejected() {
567        let fixture = Fixture::new(&shape(
568            r#"<a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst>"#,
569            &style_refs(0, 0, 0, "none", "778899"),
570        ));
571
572        assert_eq!(
573            fixture
574                .context()
575                .effective_shape_style(fixture.shape(0))
576                .unwrap_err(),
577            ResolveError::UnresolvedPlaceholderColor {
578                reference: "effect",
579            }
580        );
581    }
582
583    #[test]
584    fn explicit_opaque_effect_dag_replaces_theme_effect() {
585        let fixture = Fixture::new(&shape(
586            r#"<a:effectDag><a:cont/></a:effectDag>"#,
587            &style_refs(0, 0, 3, "none", "778899"),
588        ));
589
590        let resolved = fixture
591            .context()
592            .effective_shape_style(fixture.shape(0))
593            .unwrap();
594        assert!(resolved.effects.is_none());
595    }
596
597    #[test]
598    fn explicit_opaque_effect_dag_with_placeholder_colour_is_rejected() {
599        let fixture = Fixture::new(&shape(
600            r#"<a:effectDag><a:cont><a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst></a:cont></a:effectDag>"#,
601            &style_refs(0, 0, 3, "none", "778899"),
602        ));
603
604        assert_eq!(
605            fixture
606                .context()
607                .effective_shape_style(fixture.shape(0))
608                .unwrap_err(),
609            ResolveError::UnresolvedPlaceholderColor {
610                reference: "effect",
611            }
612        );
613    }
614
615    #[test]
616    fn theme_opaque_effect_dag_with_placeholder_colour_is_rejected() {
617        let mut fixture = Fixture::new(&shape("", &style_refs(0, 0, 1, "none", "778899")));
618        let xml = String::from_utf8(fixture.theme.to_xml().unwrap()).unwrap();
619        let xml = xml.replacen(
620            "<a:effectLst/>",
621            r#"<a:effectDag><a:cont><a:effectLst><a:glow rad="100"><a:schemeClr val="phClr"/></a:glow></a:effectLst></a:cont></a:effectDag>"#,
622            1,
623        );
624        fixture.theme = CT_OfficeStyleSheet::from_xml(xml.as_bytes()).unwrap();
625
626        assert_eq!(
627            fixture
628                .context()
629                .effective_shape_style(fixture.shape(0))
630                .unwrap_err(),
631            ResolveError::UnresolvedPlaceholderColor {
632                reference: "effect",
633            }
634        );
635    }
636
637    #[test]
638    fn foreign_namespace_effect_dag_does_not_replace_theme_effect() {
639        let fixture = Fixture::new(&shape(
640            r#"<x:effectDag xmlns:x="urn:extension"><x:schemeClr val="phClr"/></x:effectDag>"#,
641            &style_refs(0, 0, 3, "none", "778899"),
642        ));
643
644        let resolved = fixture
645            .context()
646            .effective_shape_style(fixture.shape(0))
647            .unwrap();
648        assert!(resolved.effects.is_some());
649    }
650
651    #[test]
652    fn foreign_namespace_scheme_color_is_not_a_placeholder_color() {
653        let fixture = Fixture::new(&shape(
654            r#"<a:effectDag><a:cont><x:schemeClr xmlns:x="urn:extension" val="phClr"/></a:cont></a:effectDag>"#,
655            &style_refs(0, 0, 3, "none", "778899"),
656        ));
657
658        let resolved = fixture
659            .context()
660            .effective_shape_style(fixture.shape(0))
661            .unwrap();
662        assert!(resolved.effects.is_none());
663    }
664
665    struct Fixture {
666        theme: CT_OfficeStyleSheet,
667        master: CT_SlideMaster,
668        layout: CT_SlideLayout,
669        slide: CT_Slide,
670        default_text_style: CT_TextListStyle,
671    }
672
673    impl Fixture {
674        fn new(shapes: &str) -> Self {
675            Self {
676                theme: CT_OfficeStyleSheet::office_default(),
677                master: CT_SlideMaster::from_xml(master_xml().as_bytes()).unwrap(),
678                layout: CT_SlideLayout::from_xml(layout_xml().as_bytes()).unwrap(),
679                slide: CT_Slide::from_xml(slide_xml(shapes).as_bytes()).unwrap(),
680                default_text_style: CT_TextListStyle::default(),
681            }
682        }
683
684        fn context(&self) -> ResolveCtx<'_> {
685            ResolveCtx::new(
686                &self.theme,
687                ColorMap::default(),
688                &self.master,
689                &self.layout,
690                &self.slide,
691                &self.default_text_style,
692            )
693        }
694
695        fn shape(&self, index: usize) -> &CT_Shape {
696            let ShapeTreeChild::Shape(shape) =
697                &self.slide.common_slide_data.shape_tree.children[index]
698            else {
699                panic!("expected an ordinary shape");
700            };
701            shape
702        }
703    }
704
705    fn style_refs(line: u32, fill: u32, effect: u32, font: &str, color: &str) -> String {
706        format!(
707            r#"<p:style><a:lnRef idx="{line}"><a:srgbClr val="{color}"/></a:lnRef><a:fillRef idx="{fill}"><a:srgbClr val="{color}"/></a:fillRef><a:effectRef idx="{effect}"><a:srgbClr val="{color}"/></a:effectRef><a:fontRef idx="{font}"><a:srgbClr val="{color}"/></a:fontRef></p:style>"#
708        )
709    }
710
711    fn style_refs_with_fill_colour(fill: u32, fill_colour: &str) -> String {
712        format!(
713            r#"<p:style><a:lnRef idx="0"><a:srgbClr val="000000"/></a:lnRef><a:fillRef idx="{fill}">{fill_colour}</a:fillRef><a:effectRef idx="0"><a:srgbClr val="000000"/></a:effectRef><a:fontRef idx="minor"><a:srgbClr val="000000"/></a:fontRef></p:style>"#
714        )
715    }
716
717    fn shape(properties: &str, style: &str) -> String {
718        format!(
719            "<p:sp><p:nvSpPr><p:cNvPr/><p:cNvSpPr/><p:nvPr/></p:nvSpPr><p:spPr>{properties}</p:spPr>{style}</p:sp>"
720        )
721    }
722
723    fn slide_xml(shapes: &str) -> String {
724        format!(
725            "<p:sld xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sld>",
726            shape_tree(shapes)
727        )
728    }
729
730    fn layout_xml() -> String {
731        format!(
732            "<p:sldLayout xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld></p:sldLayout>",
733            shape_tree("")
734        )
735    }
736
737    fn master_xml() -> String {
738        format!(
739            "<p:sldMaster xmlns:p=\"{P_NS}\" xmlns:a=\"{A_NS}\"><p:cSld>{}</p:cSld><p:clrMap bg1=\"lt1\" tx1=\"dk1\" bg2=\"lt2\" tx2=\"dk2\" accent1=\"accent1\" accent2=\"accent2\" accent3=\"accent3\" accent4=\"accent4\" accent5=\"accent5\" accent6=\"accent6\" hlink=\"hlink\" folHlink=\"folHlink\"/></p:sldMaster>",
740            shape_tree("")
741        )
742    }
743
744    fn shape_tree(shapes: &str) -> String {
745        format!("<p:spTree><p:nvGrpSpPr/><p:grpSpPr/>{shapes}</p:spTree>")
746    }
747}