Skip to main content

docx_rs/documents/elements/
color.rs

1use serde::{Deserialize, Serialize, Serializer};
2use std::io::Write;
3
4use crate::documents::BuildXML;
5use crate::types::ThemeColor;
6use crate::xml_builder::*;
7
8/// A `<w:color>` color property.
9///
10/// `val` is an `RRGGBB` hex string (or `auto`). The optional theme fields let
11/// the color reference a document-theme color instead of being locked to the
12/// hex value: when present they emit `w:themeColor` / `w:themeShade` /
13/// `w:themeTint`, and Word resolves the color from the active theme while
14/// `val` stays as a fallback for renderers that ignore themes.
15///
16/// ```
17/// use docx_rs::{Color, ThemeColor};
18///
19/// // Plain hex color (unchanged behavior).
20/// let c = Color::new("2E74B5");
21///
22/// // Theme-aware color: "use accent1, shaded to BF", with 2E74B5 as fallback.
23/// let c = Color::new("2E74B5")
24///     .theme_color(ThemeColor::Accent1)
25///     .theme_shade("BF");
26/// ```
27#[derive(Deserialize, Debug, Clone, PartialEq)]
28pub struct Color {
29    val: String,
30    #[serde(default)]
31    theme_color: Option<String>,
32    #[serde(default)]
33    theme_shade: Option<String>,
34    #[serde(default)]
35    theme_tint: Option<String>,
36}
37
38impl Color {
39    /// Creates a color from an `RRGGBB` hex string (or `auto`).
40    pub fn new(val: impl Into<String>) -> Color {
41        Color {
42            val: val.into(),
43            theme_color: None,
44            theme_shade: None,
45            theme_tint: None,
46        }
47    }
48
49    /// Sets the theme color reference (`w:themeColor`).
50    pub fn theme_color(mut self, theme_color: ThemeColor) -> Color {
51        // Stored as the serialized ST_ThemeColor token (e.g. "accent1") so the
52        // builder and reader share a single string representation; the typed
53        // `ThemeColor` enum is the ergonomic boundary for callers.
54        self.theme_color = Some(theme_color.to_string());
55        self
56    }
57
58    /// Sets the theme shade modifier (`w:themeShade`), a hex byte such as `"BF"`.
59    pub fn theme_shade(mut self, theme_shade: impl Into<String>) -> Color {
60        self.theme_shade = Some(theme_shade.into());
61        self
62    }
63
64    /// Sets the theme tint modifier (`w:themeTint`), a hex byte such as `"99"`.
65    pub fn theme_tint(mut self, theme_tint: impl Into<String>) -> Color {
66        self.theme_tint = Some(theme_tint.into());
67        self
68    }
69}
70
71impl BuildXML for Color {
72    fn build_to<W: Write>(
73        &self,
74        stream: crate::xml::writer::EventWriter<W>,
75    ) -> crate::xml::writer::Result<crate::xml::writer::EventWriter<W>> {
76        XMLBuilder::from(stream)
77            .color_with_theme(
78                &self.val,
79                self.theme_color.as_deref(),
80                self.theme_shade.as_deref(),
81                self.theme_tint.as_deref(),
82            )?
83            .into_inner()
84    }
85}
86
87impl Serialize for Color {
88    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
89    where
90        S: Serializer,
91    {
92        // Preserve the existing JSON shape (a bare hex string). Theme metadata
93        // is not represented in JSON to avoid a breaking format change; it is
94        // fully round-tripped through the docx/XML path, which is what consumers
95        // generating documents rely on.
96        serializer.serialize_str(&self.val)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102
103    use super::*;
104    #[cfg(test)]
105    use pretty_assertions::assert_eq;
106    use std::str;
107
108    #[test]
109    fn test_build() {
110        let c = Color::new("FFFFFF");
111        let b = c.build();
112        assert_eq!(str::from_utf8(&b).unwrap(), r#"<w:color w:val="FFFFFF" />"#);
113    }
114
115    #[test]
116    fn test_build_with_theme_color() {
117        let c = Color::new("2E74B5")
118            .theme_color(ThemeColor::Accent1)
119            .theme_shade("BF");
120        let b = c.build();
121        assert_eq!(
122            str::from_utf8(&b).unwrap(),
123            r#"<w:color w:val="2E74B5" w:themeColor="accent1" w:themeShade="BF" />"#
124        );
125    }
126
127    #[test]
128    fn test_build_with_theme_tint() {
129        let c = Color::new("2E74B5")
130            .theme_color(ThemeColor::Accent2)
131            .theme_tint("99");
132        let b = c.build();
133        assert_eq!(
134            str::from_utf8(&b).unwrap(),
135            r#"<w:color w:val="2E74B5" w:themeColor="accent2" w:themeTint="99" />"#
136        );
137    }
138}