Skip to main content

ferrum_email_core/
style.rs

1//! Typed CSS style system for email-safe styling.
2//!
3//! All style values are type-checked at compile time. The style system only includes
4//! properties that are safe to use across email clients.
5
6use crate::color::Color;
7use crate::spacing::Spacing;
8use crate::types::*;
9
10/// A typed inline style map for email elements.
11///
12/// All properties are optional. Only set properties will be emitted as CSS.
13#[derive(Debug, Clone, Default, PartialEq)]
14pub struct Style {
15    pub font_family: Option<FontFamily>,
16    pub font_size: Option<Px>,
17    pub font_weight: Option<FontWeight>,
18    pub color: Option<Color>,
19    pub background_color: Option<Color>,
20    pub padding: Option<Spacing>,
21    pub margin: Option<Spacing>,
22    pub border_radius: Option<Px>,
23    pub width: Option<SizeValue>,
24    pub max_width: Option<SizeValue>,
25    pub min_width: Option<SizeValue>,
26    pub height: Option<SizeValue>,
27    pub text_align: Option<TextAlign>,
28    pub vertical_align: Option<VerticalAlign>,
29    pub line_height: Option<LineHeight>,
30    pub display: Option<Display>,
31    pub border: Option<Border>,
32    pub border_bottom: Option<Border>,
33    pub border_top: Option<Border>,
34    pub text_decoration: Option<TextDecoration>,
35    pub letter_spacing: Option<Px>,
36    pub word_spacing: Option<Px>,
37}
38
39/// A border specification.
40#[derive(Debug, Clone, PartialEq)]
41pub struct Border {
42    pub width: Px,
43    pub style: BorderStyle,
44    pub color: Color,
45}
46
47impl Border {
48    pub fn new(width: Px, style: BorderStyle, color: Color) -> Self {
49        Border {
50            width,
51            style,
52            color,
53        }
54    }
55
56    pub fn solid(width: Px, color: Color) -> Self {
57        Border::new(width, BorderStyle::Solid, color)
58    }
59}
60
61impl std::fmt::Display for Border {
62    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
63        write!(f, "{} {} {}", self.width, self.style, self.color)
64    }
65}
66
67impl Style {
68    /// Create a new empty style.
69    pub fn new() -> Self {
70        Self::default()
71    }
72
73    /// Render this style to a CSS inline style string.
74    ///
75    /// Returns `None` if no properties are set.
76    pub fn to_css(&self) -> Option<String> {
77        let mut parts = Vec::new();
78
79        if let Some(ref ff) = self.font_family {
80            parts.push(format!("font-family:{ff}"));
81        }
82        if let Some(ref fs) = self.font_size {
83            parts.push(format!("font-size:{fs}"));
84        }
85        if let Some(ref fw) = self.font_weight {
86            parts.push(format!("font-weight:{fw}"));
87        }
88        if let Some(ref c) = self.color {
89            parts.push(format!("color:{c}"));
90        }
91        if let Some(ref bg) = self.background_color {
92            parts.push(format!("background-color:{bg}"));
93        }
94        if let Some(ref p) = self.padding {
95            parts.push(format!("padding:{p}"));
96        }
97        if let Some(ref m) = self.margin {
98            parts.push(format!("margin:{m}"));
99        }
100        if let Some(ref br) = self.border_radius {
101            parts.push(format!("border-radius:{br}"));
102        }
103        if let Some(ref w) = self.width {
104            parts.push(format!("width:{w}"));
105        }
106        if let Some(ref mw) = self.max_width {
107            parts.push(format!("max-width:{mw}"));
108        }
109        if let Some(ref mw) = self.min_width {
110            parts.push(format!("min-width:{mw}"));
111        }
112        if let Some(ref h) = self.height {
113            parts.push(format!("height:{h}"));
114        }
115        if let Some(ref ta) = self.text_align {
116            parts.push(format!("text-align:{ta}"));
117        }
118        if let Some(ref va) = self.vertical_align {
119            parts.push(format!("vertical-align:{va}"));
120        }
121        if let Some(ref lh) = self.line_height {
122            parts.push(format!("line-height:{lh}"));
123        }
124        if let Some(ref d) = self.display {
125            parts.push(format!("display:{d}"));
126        }
127        if let Some(ref b) = self.border {
128            parts.push(format!("border:{b}"));
129        }
130        if let Some(ref b) = self.border_bottom {
131            parts.push(format!("border-bottom:{b}"));
132        }
133        if let Some(ref b) = self.border_top {
134            parts.push(format!("border-top:{b}"));
135        }
136        if let Some(ref td) = self.text_decoration {
137            parts.push(format!("text-decoration:{td}"));
138        }
139        if let Some(ref ls) = self.letter_spacing {
140            parts.push(format!("letter-spacing:{ls}"));
141        }
142        if let Some(ref ws) = self.word_spacing {
143            parts.push(format!("word-spacing:{ws}"));
144        }
145
146        if parts.is_empty() {
147            None
148        } else {
149            Some(parts.join(";"))
150        }
151    }
152
153    /// Merge another style into this one. Properties from `other` override.
154    pub fn merge(&mut self, other: &Style) {
155        macro_rules! merge_field {
156            ($field:ident) => {
157                if other.$field.is_some() {
158                    self.$field = other.$field.clone();
159                }
160            };
161        }
162        merge_field!(font_family);
163        merge_field!(font_size);
164        merge_field!(font_weight);
165        merge_field!(color);
166        merge_field!(background_color);
167        merge_field!(padding);
168        merge_field!(margin);
169        merge_field!(border_radius);
170        merge_field!(width);
171        merge_field!(max_width);
172        merge_field!(min_width);
173        merge_field!(height);
174        merge_field!(text_align);
175        merge_field!(vertical_align);
176        merge_field!(line_height);
177        merge_field!(display);
178        merge_field!(border);
179        merge_field!(border_bottom);
180        merge_field!(border_top);
181        merge_field!(text_decoration);
182        merge_field!(letter_spacing);
183        merge_field!(word_spacing);
184    }
185}