//! Themes: colour scheme, font scheme and format scheme (the fill/line/effect "style matrix"). use deckcraft_color::{ColorScheme, ColorTransform, Rgba, SchemeSlot}; use serde::{Deserialize, Serialize}; use crate::style::{ColorRef, Effects, Fill, Gradient, GradientShape, GradientStop, Line, Shadow}; #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct FontSet { pub latin: String, pub ea: String, pub cs: String, /// Per-script typefaces (`a:font script="Arab" typeface="…"`), in document order. #[serde(skip_serializing_if = "Vec::is_empty")] pub scripts: Vec, } /// A theme font for one script (ISO 15924 tag, e.g. `Arab`). #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct ScriptFont { pub script: String, pub typeface: String, } impl FontSet { /// The typeface the theme gives `script` (`"Arab"`), if any. pub fn script(&self, script: &str) -> Option<&str> { self.scripts.iter().find(|f| f.script.eq_ignore_ascii_case(script) && !f.typeface.is_empty()).map(|f| f.typeface.as_str()) } } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct FontScheme { pub name: String, /// Headings. pub major: FontSet, /// Body. pub minor: FontSet, } #[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct FormatScheme { pub name: String, /// Subtle, moderate, intense (style index 1–3). pub fills: Vec, pub lines: Vec, pub effects: Vec, /// Background fills (index 1001–1003). pub bg_fills: Vec, } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(default, rename_all = "camelCase")] pub struct Theme { pub name: String, pub colors: ColorScheme, pub fonts: FontScheme, pub format: FormatScheme, /// Raw theme XML extras kept for round-trip (object defaults, extra colour lists). #[serde(skip_serializing_if = "Option::is_none")] pub raw_extra: Option, } impl Default for Theme { fn default() -> Self { builtin_themes().into_iter().next().unwrap_or_else(|| Theme { name: "DeckCraft".into(), colors: ColorScheme { name: "DeckCraft".into(), colors: [Rgba::BLACK; 12] }, fonts: FontScheme::default(), format: FormatScheme::default(), raw_extra: None, }) } } impl Theme { /// Resolve a theme font reference (`+mj-lt`, `+mn-ea`…) to a family name. pub fn font(&self, name: &str) -> String { match name { "+mj-lt" => self.fonts.major.latin.clone(), "+mn-lt" => self.fonts.minor.latin.clone(), "+mj-ea" => self.fonts.major.ea.clone(), "+mn-ea" => self.fonts.minor.ea.clone(), "+mj-cs" => self.fonts.major.cs.clone(), "+mn-cs" => self.fonts.minor.cs.clone(), other => other.to_string(), } } } fn hex(s: &str) -> Rgba { Rgba::from_hex(s).unwrap_or(Rgba::BLACK) } fn scheme(name: &str, c: [&str; 12]) -> ColorScheme { ColorScheme { name: name.into(), colors: c.map(hex) } } fn ph() -> ColorRef { ColorRef::scheme(SchemeSlot::PhClr) } /// Our default format scheme: flat solid fills, a soft gradient, and a gentle shadow for "intense". pub fn default_format() -> FormatScheme { let grad = |a: i32, b: i32| { Fill::Gradient(Gradient { stops: vec![ GradientStop { pos: 0.0, color: ph().with(ColorTransform::LumMod(a)).with(ColorTransform::SatMod(103000)) }, GradientStop { pos: 1.0, color: ph().with(ColorTransform::LumMod(b)) }, ], shape: GradientShape::Linear { angle: 90.0, scaled: false }, rotate_with_shape: true, }) }; FormatScheme { name: "DeckCraft".into(), fills: vec![Fill::solid(ph()), grad(110000, 92000), grad(105000, 80000)], lines: vec![Line::solid(ph(), 0.75), Line::solid(ph(), 1.0), Line::solid(ph(), 1.5)], effects: vec![ Effects::default(), Effects::default(), Effects { outer_shadow: Some(Shadow { color: ColorRef::rgb(Rgba::BLACK).with(ColorTransform::Alpha(40000)), blur: 6.0, dist: 3.0, dir: 90.0, inner: false, sx: 1.0, sy: 1.0, kx: 0.0, ky: 0.0, align: "t".into(), rotate_with_shape: false, }), ..Default::default() }, ], bg_fills: vec![ Fill::solid(ph()), Fill::solid(ph().with(ColorTransform::Tint(95000)).with(ColorTransform::SatMod(170000))), grad(102000, 85000), ], } } /// Our original built-in themes (shown in Design ▸ Themes). Colours and names are our own. pub fn builtin_themes() -> Vec { let t = |name: &str, colors: ColorScheme, major: &str, minor: &str| Theme { name: name.into(), colors, fonts: FontScheme { name: name.into(), major: FontSet { latin: major.into(), ..Default::default() }, minor: FontSet { latin: minor.into(), ..Default::default() }, }, format: default_format(), raw_extra: None, }; vec![ t( "DeckCraft", scheme( "DeckCraft", ["000000", "FFFFFF", "1E2B3C", "EDEBE7", "2E6FD8", "F26B1D", "2EA36F", "8A5CD8", "D83F6B", "E3B21C", "2E6FD8", "7A5BA6"], ), "Inter", "Inter", ), t( "Harbor", scheme( "Harbor", ["0B1F2A", "FFFFFF", "12384D", "E4EEF2", "1B7FA6", "36B3A8", "F2C14E", "F78154", "5D576B", "8AA29E", "1B7FA6", "5D576B"], ), "Merriweather", "Open Sans", ), t( "Ember", scheme("Ember", ["1A1A1A", "FFFCF7", "3D2B1F", "F6EDE3", "D9480F", "F59F00", "A61E4D", "5C940D", "1971C2", "862E9C", "D9480F", "862E9C"]), "Montserrat", "Lato", ), t( "Meadow", scheme( "Meadow", ["14281D", "FFFFFF", "2D4A37", "EEF5EE", "4C9A2A", "A4C639", "2F6F4E", "E9B44C", "9B2915", "50A2A7", "2F6F4E", "9B2915"], ), "Merriweather", "Open Sans", ), t( "Nocturne", scheme( "Nocturne", ["FFFFFF", "121826", "F2F4F8", "1E2638", "7C5CFF", "00C2A8", "FF6B9A", "FFC53D", "4DA3FF", "B2F35F", "7C5CFF", "FF6B9A"], ), "Inter", "Inter", ), t( "Paper", scheme("Paper", ["262626", "FFFFFF", "404040", "F2F2F2", "595959", "8C8C8C", "C0504D", "4F81BD", "9BBB59", "F79646", "4F81BD", "8064A2"]), "Playfair Display", "Lato", ), t( "Coral Reef", scheme( "Coral Reef", ["0F2A33", "FFFFFF", "0E4D64", "FDF1EC", "FF6F59", "254441", "43AA8B", "B2B09B", "EF3054", "3F88C5", "3F88C5", "EF3054"], ), "Poppins", "Nunito Sans", ), t( "Slate", scheme("Slate", ["111827", "FFFFFF", "374151", "F3F4F6", "6366F1", "0EA5E9", "10B981", "F59E0B", "EF4444", "8B5CF6", "6366F1", "8B5CF6"]), "Roboto", "Roboto", ), ] } /// Named colour schemes for Design ▸ Variants ▸ Colors. pub fn builtin_color_schemes() -> Vec { let mut v: Vec = builtin_themes().into_iter().map(|t| t.colors).collect(); v.push(scheme( "Grayscale", ["000000", "FFFFFF", "000000", "F8F8F8", "DDDDDD", "B2B2B2", "969696", "808080", "5F5F5F", "4D4D4D", "5F5F5F", "919191"], )); v.push(scheme("Ocean", ["000000", "FFFFFF", "13293D", "E8F1F2", "006494", "247BA0", "1B98E0", "6CC1E8", "13293D", "0B3954", "1B98E0", "6CC1E8"])); v.push(scheme( "Sunset", ["2B2118", "FFFFFF", "6F1A07", "FFF3E6", "FF9F1C", "FFBF69", "CB997E", "E76F51", "F4A261", "2A9D8F", "E76F51", "2A9D8F"], )); v.push(scheme( "Forest", ["1B1B1B", "FFFFFF", "283618", "FEFAE0", "606C38", "DDA15E", "BC6C25", "283618", "8A9A5B", "4F772D", "606C38", "BC6C25"], )); v.push(scheme("Berry", ["1F0A1D", "FFFFFF", "4A1942", "F7E8F3", "893168", "C6426E", "EB6A8F", "4A1942", "7D7ABC", "56A3A6", "893168", "56A3A6"])); v } /// Named font pairs for Design ▸ Variants ▸ Fonts. pub fn builtin_font_schemes() -> Vec { let f = |name: &str, major: &str, minor: &str| FontScheme { name: name.into(), major: FontSet { latin: major.into(), ..Default::default() }, minor: FontSet { latin: minor.into(), ..Default::default() }, }; vec![ f("Inter", "Inter", "Inter"), f("Classic", "Merriweather", "Open Sans"), f("Geometric", "Montserrat", "Lato"), f("Editorial", "Playfair Display", "Lato"), f("Humanist", "Merriweather", "Open Sans"), f("Friendly", "Poppins", "Nunito Sans"), f("Neutral", "Roboto", "Roboto"), f("Office Compatible", "Carlito", "Carlito"), f("Times / Arial", "Liberation Serif", "Liberation Sans"), ] }