1use astrelis_render::Color;
28
29#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31pub enum ColorRole {
32 Primary,
34 Secondary,
36 Background,
38 Surface,
40 Error,
42 Warning,
44 Success,
46 Info,
48 TextPrimary,
50 TextSecondary,
52 TextDisabled,
54 Border,
56 Divider,
58}
59
60#[derive(Debug, Clone)]
62pub struct ColorPalette {
63 pub primary: Color,
65 pub secondary: Color,
67 pub background: Color,
69 pub surface: Color,
71 pub error: Color,
73 pub warning: Color,
75 pub success: Color,
77 pub info: Color,
79 pub text_primary: Color,
81 pub text_secondary: Color,
83 pub text_disabled: Color,
85 pub border: Color,
87 pub divider: Color,
89 pub hover_overlay: Color,
91 pub active_overlay: Color,
93}
94
95impl ColorPalette {
96 pub fn get(&self, role: ColorRole) -> Color {
98 match role {
99 ColorRole::Primary => self.primary,
100 ColorRole::Secondary => self.secondary,
101 ColorRole::Background => self.background,
102 ColorRole::Surface => self.surface,
103 ColorRole::Error => self.error,
104 ColorRole::Warning => self.warning,
105 ColorRole::Success => self.success,
106 ColorRole::Info => self.info,
107 ColorRole::TextPrimary => self.text_primary,
108 ColorRole::TextSecondary => self.text_secondary,
109 ColorRole::TextDisabled => self.text_disabled,
110 ColorRole::Border => self.border,
111 ColorRole::Divider => self.divider,
112 }
113 }
114
115 pub fn dark() -> Self {
117 Self {
118 primary: Color::from_rgb_u8(140, 120, 255),
119 secondary: Color::from_rgb_u8(100, 150, 235),
120 background: Color::from_rgb_u8(10, 10, 14),
121 surface: Color::from_rgb_u8(18, 18, 24),
122 error: Color::from_rgb_u8(240, 80, 100),
123 warning: Color::from_rgb_u8(240, 180, 70),
124 success: Color::from_rgb_u8(60, 200, 130),
125 info: Color::from_rgb_u8(90, 175, 245),
126 text_primary: Color::from_rgb_u8(240, 240, 252),
127 text_secondary: Color::from_rgb_u8(120, 120, 145),
128 text_disabled: Color::from_rgb_u8(65, 65, 82),
129 border: Color::from_rgb_u8(35, 35, 48),
130 divider: Color::from_rgb_u8(25, 25, 35),
131 hover_overlay: Color::from_rgba_u8(255, 255, 255, 10),
132 active_overlay: Color::from_rgba_u8(255, 255, 255, 20),
133 }
134 }
135
136 pub fn light() -> Self {
138 Self {
139 primary: Color::from_rgb_u8(100, 80, 220),
140 secondary: Color::from_rgb_u8(70, 110, 195),
141 background: Color::from_rgb_u8(248, 248, 252),
142 surface: Color::from_rgb_u8(255, 255, 255),
143 error: Color::from_rgb_u8(210, 65, 80),
144 warning: Color::from_rgb_u8(205, 150, 50),
145 success: Color::from_rgb_u8(50, 165, 100),
146 info: Color::from_rgb_u8(70, 140, 210),
147 text_primary: Color::from_rgb_u8(25, 25, 35),
148 text_secondary: Color::from_rgb_u8(100, 100, 120),
149 text_disabled: Color::from_rgb_u8(165, 165, 180),
150 border: Color::from_rgb_u8(225, 225, 235),
151 divider: Color::from_rgb_u8(235, 235, 242),
152 hover_overlay: Color::from_rgba_u8(0, 0, 0, 8),
153 active_overlay: Color::from_rgba_u8(0, 0, 0, 16),
154 }
155 }
156}
157
158impl Default for ColorPalette {
159 fn default() -> Self {
160 Self::dark()
161 }
162}
163
164#[derive(Debug, Clone)]
166pub struct Typography {
167 pub font_family: String,
169 pub heading_sizes: [f32; 6],
171 pub body_size: f32,
173 pub small_size: f32,
175 pub tiny_size: f32,
177 pub line_height: f32,
179}
180
181impl Typography {
182 pub fn new() -> Self {
184 Self {
185 font_family: String::new(), heading_sizes: [48.0, 40.0, 32.0, 24.0, 20.0, 16.0],
187 body_size: 14.0,
188 small_size: 12.0,
189 tiny_size: 10.0,
190 line_height: 1.5,
191 }
192 }
193
194 pub fn heading_size(&self, level: usize) -> f32 {
196 if level == 0 || level > 6 {
197 self.body_size
198 } else {
199 self.heading_sizes[level - 1]
200 }
201 }
202}
203
204impl Default for Typography {
205 fn default() -> Self {
206 Self::new()
207 }
208}
209
210#[derive(Debug, Clone, Copy)]
212pub struct Spacing {
213 pub xs: f32,
215 pub sm: f32,
217 pub md: f32,
219 pub lg: f32,
221 pub xl: f32,
223 pub xxl: f32,
225}
226
227impl Spacing {
228 pub fn new() -> Self {
230 Self {
231 xs: 2.0,
232 sm: 4.0,
233 md: 8.0,
234 lg: 16.0,
235 xl: 24.0,
236 xxl: 32.0,
237 }
238 }
239
240 pub fn get(&self, name: &str) -> f32 {
242 match name {
243 "xs" => self.xs,
244 "sm" => self.sm,
245 "md" => self.md,
246 "lg" => self.lg,
247 "xl" => self.xl,
248 "xxl" => self.xxl,
249 _ => self.md,
250 }
251 }
252}
253
254impl Default for Spacing {
255 fn default() -> Self {
256 Self::new()
257 }
258}
259
260#[derive(Debug, Clone, Copy)]
262pub struct Shapes {
263 pub none: f32,
265 pub sm: f32,
267 pub md: f32,
269 pub lg: f32,
271 pub xl: f32,
273 pub full: f32,
275}
276
277impl Shapes {
278 pub fn new() -> Self {
280 Self {
281 none: 0.0,
282 sm: 4.0,
283 md: 6.0,
284 lg: 10.0,
285 xl: 14.0,
286 full: 9999.0, }
288 }
289
290 pub fn get(&self, name: &str) -> f32 {
292 match name {
293 "none" => self.none,
294 "sm" => self.sm,
295 "md" => self.md,
296 "lg" => self.lg,
297 "xl" => self.xl,
298 "full" => self.full,
299 _ => self.md,
300 }
301 }
302}
303
304impl Default for Shapes {
305 fn default() -> Self {
306 Self::new()
307 }
308}
309
310#[derive(Debug, Clone)]
312pub struct Theme {
313 pub colors: ColorPalette,
315 pub typography: Typography,
317 pub spacing: Spacing,
319 pub shapes: Shapes,
321}
322
323impl Theme {
324 pub fn new() -> Self {
326 Self {
327 colors: ColorPalette::default(),
328 typography: Typography::default(),
329 spacing: Spacing::default(),
330 shapes: Shapes::default(),
331 }
332 }
333
334 pub fn dark() -> Self {
336 Self {
337 colors: ColorPalette::dark(),
338 typography: Typography::new(),
339 spacing: Spacing::new(),
340 shapes: Shapes::new(),
341 }
342 }
343
344 pub fn light() -> Self {
346 Self {
347 colors: ColorPalette::light(),
348 typography: Typography::new(),
349 spacing: Spacing::new(),
350 shapes: Shapes::new(),
351 }
352 }
353
354 pub fn builder() -> ThemeBuilder {
356 ThemeBuilder::new()
357 }
358
359 pub fn color(&self, role: ColorRole) -> Color {
361 self.colors.get(role)
362 }
363}
364
365impl Default for Theme {
366 fn default() -> Self {
367 Self::dark()
368 }
369}
370
371pub struct ThemeBuilder {
373 theme: Theme,
374}
375
376impl ThemeBuilder {
377 pub fn new() -> Self {
379 Self {
380 theme: Theme::default(),
381 }
382 }
383
384 pub fn dark() -> Self {
386 Self {
387 theme: Theme::dark(),
388 }
389 }
390
391 pub fn light() -> Self {
393 Self {
394 theme: Theme::light(),
395 }
396 }
397
398 pub fn primary(mut self, color: Color) -> Self {
400 self.theme.colors.primary = color;
401 self
402 }
403
404 pub fn secondary(mut self, color: Color) -> Self {
406 self.theme.colors.secondary = color;
407 self
408 }
409
410 pub fn background(mut self, color: Color) -> Self {
412 self.theme.colors.background = color;
413 self
414 }
415
416 pub fn surface(mut self, color: Color) -> Self {
418 self.theme.colors.surface = color;
419 self
420 }
421
422 pub fn error(mut self, color: Color) -> Self {
424 self.theme.colors.error = color;
425 self
426 }
427
428 pub fn font_family(mut self, family: impl Into<String>) -> Self {
430 self.theme.typography.font_family = family.into();
431 self
432 }
433
434 pub fn body_size(mut self, size: f32) -> Self {
436 self.theme.typography.body_size = size;
437 self
438 }
439
440 pub fn colors(mut self, colors: ColorPalette) -> Self {
442 self.theme.colors = colors;
443 self
444 }
445
446 pub fn typography(mut self, typography: Typography) -> Self {
448 self.theme.typography = typography;
449 self
450 }
451
452 pub fn spacing(mut self, spacing: Spacing) -> Self {
454 self.theme.spacing = spacing;
455 self
456 }
457
458 pub fn shapes(mut self, shapes: Shapes) -> Self {
460 self.theme.shapes = shapes;
461 self
462 }
463
464 pub fn build(self) -> Theme {
466 self.theme
467 }
468}
469
470impl Default for ThemeBuilder {
471 fn default() -> Self {
472 Self::new()
473 }
474}
475
476#[cfg(test)]
477mod tests {
478 use super::*;
479
480 #[test]
481 fn test_dark_theme() {
482 let theme = Theme::dark();
483 assert_eq!(theme.colors.primary, Color::from_rgb_u8(140, 120, 255));
484 assert_eq!(theme.typography.body_size, 14.0);
485 }
486
487 #[test]
488 fn test_light_theme() {
489 let theme = Theme::light();
490 assert_eq!(theme.colors.background, Color::from_rgb_u8(248, 248, 252));
491 }
492
493 #[test]
494 fn test_theme_builder() {
495 let theme = Theme::builder()
496 .primary(Color::RED)
497 .secondary(Color::BLUE)
498 .body_size(16.0)
499 .build();
500
501 assert_eq!(theme.colors.primary, Color::RED);
502 assert_eq!(theme.colors.secondary, Color::BLUE);
503 assert_eq!(theme.typography.body_size, 16.0);
504 }
505
506 #[test]
507 fn test_color_roles() {
508 let theme = Theme::dark();
509 let primary = theme.color(ColorRole::Primary);
510 assert_eq!(primary, theme.colors.primary);
511 }
512
513 #[test]
514 fn test_spacing() {
515 let spacing = Spacing::new();
516 assert_eq!(spacing.xs, 2.0);
517 assert_eq!(spacing.lg, 16.0);
518 assert_eq!(spacing.get("md"), 8.0);
519 }
520
521 #[test]
522 fn test_shapes() {
523 let shapes = Shapes::new();
524 assert_eq!(shapes.sm, 4.0);
525 assert_eq!(shapes.lg, 10.0);
526 assert_eq!(shapes.get("md"), 6.0);
527 }
528
529 #[test]
530 fn test_typography_heading_sizes() {
531 let typography = Typography::new();
532 assert_eq!(typography.heading_size(1), 48.0);
533 assert_eq!(typography.heading_size(6), 16.0);
534 assert_eq!(typography.heading_size(0), typography.body_size);
535 assert_eq!(typography.heading_size(7), typography.body_size);
536 }
537}