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}