Skip to main content

ptsd/
theme.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use image::RgbaImage;
4use include_dir::{DirEntry, include_dir};
5use prism::{canvas, Assets};
6
7use std::fmt;
8use std::fmt::Display;
9
10pub use crate::color::Color;
11pub use crate::colors::*;
12
13#[derive(Default, Clone, Debug)]
14pub struct Theme {
15    pub colors: ColorResources,
16    pub fonts: FontResources,
17    pub icons: IconResources,
18}
19
20impl Theme {
21    pub fn light(color: Color) -> Self {
22        Theme { colors: ColorResources::light(color), ..Default::default() }
23    }
24
25    pub fn dark(color: Color) -> Self {
26        Theme { colors: ColorResources::dark(color), ..Default::default() }
27    }
28
29    pub fn from(color: Color) -> (Self, bool) {
30        let is_dark = color.is_high_contrast();
31        (if is_dark {Self::dark(color)} else {Self::light(color)}, is_dark)
32    }
33}
34
35#[derive(Debug, Clone)]
36pub struct FontResources {
37    sizes: HashMap<String, f32>,
38    fonts: HashMap<String, canvas::Font>,
39}
40
41impl FontResources {
42    pub fn insert_font<K: Display>(&mut self, key: K, font: canvas::Font) {
43        self.fonts.insert(key.to_string(), font);
44    }
45
46    pub fn insert_size<K: Display>(&mut self, key: K, size: f32) {
47        self.sizes.insert(key.to_string(), size);
48    }
49
50    pub fn get_font<K: Display>(&self, key: K) -> Option<&canvas::Font> {
51        self.fonts.get(&key.to_string())
52    }
53
54    pub fn get_size<K: Display>(&self, key: K) -> f32 {
55        self.sizes.get(&key.to_string()).copied().unwrap_or_default()
56    }
57}
58
59impl Default for FontResources {
60    fn default() -> Self {
61        let bold = canvas::Font::from_bytes(include_bytes!("../resources/fonts/outfit_bold.ttf")).unwrap();
62        let regular = canvas::Font::from_bytes(include_bytes!("../resources/fonts/outfit_regular.ttf")).unwrap();
63        let mut resources = FontResources {fonts: HashMap::new(), sizes: HashMap::new()};
64        resources.insert_font(FontStyle::Heading, bold.clone());
65        resources.insert_font(FontStyle::Text, regular);
66        resources.insert_font(FontStyle::Label, bold);
67        resources.insert_size(TextSize::Title, 72.0);
68        resources.insert_size(TextSize::H1, 48.0);
69        resources.insert_size(TextSize::H2, 32.0);
70        resources.insert_size(TextSize::H3, 24.0);
71        resources.insert_size(TextSize::H4, 20.0);
72        resources.insert_size(TextSize::H5, 16.0);
73        resources.insert_size(TextSize::H6, 14.0);
74        resources.insert_size(TextSize::Xl, 24.0);
75        resources.insert_size(TextSize::Lg, 20.0);
76        resources.insert_size(TextSize::Md, 16.0);
77        resources.insert_size(TextSize::Sm, 14.0);
78        resources.insert_size(TextSize::Xs, 12.0);
79        resources
80    }
81}
82
83pub enum FontStyle {Heading, Text, Label}
84impl fmt::Display for FontStyle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "FontStyle::{}", match self { 
85    FontStyle::Heading => "Heading", FontStyle::Text => "Text", FontStyle::Label => "Label" 
86}) } }
87
88#[derive(Debug, Default, Copy, Clone)]
89pub enum TextSize {Title, H1, H2, H3, H4, H5, H6, Xl, #[default] Lg, Md, Sm, Xs}
90impl fmt::Display for TextSize { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "TextSize::{}", match self { 
91    TextSize::Title => "Title", TextSize::H1 => "H1", TextSize::H2 => "H2", TextSize::H3 => "H3", TextSize::H4 => "H4", 
92    TextSize::H5 => "H5", TextSize::H6 => "H6", TextSize::Xl => "Xl", TextSize::Lg => "Lg", TextSize::Md => "Md", 
93    TextSize::Sm => "Sm", TextSize::Xs => "Xs" 
94}) } }
95
96
97/// - Icons will automatically be added to resources when they meet these conditions:
98///     - Icons must be `.svg` files.
99///     - Icons must be located in `project/resources/icons/`.
100#[derive(Debug, Clone)]
101pub struct IconResources(HashMap<String, Arc<RgbaImage>>);
102
103impl Default for IconResources {
104    fn default() -> Self {
105        let result = include_dir!("resources/icons").entries().iter().filter_map(|e| match e {
106            DirEntry::File(f) => Some(f),
107            _ => None,
108        }).filter(|p| p.path().to_str().unwrap().ends_with(".svg")).collect::<Vec<_>>();
109
110        Self(result.iter().map(|p| {
111            let name = p.path().to_str().unwrap().strip_suffix(".svg").unwrap().replace(' ', "_");
112            (name, Arc::new(Assets::load_svg(p.contents())))
113        }).collect())
114    }
115}
116
117impl IconResources {
118    pub fn get(&self, name: &str) -> Arc<RgbaImage> {
119        self.0.get(name).unwrap_or_else(|| {
120            println!("Failed to get icon by name {name:?}");
121            self.0.get("error").expect("IconResources corrupted.")
122        }).clone()
123    }
124}
125
126#[derive(Debug, Clone)]
127pub struct ColorResources(HashMap<String, Color>);
128
129impl ColorResources {
130    pub fn insert<K: Display>(&mut self, key: K, color: Color) {
131        self.0.insert(key.to_string(), color);
132    }
133
134    pub fn get<K: Display>(&self, key: K) -> Color {
135        self.0.get(&key.to_string()).cloned().unwrap_or_default()
136    }
137
138    pub fn light(brand: Color) -> Self {
139        let mut colors = ColorResources(HashMap::new());
140        colors.insert(Background::Primary, Color::WHITE);
141        colors.insert(Background::Secondary, Color::from_hex("#DDDDDD", 255));
142        colors.insert(Text::Primary, Color::BLACK);
143        colors.insert(Text::Secondary, Color::from_hex("#9e9e9e", 255));
144        colors.insert(Text::Heading, Color::BLACK);
145        colors.insert(Outline::Primary, Color::from_hex("#585250", 255));
146        colors.insert(Outline::Secondary, Color::from_hex("#9e9e9e", 255));
147        colors.insert(Status::Success, Color::from_hex("#3ccb5a", 255));
148        colors.insert(Status::Warning, Color::from_hex("#f5bd14", 255));
149        colors.insert(Status::Danger, Color::from_hex("#ff330a", 255));
150        colors.insert(Brand, brand);
151        colors
152    }
153
154    pub fn dark(brand: Color) -> Self {
155        let mut colors = ColorResources(HashMap::new());
156        colors.insert(Background::Primary, Color::BLACK);
157        colors.insert(Background::Secondary, Color::from_hex("#262322", 255));
158        colors.insert(Text::Primary, Color::WHITE);
159        colors.insert(Text::Secondary, Color::from_hex("#a7a29d", 255));
160        colors.insert(Text::Heading, Color::WHITE);
161        colors.insert(Outline::Primary, Color::from_hex("#585250", 255));
162        colors.insert(Outline::Secondary, Color::from_hex("#a7a29d", 255));
163        colors.insert(Status::Success, Color::from_hex("#3ccb5a", 255));
164        colors.insert(Status::Warning, Color::from_hex("#f5bd14", 255));
165        colors.insert(Status::Danger, Color::from_hex("#ff330a", 255));
166        colors.insert(Brand, brand);
167        colors
168    }
169}
170
171impl Default for ColorResources { fn default() -> Self { ColorResources::dark(Color::from_hex("#1ca758", 255)) } }