Skip to main content

deckcraft_model/
theme.rs

1//! Themes: colour scheme, font scheme and format scheme (the fill/line/effect "style matrix").
2
3use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot};
4use serde::{Deserialize, Serialize};
5
6use crate::style::{ColorRef, Effects, Fill, Gradient, GradientShape, GradientStop, Line, Shadow};
7
8#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
9#[serde(default, rename_all = "camelCase")]
10pub struct FontSet {
11    pub latin: String,
12    pub ea: String,
13    pub cs: String,
14    /// Per-script typefaces (`a:font script="Arab" typeface="…"`), in document order.
15    #[serde(skip_serializing_if = "Vec::is_empty")]
16    pub scripts: Vec<ScriptFont>,
17}
18
19/// A theme font for one script (ISO 15924 tag, e.g. `Arab`).
20#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
21#[serde(default, rename_all = "camelCase")]
22pub struct ScriptFont {
23    pub script: String,
24    pub typeface: String,
25}
26
27impl FontSet {
28    /// The typeface the theme gives `script` (`"Arab"`), if any.
29    pub fn script(&self, script: &str) -> Option<&str> {
30        self.scripts
31            .iter()
32            .find(|f| f.script.eq_ignore_ascii_case(script) && !f.typeface.is_empty())
33            .map(|f| f.typeface.as_str())
34    }
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
38#[serde(default, rename_all = "camelCase")]
39pub struct FontScheme {
40    pub name: String,
41    /// Headings.
42    pub major: FontSet,
43    /// Body.
44    pub minor: FontSet,
45}
46
47#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
48#[serde(default, rename_all = "camelCase")]
49pub struct FormatScheme {
50    pub name: String,
51    /// Subtle, moderate, intense (style index 1–3).
52    pub fills: Vec<Fill>,
53    pub lines: Vec<Line>,
54    pub effects: Vec<Effects>,
55    /// Background fills (index 1001–1003).
56    pub bg_fills: Vec<Fill>,
57}
58
59#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
60#[serde(default, rename_all = "camelCase")]
61pub struct Theme {
62    pub name: String,
63    pub colors: ColorScheme,
64    pub fonts: FontScheme,
65    pub format: FormatScheme,
66    /// Raw theme XML extras kept for round-trip (object defaults, extra colour lists).
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub raw_extra: Option<String>,
69}
70
71impl Default for Theme {
72    fn default() -> Self {
73        builtin_themes()
74            .into_iter()
75            .next()
76            .unwrap_or_else(|| Theme {
77                name: "DeckCraft".into(),
78                colors: ColorScheme {
79                    name: "DeckCraft".into(),
80                    colors: [Rgba::BLACK; 12],
81                },
82                fonts: FontScheme::default(),
83                format: FormatScheme::default(),
84                raw_extra: None,
85            })
86    }
87}
88
89impl Theme {
90    /// Resolve a theme font reference (`+mj-lt`, `+mn-ea`…) to a family name.
91    pub fn font(&self, name: &str) -> String {
92        match name {
93            "+mj-lt" => self.fonts.major.latin.clone(),
94            "+mn-lt" => self.fonts.minor.latin.clone(),
95            "+mj-ea" => self.fonts.major.ea.clone(),
96            "+mn-ea" => self.fonts.minor.ea.clone(),
97            "+mj-cs" => self.fonts.major.cs.clone(),
98            "+mn-cs" => self.fonts.minor.cs.clone(),
99            other => other.to_string(),
100        }
101    }
102}
103
104fn hex(s: &str) -> Rgba {
105    Rgba::from_hex(s).unwrap_or(Rgba::BLACK)
106}
107
108fn scheme(name: &str, c: [&str; 12]) -> ColorScheme {
109    ColorScheme {
110        name: name.into(),
111        colors: c.map(hex),
112    }
113}
114
115fn ph() -> ColorRef {
116    ColorRef::scheme(SchemeSlot::PhClr)
117}
118
119/// Our default format scheme: flat solid fills, a soft gradient, and a gentle shadow for "intense".
120pub fn default_format() -> FormatScheme {
121    let grad = |a: i32, b: i32| {
122        Fill::Gradient(Gradient {
123            stops: vec![
124                GradientStop {
125                    pos: 0.0,
126                    color: ph()
127                        .with(ColorTransform::LumMod(a))
128                        .with(ColorTransform::SatMod(103000)),
129                },
130                GradientStop {
131                    pos: 1.0,
132                    color: ph().with(ColorTransform::LumMod(b)),
133                },
134            ],
135            shape: GradientShape::Linear {
136                angle: 90.0,
137                scaled: false,
138            },
139            rotate_with_shape: true,
140        })
141    };
142    FormatScheme {
143        name: "DeckCraft".into(),
144        fills: vec![Fill::solid(ph()), grad(110000, 92000), grad(105000, 80000)],
145        lines: vec![
146            Line::solid(ph(), 0.75),
147            Line::solid(ph(), 1.0),
148            Line::solid(ph(), 1.5),
149        ],
150        effects: vec![
151            Effects::default(),
152            Effects::default(),
153            Effects {
154                outer_shadow: Some(Shadow {
155                    color: ColorRef::rgb(Rgba::BLACK).with(ColorTransform::Alpha(40000)),
156                    blur: 6.0,
157                    dist: 3.0,
158                    dir: 90.0,
159                    inner: false,
160                    sx: 1.0,
161                    sy: 1.0,
162                    kx: 0.0,
163                    ky: 0.0,
164                    align: "t".into(),
165                    rotate_with_shape: false,
166                }),
167                ..Default::default()
168            },
169        ],
170        bg_fills: vec![
171            Fill::solid(ph()),
172            Fill::solid(
173                ph().with(ColorTransform::Tint(95000))
174                    .with(ColorTransform::SatMod(170000)),
175            ),
176            grad(102000, 85000),
177        ],
178    }
179}
180
181/// Our original built-in themes (shown in Design ▸ Themes). Colours and names are our own.
182pub fn builtin_themes() -> Vec<Theme> {
183    let t = |name: &str, colors: ColorScheme, major: &str, minor: &str| Theme {
184        name: name.into(),
185        colors,
186        fonts: FontScheme {
187            name: name.into(),
188            major: FontSet {
189                latin: major.into(),
190                ..Default::default()
191            },
192            minor: FontSet {
193                latin: minor.into(),
194                ..Default::default()
195            },
196        },
197        format: default_format(),
198        raw_extra: None,
199    };
200    vec![
201        t(
202            "DeckCraft",
203            scheme(
204                "DeckCraft",
205                [
206                    "000000", "FFFFFF", "1E2B3C", "EDEBE7", "2E6FD8", "F26B1D", "2EA36F", "8A5CD8",
207                    "D83F6B", "E3B21C", "2E6FD8", "7A5BA6",
208                ],
209            ),
210            "Inter",
211            "Inter",
212        ),
213        t(
214            "Harbor",
215            scheme(
216                "Harbor",
217                [
218                    "0B1F2A", "FFFFFF", "12384D", "E4EEF2", "1B7FA6", "36B3A8", "F2C14E", "F78154",
219                    "5D576B", "8AA29E", "1B7FA6", "5D576B",
220                ],
221            ),
222            "Merriweather",
223            "Open Sans",
224        ),
225        t(
226            "Ember",
227            scheme(
228                "Ember",
229                [
230                    "1A1A1A", "FFFCF7", "3D2B1F", "F6EDE3", "D9480F", "F59F00", "A61E4D", "5C940D",
231                    "1971C2", "862E9C", "D9480F", "862E9C",
232                ],
233            ),
234            "Montserrat",
235            "Lato",
236        ),
237        t(
238            "Meadow",
239            scheme(
240                "Meadow",
241                [
242                    "14281D", "FFFFFF", "2D4A37", "EEF5EE", "4C9A2A", "A4C639", "2F6F4E", "E9B44C",
243                    "9B2915", "50A2A7", "2F6F4E", "9B2915",
244                ],
245            ),
246            "Merriweather",
247            "Open Sans",
248        ),
249        t(
250            "Nocturne",
251            scheme(
252                "Nocturne",
253                [
254                    "FFFFFF", "121826", "F2F4F8", "1E2638", "7C5CFF", "00C2A8", "FF6B9A", "FFC53D",
255                    "4DA3FF", "B2F35F", "7C5CFF", "FF6B9A",
256                ],
257            ),
258            "Inter",
259            "Inter",
260        ),
261        t(
262            "Paper",
263            scheme(
264                "Paper",
265                [
266                    "262626", "FFFFFF", "404040", "F2F2F2", "595959", "8C8C8C", "C0504D", "4F81BD",
267                    "9BBB59", "F79646", "4F81BD", "8064A2",
268                ],
269            ),
270            "Playfair Display",
271            "Lato",
272        ),
273        t(
274            "Coral Reef",
275            scheme(
276                "Coral Reef",
277                [
278                    "0F2A33", "FFFFFF", "0E4D64", "FDF1EC", "FF6F59", "254441", "43AA8B", "B2B09B",
279                    "EF3054", "3F88C5", "3F88C5", "EF3054",
280                ],
281            ),
282            "Poppins",
283            "Nunito Sans",
284        ),
285        t(
286            "Slate",
287            scheme(
288                "Slate",
289                [
290                    "111827", "FFFFFF", "374151", "F3F4F6", "6366F1", "0EA5E9", "10B981", "F59E0B",
291                    "EF4444", "8B5CF6", "6366F1", "8B5CF6",
292                ],
293            ),
294            "Roboto",
295            "Roboto",
296        ),
297    ]
298}
299
300/// Named colour schemes for Design ▸ Variants ▸ Colors.
301pub fn builtin_color_schemes() -> Vec<ColorScheme> {
302    let mut v: Vec<ColorScheme> = builtin_themes().into_iter().map(|t| t.colors).collect();
303    v.push(scheme(
304        "Grayscale",
305        [
306            "000000", "FFFFFF", "000000", "F8F8F8", "DDDDDD", "B2B2B2", "969696", "808080",
307            "5F5F5F", "4D4D4D", "5F5F5F", "919191",
308        ],
309    ));
310    v.push(scheme(
311        "Ocean",
312        [
313            "000000", "FFFFFF", "13293D", "E8F1F2", "006494", "247BA0", "1B98E0", "6CC1E8",
314            "13293D", "0B3954", "1B98E0", "6CC1E8",
315        ],
316    ));
317    v.push(scheme(
318        "Sunset",
319        [
320            "2B2118", "FFFFFF", "6F1A07", "FFF3E6", "FF9F1C", "FFBF69", "CB997E", "E76F51",
321            "F4A261", "2A9D8F", "E76F51", "2A9D8F",
322        ],
323    ));
324    v.push(scheme(
325        "Forest",
326        [
327            "1B1B1B", "FFFFFF", "283618", "FEFAE0", "606C38", "DDA15E", "BC6C25", "283618",
328            "8A9A5B", "4F772D", "606C38", "BC6C25",
329        ],
330    ));
331    v.push(scheme(
332        "Berry",
333        [
334            "1F0A1D", "FFFFFF", "4A1942", "F7E8F3", "893168", "C6426E", "EB6A8F", "4A1942",
335            "7D7ABC", "56A3A6", "893168", "56A3A6",
336        ],
337    ));
338    v
339}
340
341/// Named font pairs for Design ▸ Variants ▸ Fonts.
342pub fn builtin_font_schemes() -> Vec<FontScheme> {
343    let f = |name: &str, major: &str, minor: &str| FontScheme {
344        name: name.into(),
345        major: FontSet {
346            latin: major.into(),
347            ..Default::default()
348        },
349        minor: FontSet {
350            latin: minor.into(),
351            ..Default::default()
352        },
353    };
354    vec![
355        f("Inter", "Inter", "Inter"),
356        f("Classic", "Merriweather", "Open Sans"),
357        f("Geometric", "Montserrat", "Lato"),
358        f("Editorial", "Playfair Display", "Lato"),
359        f("Humanist", "Merriweather", "Open Sans"),
360        f("Friendly", "Poppins", "Nunito Sans"),
361        f("Neutral", "Roboto", "Roboto"),
362        f("Office Compatible", "Carlito", "Carlito"),
363        f("Times / Arial", "Liberation Serif", "Liberation Sans"),
364    ]
365}