1use serde::{Deserialize, Serialize};
4
5use crate::{ColorValue, ShapeFill, ShapeOutline, Theme, resolve_color_value_to_hex_with_theme};
6
7const DEFAULT_STYLE_LINE_WIDTH_EMU: f64 = 9_525.0;
9
10const BACKGROUND_FILL_INDEX_BASE: u32 = 1_001;
12
13#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
15#[serde(rename_all = "camelCase")]
16pub struct ThemeFormatScheme {
17 #[serde(default, skip_serializing_if = "Vec::is_empty")]
18 pub fills: Vec<Option<ShapeFill>>,
19 #[serde(default, skip_serializing_if = "Vec::is_empty")]
20 pub lines: Vec<Option<ShapeOutline>>,
21 #[serde(default, skip_serializing_if = "Vec::is_empty")]
23 pub background_fills: Vec<Option<ShapeFill>>,
24}
25
26impl ThemeFormatScheme {
27 pub fn is_empty(&self) -> bool {
28 self.fills.is_empty() && self.lines.is_empty() && self.background_fills.is_empty()
29 }
30
31 fn fill(&self, index: u32) -> Option<&ShapeFill> {
32 let (list, first) = if index >= BACKGROUND_FILL_INDEX_BASE {
33 (&self.background_fills, BACKGROUND_FILL_INDEX_BASE)
34 } else {
35 (&self.fills, 1)
36 };
37 list.get(index.checked_sub(first)? as usize)?.as_ref()
38 }
39
40 fn line(&self, index: u32) -> Option<&ShapeOutline> {
41 self.lines.get(index.checked_sub(1)? as usize)?.as_ref()
42 }
43}
44
45#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "camelCase")]
48pub struct StyleReference {
49 pub index: u32,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub color: Option<ColorValue>,
52}
53
54#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
56#[serde(rename_all = "camelCase")]
57pub struct ShapeStyle {
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub font_color: Option<ColorValue>,
60 #[serde(skip_serializing_if = "Option::is_none")]
61 pub fill: Option<StyleReference>,
62 #[serde(skip_serializing_if = "Option::is_none")]
63 pub line: Option<StyleReference>,
64 #[serde(default, skip_serializing_if = "is_false")]
65 pub fill_disabled: bool,
66 #[serde(default, skip_serializing_if = "is_false")]
67 pub line_disabled: bool,
68}
69
70fn is_false(value: &bool) -> bool {
71 !value
72}
73
74impl ShapeStyle {
75 pub fn is_empty(&self) -> bool {
76 self.font_color.is_none()
77 && self.fill.is_none()
78 && self.line.is_none()
79 && !self.fill_disabled
80 && !self.line_disabled
81 }
82}
83
84pub fn style_fill(
86 scheme: &ThemeFormatScheme,
87 reference: &StyleReference,
88 theme: &Theme,
89) -> ShapeFill {
90 if matches!(reference.index, 0 | 1000) {
91 return ShapeFill::named("none");
92 }
93 let placeholder = placeholder_color(reference, theme);
94 let Some(fill) = scheme.fill(reference.index) else {
95 return ShapeFill {
96 fill_type: "solid".to_owned(),
97 color: placeholder,
98 gradient: None,
99 };
100 };
101 let mut resolved = fill.clone();
102 resolved.color = substitute_color(fill.color.as_ref(), placeholder.as_ref());
103 if let Some(gradient) = &mut resolved.gradient {
104 for stop in &mut gradient.stops {
105 if let Some(color) = substitute_color(Some(&stop.color), placeholder.as_ref()) {
106 stop.color = color;
107 }
108 }
109 }
110 resolved
111}
112
113pub fn style_outline(
115 scheme: &ThemeFormatScheme,
116 reference: &StyleReference,
117 theme: &Theme,
118) -> Option<ShapeOutline> {
119 if reference.index == 0 {
120 return None;
121 }
122 let placeholder = placeholder_color(reference, theme);
123 let Some(outline) = scheme.line(reference.index) else {
124 return Some(ShapeOutline {
125 width: Some(DEFAULT_STYLE_LINE_WIDTH_EMU),
126 color: placeholder,
127 ..ShapeOutline::default()
128 });
129 };
130 let mut resolved = ShapeOutline {
131 width: outline.width.or(Some(DEFAULT_STYLE_LINE_WIDTH_EMU)),
132 color: substitute_color(outline.color.as_ref(), placeholder.as_ref()),
133 ..outline.clone()
134 };
135 if let Some(gradient) = &mut resolved.gradient {
136 for stop in &mut gradient.stops {
137 if let Some(color) = substitute_color(Some(&stop.color), placeholder.as_ref()) {
138 stop.color = color;
139 }
140 }
141 }
142 Some(resolved)
143}
144
145fn placeholder_color(reference: &StyleReference, theme: &Theme) -> Option<ColorValue> {
147 let color = reference.color.as_ref()?;
148 let rgb = resolve_color_value_to_hex_with_theme(Some(color), Some(theme))?;
149 Some(ColorValue {
150 rgb: Some(rgb.trim_start_matches('#').to_owned()),
151 alpha: color.alpha,
152 ..ColorValue::default()
153 })
154}
155
156fn substitute_color(
158 value: Option<&ColorValue>,
159 placeholder: Option<&ColorValue>,
160) -> Option<ColorValue> {
161 let value = value?;
162 if value.theme_color.as_deref() != Some("phClr") {
163 return Some(value.clone());
164 }
165 let Some(placeholder) = placeholder else {
166 return Some(value.clone());
167 };
168 Some(ColorValue {
169 rgb: placeholder.rgb.clone(),
170 theme_color: None,
171 auto: None,
172 alpha: value.alpha.or(placeholder.alpha),
173 ..value.clone()
174 })
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use crate::{GradientFill, GradientStop};
181
182 fn rgb(value: &str) -> ColorValue {
183 ColorValue {
184 rgb: Some(value.to_owned()),
185 ..Default::default()
186 }
187 }
188
189 #[test]
190 fn gradient_outline_substitutes_placeholder_colour_and_preserves_modifiers() {
191 let scheme = ThemeFormatScheme {
192 lines: vec![Some(ShapeOutline {
193 gradient: Some(GradientFill {
194 gradient_type: "linear".into(),
195 angle: Some(45.0),
196 stops: vec![GradientStop {
197 position: 0.0,
198 color: ColorValue {
199 theme_color: Some("phClr".into()),
200 luminance_modulation: Some(0.5),
201 ..Default::default()
202 },
203 }],
204 }),
205 ..Default::default()
206 })],
207 ..Default::default()
208 };
209 let reference = StyleReference {
210 index: 1,
211 color: Some(ColorValue {
212 alpha: Some(0.25),
213 ..rgb("FF0000")
214 }),
215 };
216 let resolved = style_outline(&scheme, &reference, &Theme::default()).unwrap();
217 let gradient = resolved.gradient.unwrap();
218 assert_eq!(gradient.angle, Some(45.0));
219 assert_eq!(gradient.stops[0].color.rgb.as_deref(), Some("FF0000"));
220 assert_eq!(gradient.stops[0].color.theme_color, None);
221 assert_eq!(gradient.stops[0].color.luminance_modulation, Some(0.5));
222 assert_eq!(gradient.stops[0].color.alpha, Some(0.25));
223 }
224
225 fn solid(color: ColorValue) -> ShapeFill {
226 ShapeFill {
227 fill_type: "solid".to_owned(),
228 color: Some(color),
229 gradient: None,
230 }
231 }
232
233 fn reference(index: u32) -> StyleReference {
234 StyleReference {
235 index,
236 color: Some(rgb("808080")),
237 }
238 }
239
240 #[test]
241 fn fill_indices_keep_holes_and_select_background_styles() {
242 let scheme = ThemeFormatScheme {
243 fills: vec![Some(solid(rgb("112233"))), None, Some(solid(rgb("445566")))],
244 background_fills: vec![Some(solid(rgb("778899"))), Some(solid(rgb("AABBCC")))],
245 ..Default::default()
246 };
247 let theme = Theme::default();
248 for (index, expected) in [
249 (1, "112233"),
250 (2, "808080"),
251 (3, "445566"),
252 (999, "808080"),
253 (1001, "778899"),
254 (1002, "AABBCC"),
255 (1003, "808080"),
256 ] {
257 assert_eq!(
258 style_fill(&scheme, &reference(index), &theme)
259 .color
260 .unwrap()
261 .rgb
262 .as_deref(),
263 Some(expected),
264 "index {index}"
265 );
266 }
267 for index in [0, 1000] {
268 assert_eq!(
269 style_fill(&scheme, &reference(index), &theme),
270 ShapeFill::named("none")
271 );
272 }
273 }
274
275 #[test]
276 fn placeholder_modifiers_compose_in_solids_lines_and_every_gradient_stop() {
277 let first = ColorValue {
278 theme_color: Some("phClr".to_owned()),
279 luminance_modulation: Some(0.5),
280 luminance_offset: Some(0.1),
281 ..Default::default()
282 };
283 let last = ColorValue {
284 luminance_modulation: Some(0.25),
285 luminance_offset: Some(0.2),
286 ..first.clone()
287 };
288 let scheme = ThemeFormatScheme {
289 fills: vec![
290 Some(solid(first.clone())),
291 Some(ShapeFill {
292 fill_type: "gradient".to_owned(),
293 color: None,
294 gradient: Some(GradientFill {
295 gradient_type: "linear".to_owned(),
296 angle: Some(90.0),
297 stops: vec![
298 GradientStop {
299 position: 0.0,
300 color: first.clone(),
301 },
302 GradientStop {
303 position: 50000.0,
304 color: rgb("123456"),
305 },
306 GradientStop {
307 position: 100000.0,
308 color: last,
309 },
310 ],
311 }),
312 }),
313 ],
314 lines: vec![Some(ShapeOutline {
315 color: Some(first),
316 ..Default::default()
317 })],
318 ..Default::default()
319 };
320 let mut theme = Theme::default();
321 theme.color_scheme.accent1 = "808080".to_owned();
322 let reference = |index| StyleReference {
323 index,
324 color: Some(ColorValue {
325 theme_color: Some("accent1".to_owned()),
326 luminance_modulation: Some(0.5),
327 luminance_offset: Some(0.1),
328 ..Default::default()
329 }),
330 };
331 let hex = |color: &ColorValue| {
332 resolve_color_value_to_hex_with_theme(Some(color), Some(&theme)).unwrap()
333 };
334 assert_eq!(
335 hex(style_fill(&scheme, &reference(1), &theme)
336 .color
337 .as_ref()
338 .unwrap()),
339 "#474747"
340 );
341 assert_eq!(
342 hex(style_outline(&scheme, &reference(1), &theme)
343 .unwrap()
344 .color
345 .as_ref()
346 .unwrap()),
347 "#474747"
348 );
349 let gradient = style_fill(&scheme, &reference(2), &theme).gradient.unwrap();
350 assert_eq!(gradient.angle, Some(90.0));
351 assert_eq!(
352 gradient
353 .stops
354 .iter()
355 .map(|s| (s.position, hex(&s.color)))
356 .collect::<Vec<_>>(),
357 vec![
358 (0.0, "#474747".to_owned()),
359 (50000.0, "#123456".to_owned()),
360 (100000.0, "#4A4A4A".to_owned())
361 ]
362 );
363 }
364
365 #[test]
366 fn line_indices_preserve_no_fill_slots_and_explicit_style_properties() {
367 let line = ShapeOutline {
368 width: Some(28575.0),
369 color: Some(rgb("123456")),
370 style: Some("dash".to_owned()),
371 cap: Some("rnd".to_owned()),
372 ..Default::default()
373 };
374 let scheme = ThemeFormatScheme {
375 lines: vec![Some(ShapeOutline::default()), None, Some(line.clone())],
376 ..Default::default()
377 };
378 let theme = Theme::default();
379 assert_eq!(style_outline(&scheme, &reference(0), &theme), None);
380 assert!(
381 style_outline(&scheme, &reference(1), &theme)
382 .unwrap()
383 .color
384 .is_none()
385 );
386 assert_eq!(style_outline(&scheme, &reference(3), &theme), Some(line));
387 for index in [2, 9] {
388 let outline = style_outline(&scheme, &reference(index), &theme).unwrap();
389 assert_eq!(outline.width, Some(9525.0));
390 assert_eq!(outline.color.unwrap().rgb.as_deref(), Some("808080"));
391 }
392 }
393}