270 lines
9.5 KiB
Rust
270 lines
9.5 KiB
Rust
//! 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<ScriptFont>,
|
||
}
|
||
|
||
/// 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<Fill>,
|
||
pub lines: Vec<Line>,
|
||
pub effects: Vec<Effects>,
|
||
/// Background fills (index 1001–1003).
|
||
pub bg_fills: Vec<Fill>,
|
||
}
|
||
|
||
#[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<String>,
|
||
}
|
||
|
||
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<Theme> {
|
||
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<ColorScheme> {
|
||
let mut v: Vec<ColorScheme> = 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<FontScheme> {
|
||
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"),
|
||
]
|
||
}
|