1use embedded_graphics_core::pixelcolor::{Rgb565, RgbColor};
2
3use crate::{font::FontId, geometry::EdgeInsets};
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub struct Border {
7 pub color: Rgb565,
8 pub width: u8,
9}
10
11impl Border {
12 pub const fn none() -> Self {
13 Self {
14 color: Rgb565::BLACK,
15 width: 0,
16 }
17 }
18
19 pub const fn one(color: Rgb565) -> Self {
20 Self { color, width: 1 }
21 }
22}
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub struct Shadow {
26 pub color: Rgb565,
27 pub opacity: u8,
28 pub offset_x: i8,
29 pub offset_y: i8,
30 pub spread: u8,
31}
32
33impl Shadow {
34 pub const fn none() -> Option<Self> {
35 None
36 }
37
38 pub const fn soft() -> Self {
39 Self {
40 color: Rgb565::BLACK,
41 opacity: 96,
42 offset_x: 1,
43 offset_y: 2,
44 spread: 2,
45 }
46 }
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub enum GradientDirection {
51 Vertical,
52 Horizontal,
53}
54
55#[derive(Clone, Copy, Debug, PartialEq, Eq)]
56pub struct LinearGradient {
57 pub start: Rgb565,
58 pub end: Rgb565,
59 pub direction: GradientDirection,
60}
61
62impl LinearGradient {
63 pub const fn vertical(start: Rgb565, end: Rgb565) -> Self {
64 Self {
65 start,
66 end,
67 direction: GradientDirection::Vertical,
68 }
69 }
70
71 pub const fn horizontal(start: Rgb565, end: Rgb565) -> Self {
72 Self {
73 start,
74 end,
75 direction: GradientDirection::Horizontal,
76 }
77 }
78}
79
80#[derive(Clone, Copy, Debug, PartialEq, Eq)]
81pub struct AlphaLinearGradient {
82 pub start_color: Rgb565,
83 pub start_alpha: u8,
84 pub end_color: Rgb565,
85 pub end_alpha: u8,
86 pub direction: GradientDirection,
87}
88
89impl AlphaLinearGradient {
90 pub const fn new(
91 start_color: Rgb565,
92 start_alpha: u8,
93 end_color: Rgb565,
94 end_alpha: u8,
95 direction: GradientDirection,
96 ) -> Self {
97 Self {
98 start_color,
99 start_alpha,
100 end_color,
101 end_alpha,
102 direction,
103 }
104 }
105
106 pub const fn vertical(
107 start_color: Rgb565,
108 start_alpha: u8,
109 end_color: Rgb565,
110 end_alpha: u8,
111 ) -> Self {
112 Self::new(
113 start_color,
114 start_alpha,
115 end_color,
116 end_alpha,
117 GradientDirection::Vertical,
118 )
119 }
120
121 pub const fn horizontal(
122 start_color: Rgb565,
123 start_alpha: u8,
124 end_color: Rgb565,
125 end_alpha: u8,
126 ) -> Self {
127 Self::new(
128 start_color,
129 start_alpha,
130 end_color,
131 end_alpha,
132 GradientDirection::Horizontal,
133 )
134 }
135
136 pub fn sample(&self, t: u8) -> (Rgb565, u8) {
137 let color = lerp_rgb565_public(self.start_color, self.end_color, t);
138 let alpha = lerp_u8(self.start_alpha, self.end_alpha, t);
139 (color, alpha)
140 }
141}
142
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct AlphaRadialGradient {
145 pub center_x: f32,
146 pub center_y: f32,
147 pub radius: f32,
148 pub start_color: Rgb565,
149 pub start_alpha: u8,
150 pub end_color: Rgb565,
151 pub end_alpha: u8,
152}
153
154impl AlphaRadialGradient {
155 pub fn new(
156 center_x: f32,
157 center_y: f32,
158 radius: f32,
159 start_color: Rgb565,
160 start_alpha: u8,
161 end_color: Rgb565,
162 end_alpha: u8,
163 ) -> Self {
164 Self {
165 center_x,
166 center_y,
167 radius: if radius <= 0.0 { 1.0 } else { radius },
168 start_color,
169 start_alpha,
170 end_color,
171 end_alpha,
172 }
173 }
174
175 pub fn sample_at_dist(&self, dist: f32) -> (Rgb565, u8) {
176 let t = (dist / self.radius).clamp(0.0, 1.0);
177 let t_u8 = (t * 255.0) as u8;
178 let color = lerp_rgb565_public(self.start_color, self.end_color, t_u8);
179 let alpha = lerp_u8(self.start_alpha, self.end_alpha, t_u8);
180 (color, alpha)
181 }
182}
183
184#[inline]
185pub fn lerp_u8(a: u8, b: u8, t: u8) -> u8 {
186 let t = t as u32;
187 let inv = 255u32 - t;
188 (((a as u32 * inv) + (b as u32 * t)) / 255) as u8
189}
190
191#[inline]
192pub fn lerp_rgb565_public(a: Rgb565, b: Rgb565, t: u8) -> Rgb565 {
193 let t = t as u16;
194 let inv = 255u16.saturating_sub(t);
195 let r = ((a.r() as u16 * inv) + (b.r() as u16 * t)) / 255;
196 let g = ((a.g() as u16 * inv) + (b.g() as u16 * t)) / 255;
197 let bb = ((a.b() as u16 * inv) + (b.b() as u16 * t)) / 255;
198 Rgb565::new(r as u8, g as u8, bb as u8)
199}
200
201#[derive(Clone, Copy, Debug, PartialEq, Eq)]
202pub struct Style {
203 pub background: Option<Rgb565>,
204 pub gradient: Option<LinearGradient>,
205 pub font: FontId,
206 pub foreground: Rgb565,
207 pub text: Rgb565,
208 pub accent: Rgb565,
209 pub opacity: u8,
210 pub corner_radius: u8,
211 pub shadow: Option<Shadow>,
212 pub border: Border,
213 pub padding: EdgeInsets,
214}
215
216impl Style {
217 pub const fn new() -> Self {
218 Self {
219 background: None,
220 gradient: None,
221 font: FontId::Tiny3x5,
222 foreground: Rgb565::WHITE,
223 text: Rgb565::WHITE,
224 accent: Rgb565::new(0, 42, 31),
225 opacity: 255,
226 corner_radius: 0,
227 shadow: Shadow::none(),
228 border: Border::none(),
229 padding: EdgeInsets::all(0),
230 }
231 }
232
233 pub const fn panel() -> Self {
234 Self {
235 background: Some(Rgb565::new(2, 4, 8)),
236 gradient: Some(LinearGradient::vertical(
237 Rgb565::new(4, 8, 12),
238 Rgb565::new(1, 2, 5),
239 )),
240 font: FontId::Tiny3x5,
241 foreground: Rgb565::WHITE,
242 text: Rgb565::WHITE,
243 accent: Rgb565::new(0, 42, 31),
244 opacity: 255,
245 corner_radius: 2,
246 shadow: Some(Shadow::soft()),
247 border: Border::one(Rgb565::new(8, 16, 20)),
248 padding: EdgeInsets::all(2),
249 }
250 }
251
252 pub const fn label() -> Self {
253 Self {
254 background: None,
255 gradient: None,
256 font: FontId::Tiny3x5,
257 foreground: Rgb565::WHITE,
258 text: Rgb565::WHITE,
259 accent: Rgb565::new(0, 42, 31),
260 opacity: 255,
261 corner_radius: 0,
262 shadow: Shadow::none(),
263 border: Border::none(),
264 padding: EdgeInsets::all(0),
265 }
266 }
267
268 pub const fn button() -> Self {
269 Self {
270 background: Some(Rgb565::new(4, 8, 12)),
271 gradient: Some(LinearGradient::vertical(
272 Rgb565::new(6, 12, 16),
273 Rgb565::new(2, 4, 8),
274 )),
275 font: FontId::Medium4x7,
276 foreground: Rgb565::WHITE,
277 text: Rgb565::WHITE,
278 accent: Rgb565::new(0, 48, 40),
279 opacity: 255,
280 corner_radius: 2,
281 shadow: Some(Shadow {
282 color: Rgb565::BLACK,
283 opacity: 88,
284 offset_x: 1,
285 offset_y: 1,
286 spread: 1,
287 }),
288 border: Border::one(Rgb565::new(12, 24, 28)),
289 padding: EdgeInsets::symmetric(3, 2),
290 }
291 }
292
293 pub const fn progress() -> Self {
294 Self {
295 background: Some(Rgb565::new(3, 4, 5)),
296 gradient: Some(LinearGradient::horizontal(
297 Rgb565::new(3, 5, 6),
298 Rgb565::new(1, 2, 3),
299 )),
300 font: FontId::Tiny3x5,
301 foreground: Rgb565::new(0, 50, 18),
302 text: Rgb565::WHITE,
303 accent: Rgb565::new(0, 50, 18),
304 opacity: 255,
305 corner_radius: 1,
306 shadow: Shadow::none(),
307 border: Border::one(Rgb565::new(9, 14, 14)),
308 padding: EdgeInsets::all(1),
309 }
310 }
311
312 pub const fn selected(mut self, selected: bool) -> Self {
313 if selected {
314 self.background = Some(self.accent);
315 self.border = Border::one(Rgb565::WHITE);
316 }
317 self
318 }
319
320 pub const fn with_font_id(mut self, font: FontId) -> Self {
321 self.font = font;
322 self
323 }
324
325 pub fn with_font(mut self, font: impl Into<FontId>) -> Self {
326 self.font = font.into();
327 self
328 }
329}
330
331impl Default for Style {
332 fn default() -> Self {
333 Self::new()
334 }
335}
336
337#[derive(Clone, Copy, Debug, PartialEq, Eq)]
338pub struct StateStyle {
339 pub style: Style,
340}
341
342impl StateStyle {
343 pub const fn new(style: Style) -> Self {
344 Self { style }
345 }
346}
347
348#[derive(Clone, Copy, Debug, PartialEq, Eq)]
349pub struct WidgetStyle {
350 pub normal: Style,
351 pub focused: Style,
352 pub pressed: Style,
353 pub disabled: Style,
354}
355
356impl WidgetStyle {
357 pub const fn new(normal: Style) -> Self {
358 Self {
359 normal,
360 focused: normal.selected(true),
361 pressed: normal.selected(true),
362 disabled: Style {
363 background: normal.background,
364 gradient: normal.gradient,
365 font: normal.font,
366 foreground: Rgb565::new(8, 12, 12),
367 text: Rgb565::new(12, 18, 18),
368 accent: normal.accent,
369 opacity: 170,
370 corner_radius: normal.corner_radius,
371 shadow: normal.shadow,
372 border: normal.border,
373 padding: normal.padding,
374 },
375 }
376 }
377
378 pub const fn with_focused(mut self, focused: Style) -> Self {
379 self.focused = focused;
380 self
381 }
382
383 pub const fn with_pressed(mut self, pressed: Style) -> Self {
384 self.pressed = pressed;
385 self
386 }
387
388 pub const fn with_disabled(mut self, disabled: Style) -> Self {
389 self.disabled = disabled;
390 self
391 }
392
393 pub const fn resolve(self, state: VisualState) -> Style {
394 match state {
395 VisualState::Normal => self.normal,
396 VisualState::Focused => self.focused,
397 VisualState::Pressed => self.pressed,
398 VisualState::Disabled => self.disabled,
399 }
400 }
401
402 pub const fn with_state_override(mut self, state: VisualState, style: Style) -> Self {
403 match state {
404 VisualState::Normal => self.normal = style,
405 VisualState::Focused => self.focused = style,
406 VisualState::Pressed => self.pressed = style,
407 VisualState::Disabled => self.disabled = style,
408 }
409 self
410 }
411
412 pub fn resolve_interpolated(self, from: VisualState, to: VisualState, t: f32) -> Style {
413 let a = self.resolve(from);
414 let b = self.resolve(to);
415 lerp_style(a, b, t)
416 }
417}
418
419impl From<Style> for WidgetStyle {
420 fn from(style: Style) -> Self {
421 Self::new(style)
422 }
423}
424
425impl From<StateStyle> for WidgetStyle {
426 fn from(style: StateStyle) -> Self {
427 Self::new(style.style)
428 }
429}
430
431#[derive(Clone, Copy, Debug, PartialEq, Eq)]
432pub struct Theme {
433 pub panel: Style,
434 pub label: Style,
435 pub button: Style,
436 pub progress: Style,
437 pub toggle: Style,
438 pub checkbox: Style,
439 pub slider: Style,
440 pub value_label: Style,
441 pub icon_button: Style,
442 pub list: Style,
443 pub dialog: Style,
444 pub toast: Style,
445 pub tabs: Style,
446 pub meter: Style,
447 pub focus_ring: Rgb565,
448}
449
450impl Theme {
451 pub const fn dark() -> Self {
452 Self {
453 panel: Style::panel(),
454 label: Style::label(),
455 button: Style::button(),
456 progress: Style::progress(),
457 toggle: Style::button(),
458 checkbox: Style::button(),
459 slider: Style::button(),
460 value_label: Style::panel(),
461 icon_button: Style::button(),
462 list: Style::button(),
463 dialog: Style {
464 background: Some(Rgb565::new(5, 8, 14)),
465 gradient: Some(LinearGradient::vertical(
466 Rgb565::new(7, 12, 18),
467 Rgb565::new(2, 4, 8),
468 )),
469 font: FontId::Scaled6x10,
470 foreground: Rgb565::WHITE,
471 text: Rgb565::WHITE,
472 accent: Rgb565::new(31, 44, 0),
473 opacity: 255,
474 corner_radius: 3,
475 shadow: Some(Shadow {
476 color: Rgb565::BLACK,
477 opacity: 120,
478 offset_x: 2,
479 offset_y: 2,
480 spread: 3,
481 }),
482 border: Border::one(Rgb565::WHITE),
483 padding: EdgeInsets::all(4),
484 },
485 toast: Style {
486 background: Some(Rgb565::new(8, 10, 2)),
487 gradient: Some(LinearGradient::vertical(
488 Rgb565::new(10, 14, 4),
489 Rgb565::new(5, 6, 1),
490 )),
491 font: FontId::Medium4x7,
492 foreground: Rgb565::WHITE,
493 text: Rgb565::WHITE,
494 accent: Rgb565::new(31, 48, 0),
495 opacity: 255,
496 corner_radius: 2,
497 shadow: Some(Shadow {
498 color: Rgb565::BLACK,
499 opacity: 72,
500 offset_x: 1,
501 offset_y: 1,
502 spread: 1,
503 }),
504 border: Border::one(Rgb565::new(18, 22, 6)),
505 padding: EdgeInsets::symmetric(4, 2),
506 },
507 tabs: Style::button(),
508 meter: Style::progress(),
509 focus_ring: Rgb565::new(31, 56, 0),
510 }
511 }
512}
513
514impl Default for Theme {
515 fn default() -> Self {
516 Self::dark()
517 }
518}
519
520pub fn lerp_style(a: Style, b: Style, t: f32) -> Style {
521 let t = t.clamp(0.0, 1.0);
522 let blend = |c1: Rgb565, c2: Rgb565| {
523 let lerp = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
524 Rgb565::new(
525 lerp(c1.r(), c2.r()),
526 lerp(c1.g(), c2.g()),
527 lerp(c1.b(), c2.b()),
528 )
529 };
530 Style {
531 background: Some(blend(
532 a.background.unwrap_or(Rgb565::BLACK),
533 b.background.unwrap_or(Rgb565::BLACK),
534 )),
535 gradient: a.gradient.or(b.gradient),
536 font: a.font,
537 foreground: blend(a.foreground, b.foreground),
538 text: blend(a.text, b.text),
539 accent: blend(a.accent, b.accent),
540 opacity: (a.opacity as f32 + (b.opacity as f32 - a.opacity as f32) * t) as u8,
541 corner_radius: (a.corner_radius as f32
542 + (b.corner_radius as f32 - a.corner_radius as f32) * t) as u8,
543 shadow: a.shadow.or(b.shadow),
544 border: Border {
545 color: blend(a.border.color, b.border.color),
546 width: (a.border.width as f32 + (b.border.width as f32 - a.border.width as f32) * t)
547 as u8,
548 },
549 padding: a.padding,
550 }
551}
552
553pub fn lerp_theme(a: Theme, b: Theme, t: f32) -> Theme {
554 let t = t.clamp(0.0, 1.0);
555 let blend_color = |c1: Rgb565, c2: Rgb565| {
556 let lerp = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
557 Rgb565::new(
558 lerp(c1.r(), c2.r()),
559 lerp(c1.g(), c2.g()),
560 lerp(c1.b(), c2.b()),
561 )
562 };
563 Theme {
564 panel: lerp_style(a.panel, b.panel, t),
565 label: lerp_style(a.label, b.label, t),
566 button: lerp_style(a.button, b.button, t),
567 progress: lerp_style(a.progress, b.progress, t),
568 toggle: lerp_style(a.toggle, b.toggle, t),
569 checkbox: lerp_style(a.checkbox, b.checkbox, t),
570 slider: lerp_style(a.slider, b.slider, t),
571 value_label: lerp_style(a.value_label, b.value_label, t),
572 icon_button: lerp_style(a.icon_button, b.icon_button, t),
573 list: lerp_style(a.list, b.list, t),
574 dialog: lerp_style(a.dialog, b.dialog, t),
575 toast: lerp_style(a.toast, b.toast, t),
576 tabs: lerp_style(a.tabs, b.tabs, t),
577 meter: lerp_style(a.meter, b.meter, t),
578 focus_ring: blend_color(a.focus_ring, b.focus_ring),
579 }
580}
581
582#[derive(Clone, Copy, Debug, PartialEq)]
583pub struct StyleTransition {
584 pub from: VisualState,
585 pub to: VisualState,
586 pub animation: crate::Animation,
587}
588
589impl StyleTransition {
590 pub const fn new(
591 from: VisualState,
592 to: VisualState,
593 duration_ms: u32,
594 easing: crate::Easing,
595 ) -> Self {
596 Self {
597 from,
598 to,
599 animation: crate::Animation::new(0.0, 1.0, duration_ms, easing),
600 }
601 }
602
603 pub fn tick(&mut self, dt_ms: u32) {
604 self.animation.tick(dt_ms);
605 }
606
607 pub fn style(&self, styles: WidgetStyle) -> Style {
608 styles.resolve_interpolated(self.from, self.to, self.animation.value())
609 }
610}
611
612#[derive(Clone, Copy, Debug, PartialEq, Eq)]
613pub enum VisualState {
614 Normal,
615 Focused,
616 Pressed,
617 Disabled,
618}
619
620#[cfg(test)]
621mod tests {
622 use super::*;
623 use embedded_graphics_core::pixelcolor::WebColors;
624
625 #[test]
626 fn test_border_and_shadow_presets() {
627 let none_border = Border::none();
628 assert_eq!(none_border.width, 0);
629
630 let one_border = Border::one(Rgb565::CSS_RED);
631 assert_eq!(one_border.width, 1);
632 assert_eq!(one_border.color, Rgb565::CSS_RED);
633
634 assert_eq!(Shadow::none(), None);
635 let soft_shadow = Shadow::soft();
636 assert_eq!(soft_shadow.opacity, 96);
637 assert_eq!(soft_shadow.offset_x, 1);
638 }
639
640 #[test]
641 fn test_style_resolution_and_state_overrides() {
642 let base_style = Style {
643 background: Some(Rgb565::CSS_BLUE),
644 gradient: None,
645 font: FontId::Tiny3x5,
646 foreground: Rgb565::CSS_WHITE,
647 text: Rgb565::CSS_WHITE,
648 accent: Rgb565::CSS_RED,
649 opacity: 255,
650 corner_radius: 0,
651 shadow: Shadow::none(),
652 border: Border::none(),
653 padding: EdgeInsets::all(4),
654 };
655
656 let focused_style = Style {
657 background: Some(Rgb565::CSS_YELLOW),
658 ..base_style
659 };
660
661 let widget_style =
662 WidgetStyle::new(base_style).with_state_override(VisualState::Focused, focused_style);
663
664 assert_eq!(widget_style.resolve(VisualState::Normal), base_style);
665 assert_eq!(widget_style.resolve(VisualState::Focused), focused_style);
666 assert_eq!(
667 widget_style.resolve(VisualState::Pressed),
668 base_style.selected(true)
669 );
670 }
671
672 #[test]
673 fn test_style_lerp_and_transition() {
674 let s1 = Style {
675 background: Some(Rgb565::new(0, 0, 0)),
676 gradient: None,
677 font: FontId::Tiny3x5,
678 foreground: Rgb565::new(0, 0, 0),
679 text: Rgb565::new(0, 0, 0),
680 accent: Rgb565::new(0, 0, 0),
681 opacity: 0,
682 corner_radius: 0,
683 shadow: Shadow::none(),
684 border: Border::none(),
685 padding: EdgeInsets::all(0),
686 };
687
688 let s2 = Style {
689 background: Some(Rgb565::new(31, 63, 31)),
690 gradient: None,
691 font: FontId::Tiny3x5,
692 foreground: Rgb565::new(31, 63, 31),
693 text: Rgb565::new(31, 63, 31),
694 accent: Rgb565::new(31, 63, 31),
695 opacity: 255,
696 corner_radius: 8,
697 shadow: Shadow::none(),
698 border: Border::one(Rgb565::new(31, 63, 31)),
699 padding: EdgeInsets::all(10),
700 };
701
702 let mid = lerp_style(s1, s2, 0.5);
703 assert!((mid.background.unwrap().r() as i32 - 15).abs() <= 1);
704 assert_eq!(mid.corner_radius, 4);
705
706 let widget_style = WidgetStyle::new(s1).with_state_override(VisualState::Focused, s2);
707
708 let mut transition = StyleTransition::new(
709 VisualState::Normal,
710 VisualState::Focused,
711 100,
712 crate::Easing::Linear,
713 );
714
715 transition.tick(50);
716 let current_style = transition.style(widget_style);
717 assert_eq!(current_style.corner_radius, 4);
718 }
719}