raui_material/
theme.rs

1use raui_core::{
2    widget::{
3        unit::{
4            image::{ImageBoxImage, ImageBoxProcedural},
5            text::{TextBoxDirection, TextBoxFont, TextBoxHorizontalAlign, TextBoxVerticalAlign},
6        },
7        utils::{Color, lerp_clamped},
8    },
9    {PropsData, Scalar},
10};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::f32::consts::PI;
14
15const DEFAULT_BACKGROUND_MIXING_FACTOR: Scalar = 0.1;
16const DEFAULT_VARIANT_MIXING_FACTOR: Scalar = 0.2;
17
18#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub enum ThemeColor {
20    #[default]
21    Default,
22    Primary,
23    Secondary,
24}
25
26#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
27pub enum ThemeColorVariant {
28    #[default]
29    Main,
30    Light,
31    Dark,
32}
33
34#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub enum ThemeVariant {
36    ContentOnly,
37    #[default]
38    Filled,
39    Outline,
40}
41
42#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
43#[props_data(raui_core::props::PropsData)]
44#[prefab(raui_core::Prefab)]
45pub struct ThemedWidgetProps {
46    #[serde(default)]
47    pub color: ThemeColor,
48    #[serde(default)]
49    pub color_variant: ThemeColorVariant,
50    #[serde(default)]
51    pub variant: ThemeVariant,
52}
53
54#[derive(Debug, Default, Clone, Serialize, Deserialize)]
55pub struct ThemeColorSet {
56    #[serde(default)]
57    pub main: Color,
58    #[serde(default)]
59    pub light: Color,
60    #[serde(default)]
61    pub dark: Color,
62}
63
64impl ThemeColorSet {
65    pub fn uniform(color: Color) -> Self {
66        Self {
67            main: color,
68            light: color,
69            dark: color,
70        }
71    }
72
73    pub fn get(&self, variant: ThemeColorVariant) -> Color {
74        match variant {
75            ThemeColorVariant::Main => self.main,
76            ThemeColorVariant::Light => self.light,
77            ThemeColorVariant::Dark => self.dark,
78        }
79    }
80
81    pub fn get_themed(&self, themed: &ThemedWidgetProps) -> Color {
82        self.get(themed.color_variant)
83    }
84}
85
86#[derive(Debug, Default, Clone, Serialize, Deserialize)]
87pub struct ThemeColors {
88    #[serde(default)]
89    pub default: ThemeColorSet,
90    #[serde(default)]
91    pub primary: ThemeColorSet,
92    #[serde(default)]
93    pub secondary: ThemeColorSet,
94}
95
96impl ThemeColors {
97    pub fn uniform(set: ThemeColorSet) -> Self {
98        Self {
99            default: set.to_owned(),
100            primary: set.to_owned(),
101            secondary: set,
102        }
103    }
104
105    pub fn get(&self, color: ThemeColor, variant: ThemeColorVariant) -> Color {
106        match color {
107            ThemeColor::Default => self.default.get(variant),
108            ThemeColor::Primary => self.primary.get(variant),
109            ThemeColor::Secondary => self.secondary.get(variant),
110        }
111    }
112
113    pub fn get_themed(&self, themed: &ThemedWidgetProps) -> Color {
114        self.get(themed.color, themed.color_variant)
115    }
116}
117
118#[derive(Debug, Default, Clone, Serialize, Deserialize)]
119pub struct ThemeColorsBundle {
120    #[serde(default)]
121    pub main: ThemeColors,
122    #[serde(default)]
123    pub contrast: ThemeColors,
124}
125
126impl ThemeColorsBundle {
127    pub fn uniform(colors: ThemeColors) -> Self {
128        Self {
129            main: colors.to_owned(),
130            contrast: colors,
131        }
132    }
133
134    pub fn get(&self, use_main: bool, color: ThemeColor, variant: ThemeColorVariant) -> Color {
135        if use_main {
136            self.main.get(color, variant)
137        } else {
138            self.contrast.get(color, variant)
139        }
140    }
141
142    pub fn get_themed(&self, use_main: bool, themed: &ThemedWidgetProps) -> Color {
143        self.get(use_main, themed.color, themed.color_variant)
144    }
145}
146
147#[derive(Debug, Default, Clone, Serialize, Deserialize)]
148pub enum ThemedImageMaterial {
149    #[default]
150    Color,
151    Image(ImageBoxImage),
152    Procedural(ImageBoxProcedural),
153}
154
155#[derive(Debug, Default, Clone, Serialize, Deserialize)]
156pub struct ThemedTextMaterial {
157    #[serde(default)]
158    pub horizontal_align: TextBoxHorizontalAlign,
159    #[serde(default)]
160    pub vertical_align: TextBoxVerticalAlign,
161    #[serde(default)]
162    pub direction: TextBoxDirection,
163    #[serde(default)]
164    pub font: TextBoxFont,
165}
166
167#[derive(Debug, Default, Clone, Serialize, Deserialize)]
168pub struct ThemedButtonMaterial {
169    #[serde(default)]
170    pub default: ThemedImageMaterial,
171    #[serde(default)]
172    pub selected: ThemedImageMaterial,
173    #[serde(default)]
174    pub trigger: ThemedImageMaterial,
175}
176
177#[derive(Debug, Default, Clone, Serialize, Deserialize)]
178pub struct ThemedSwitchMaterial {
179    #[serde(default)]
180    pub on: ThemedImageMaterial,
181    #[serde(default)]
182    pub off: ThemedImageMaterial,
183}
184
185#[derive(Debug, Default, Clone, Serialize, Deserialize)]
186pub struct ThemedSliderMaterial {
187    #[serde(default)]
188    pub background: ThemedImageMaterial,
189    #[serde(default)]
190    pub filling: ThemedImageMaterial,
191}
192
193#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
194#[props_data(raui_core::props::PropsData)]
195#[prefab(raui_core::Prefab)]
196pub struct ThemeProps {
197    #[serde(default)]
198    pub active_colors: ThemeColorsBundle,
199    #[serde(default)]
200    pub background_colors: ThemeColorsBundle,
201    #[serde(default)]
202    #[serde(skip_serializing_if = "HashMap::is_empty")]
203    pub content_backgrounds: HashMap<String, ThemedImageMaterial>,
204    #[serde(default)]
205    #[serde(skip_serializing_if = "HashMap::is_empty")]
206    pub button_backgrounds: HashMap<String, ThemedButtonMaterial>,
207    #[serde(default)]
208    #[serde(skip_serializing_if = "Vec::is_empty")]
209    pub icons_level_sizes: Vec<Scalar>,
210    #[serde(default)]
211    #[serde(skip_serializing_if = "HashMap::is_empty")]
212    pub text_variants: HashMap<String, ThemedTextMaterial>,
213    #[serde(default)]
214    #[serde(skip_serializing_if = "HashMap::is_empty")]
215    pub switch_variants: HashMap<String, ThemedSwitchMaterial>,
216    #[serde(default)]
217    #[serde(skip_serializing_if = "HashMap::is_empty")]
218    pub slider_variants: HashMap<String, ThemedSliderMaterial>,
219    #[serde(default)]
220    #[serde(skip_serializing_if = "HashMap::is_empty")]
221    pub modal_shadow_variants: HashMap<String, Color>,
222}
223
224impl ThemeProps {
225    pub fn active_colors(mut self, bundle: ThemeColorsBundle) -> Self {
226        self.active_colors = bundle;
227        self
228    }
229
230    pub fn background_colors(mut self, bundle: ThemeColorsBundle) -> Self {
231        self.background_colors = bundle;
232        self
233    }
234
235    pub fn content_background(mut self, id: impl ToString, material: ThemedImageMaterial) -> Self {
236        self.content_backgrounds.insert(id.to_string(), material);
237        self
238    }
239
240    pub fn button_background(mut self, id: impl ToString, material: ThemedButtonMaterial) -> Self {
241        self.button_backgrounds.insert(id.to_string(), material);
242        self
243    }
244
245    pub fn icons_level_size(mut self, level: usize, size: Scalar) -> Self {
246        self.icons_level_sizes.insert(level, size);
247        self
248    }
249
250    pub fn text_variant(mut self, id: impl ToString, material: ThemedTextMaterial) -> Self {
251        self.text_variants.insert(id.to_string(), material);
252        self
253    }
254
255    pub fn switch_variant(mut self, id: impl ToString, material: ThemedSwitchMaterial) -> Self {
256        self.switch_variants.insert(id.to_string(), material);
257        self
258    }
259
260    pub fn slider_variant(mut self, id: impl ToString, material: ThemedSliderMaterial) -> Self {
261        self.slider_variants.insert(id.to_string(), material);
262        self
263    }
264
265    pub fn modal_shadow_variant(mut self, id: impl ToString, color: Color) -> Self {
266        self.modal_shadow_variants.insert(id.to_string(), color);
267        self
268    }
269}
270
271pub fn new_light_theme() -> ThemeProps {
272    new_light_theme_parameterized(
273        DEFAULT_BACKGROUND_MIXING_FACTOR,
274        DEFAULT_VARIANT_MIXING_FACTOR,
275    )
276}
277
278pub fn new_light_theme_parameterized(
279    background_mixing_factor: Scalar,
280    variant_mixing_factor: Scalar,
281) -> ThemeProps {
282    new_default_theme_parameterized(
283        color_from_rgba(241, 250, 238, 1.0),
284        color_from_rgba(29, 53, 87, 1.0),
285        color_from_rgba(230, 57, 70, 1.0),
286        color_from_rgba(255, 255, 255, 1.0),
287        background_mixing_factor,
288        variant_mixing_factor,
289    )
290}
291
292pub fn new_dark_theme() -> ThemeProps {
293    new_dark_theme_parameterized(
294        DEFAULT_BACKGROUND_MIXING_FACTOR,
295        DEFAULT_VARIANT_MIXING_FACTOR,
296    )
297}
298
299pub fn new_dark_theme_parameterized(
300    background_mixing_factor: Scalar,
301    variant_mixing_factor: Scalar,
302) -> ThemeProps {
303    new_default_theme_parameterized(
304        color_from_rgba(64, 64, 64, 1.0),
305        color_from_rgba(255, 98, 86, 1.0),
306        color_from_rgba(0, 196, 228, 1.0),
307        color_from_rgba(32, 32, 32, 1.0),
308        background_mixing_factor,
309        variant_mixing_factor,
310    )
311}
312
313pub fn new_all_white_theme() -> ThemeProps {
314    new_default_theme(
315        color_from_rgba(255, 255, 255, 1.0),
316        color_from_rgba(255, 255, 255, 1.0),
317        color_from_rgba(255, 255, 255, 1.0),
318        color_from_rgba(255, 255, 255, 1.0),
319    )
320}
321
322pub fn new_default_theme(
323    default: Color,
324    primary: Color,
325    secondary: Color,
326    background: Color,
327) -> ThemeProps {
328    new_default_theme_parameterized(
329        default,
330        primary,
331        secondary,
332        background,
333        DEFAULT_BACKGROUND_MIXING_FACTOR,
334        DEFAULT_VARIANT_MIXING_FACTOR,
335    )
336}
337
338pub fn new_default_theme_parameterized(
339    default: Color,
340    primary: Color,
341    secondary: Color,
342    background: Color,
343    background_mixing_factor: Scalar,
344    variant_mixing_factor: Scalar,
345) -> ThemeProps {
346    let background_primary = color_lerp(background, primary, background_mixing_factor);
347    let background_secondary = color_lerp(background, secondary, background_mixing_factor);
348    let mut background_modal = fluid_polarize_color(background);
349    background_modal.a = 0.75;
350    let mut content_backgrounds = HashMap::with_capacity(1);
351    content_backgrounds.insert(String::new(), Default::default());
352    let mut button_backgrounds = HashMap::with_capacity(1);
353    button_backgrounds.insert(String::new(), Default::default());
354    let mut text_variants = HashMap::with_capacity(1);
355    text_variants.insert(
356        String::new(),
357        ThemedTextMaterial {
358            font: TextBoxFont {
359                size: 18.0,
360                ..Default::default()
361            },
362            ..Default::default()
363        },
364    );
365    let mut switch_variants = HashMap::with_capacity(4);
366    switch_variants.insert(String::new(), ThemedSwitchMaterial::default());
367    switch_variants.insert("checkbox".to_owned(), ThemedSwitchMaterial::default());
368    switch_variants.insert("toggle".to_owned(), ThemedSwitchMaterial::default());
369    switch_variants.insert("radio".to_owned(), ThemedSwitchMaterial::default());
370    let mut slider_variants = HashMap::with_capacity(1);
371    let mut modal_shadow_variants = HashMap::with_capacity(1);
372    slider_variants.insert(String::default(), ThemedSliderMaterial::default());
373    modal_shadow_variants.insert(String::new(), background_modal);
374    ThemeProps {
375        active_colors: make_colors_bundle(
376            make_color_set(default, variant_mixing_factor, variant_mixing_factor),
377            make_color_set(primary, variant_mixing_factor, variant_mixing_factor),
378            make_color_set(secondary, variant_mixing_factor, variant_mixing_factor),
379        ),
380        background_colors: make_colors_bundle(
381            make_color_set(background, variant_mixing_factor, variant_mixing_factor),
382            make_color_set(
383                background_primary,
384                variant_mixing_factor,
385                variant_mixing_factor,
386            ),
387            make_color_set(
388                background_secondary,
389                variant_mixing_factor,
390                variant_mixing_factor,
391            ),
392        ),
393        content_backgrounds,
394        button_backgrounds,
395        icons_level_sizes: vec![18.0, 24.0, 32.0, 48.0, 64.0, 128.0, 256.0, 512.0, 1024.0],
396        text_variants,
397        switch_variants,
398        slider_variants,
399        modal_shadow_variants,
400    }
401}
402
403pub fn color_from_rgba(r: u8, g: u8, b: u8, a: Scalar) -> Color {
404    Color {
405        r: r as Scalar / 255.0,
406        g: g as Scalar / 255.0,
407        b: b as Scalar / 255.0,
408        a,
409    }
410}
411
412pub fn make_colors_bundle(
413    default: ThemeColorSet,
414    primary: ThemeColorSet,
415    secondary: ThemeColorSet,
416) -> ThemeColorsBundle {
417    let contrast = ThemeColors {
418        default: ThemeColorSet {
419            main: contrast_color(default.main),
420            light: contrast_color(default.light),
421            dark: contrast_color(default.dark),
422        },
423        primary: ThemeColorSet {
424            main: contrast_color(primary.main),
425            light: contrast_color(primary.light),
426            dark: contrast_color(primary.dark),
427        },
428        secondary: ThemeColorSet {
429            main: contrast_color(secondary.main),
430            light: contrast_color(secondary.light),
431            dark: contrast_color(secondary.dark),
432        },
433    };
434    let main = ThemeColors {
435        default,
436        primary,
437        secondary,
438    };
439    ThemeColorsBundle { main, contrast }
440}
441
442pub fn contrast_color(base_color: Color) -> Color {
443    Color {
444        r: 1.0 - base_color.r,
445        g: 1.0 - base_color.g,
446        b: 1.0 - base_color.b,
447        a: base_color.a,
448    }
449}
450
451pub fn fluid_polarize(v: Scalar) -> Scalar {
452    (v - 0.5 * PI).sin() * 0.5 + 0.5
453}
454
455pub fn fluid_polarize_color(color: Color) -> Color {
456    Color {
457        r: fluid_polarize(color.r),
458        g: fluid_polarize(color.g),
459        b: fluid_polarize(color.b),
460        a: color.a,
461    }
462}
463
464pub fn make_color_set(base_color: Color, lighter: Scalar, darker: Scalar) -> ThemeColorSet {
465    let main = base_color;
466    let light = Color {
467        r: lerp_clamped(main.r, 1.0, lighter),
468        g: lerp_clamped(main.g, 1.0, lighter),
469        b: lerp_clamped(main.b, 1.0, lighter),
470        a: main.a,
471    };
472    let dark = Color {
473        r: lerp_clamped(main.r, 0.0, darker),
474        g: lerp_clamped(main.g, 0.0, darker),
475        b: lerp_clamped(main.b, 0.0, darker),
476        a: main.a,
477    };
478    ThemeColorSet { main, light, dark }
479}
480
481pub fn color_lerp(from: Color, to: Color, factor: Scalar) -> Color {
482    Color {
483        r: lerp_clamped(from.r, to.r, factor),
484        g: lerp_clamped(from.g, to.g, factor),
485        b: lerp_clamped(from.b, to.b, factor),
486        a: lerp_clamped(from.a, to.a, factor),
487    }
488}