Skip to main content

embedded_gui/
style.rs

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
321impl Default for Style {
322    fn default() -> Self {
323        Self::new()
324    }
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328pub struct StateStyle {
329    pub style: Style,
330}
331
332impl StateStyle {
333    pub const fn new(style: Style) -> Self {
334        Self { style }
335    }
336}
337
338#[derive(Clone, Copy, Debug, PartialEq, Eq)]
339pub struct WidgetStyle {
340    pub normal: Style,
341    pub focused: Style,
342    pub pressed: Style,
343    pub disabled: Style,
344}
345
346impl WidgetStyle {
347    pub const fn new(normal: Style) -> Self {
348        Self {
349            normal,
350            focused: normal.selected(true),
351            pressed: normal.selected(true),
352            disabled: Style {
353                background: normal.background,
354                gradient: normal.gradient,
355                font: normal.font,
356                foreground: Rgb565::new(8, 12, 12),
357                text: Rgb565::new(12, 18, 18),
358                accent: normal.accent,
359                opacity: 170,
360                corner_radius: normal.corner_radius,
361                shadow: normal.shadow,
362                border: normal.border,
363                padding: normal.padding,
364            },
365        }
366    }
367
368    pub const fn with_focused(mut self, focused: Style) -> Self {
369        self.focused = focused;
370        self
371    }
372
373    pub const fn with_pressed(mut self, pressed: Style) -> Self {
374        self.pressed = pressed;
375        self
376    }
377
378    pub const fn with_disabled(mut self, disabled: Style) -> Self {
379        self.disabled = disabled;
380        self
381    }
382
383    pub const fn resolve(self, state: VisualState) -> Style {
384        match state {
385            VisualState::Normal => self.normal,
386            VisualState::Focused => self.focused,
387            VisualState::Pressed => self.pressed,
388            VisualState::Disabled => self.disabled,
389        }
390    }
391
392    pub const fn with_state_override(mut self, state: VisualState, style: Style) -> Self {
393        match state {
394            VisualState::Normal => self.normal = style,
395            VisualState::Focused => self.focused = style,
396            VisualState::Pressed => self.pressed = style,
397            VisualState::Disabled => self.disabled = style,
398        }
399        self
400    }
401
402    pub fn resolve_interpolated(self, from: VisualState, to: VisualState, t: f32) -> Style {
403        let a = self.resolve(from);
404        let b = self.resolve(to);
405        lerp_style(a, b, t)
406    }
407}
408
409impl From<Style> for WidgetStyle {
410    fn from(style: Style) -> Self {
411        Self::new(style)
412    }
413}
414
415impl From<StateStyle> for WidgetStyle {
416    fn from(style: StateStyle) -> Self {
417        Self::new(style.style)
418    }
419}
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq)]
422pub struct Theme {
423    pub panel: Style,
424    pub label: Style,
425    pub button: Style,
426    pub progress: Style,
427    pub toggle: Style,
428    pub checkbox: Style,
429    pub slider: Style,
430    pub value_label: Style,
431    pub icon_button: Style,
432    pub list: Style,
433    pub dialog: Style,
434    pub toast: Style,
435    pub tabs: Style,
436    pub meter: Style,
437    pub focus_ring: Rgb565,
438}
439
440impl Theme {
441    pub const fn dark() -> Self {
442        Self {
443            panel: Style::panel(),
444            label: Style::label(),
445            button: Style::button(),
446            progress: Style::progress(),
447            toggle: Style::button(),
448            checkbox: Style::button(),
449            slider: Style::button(),
450            value_label: Style::panel(),
451            icon_button: Style::button(),
452            list: Style::button(),
453            dialog: Style {
454                background: Some(Rgb565::new(5, 8, 14)),
455                gradient: Some(LinearGradient::vertical(
456                    Rgb565::new(7, 12, 18),
457                    Rgb565::new(2, 4, 8),
458                )),
459                font: FontId::Scaled6x10,
460                foreground: Rgb565::WHITE,
461                text: Rgb565::WHITE,
462                accent: Rgb565::new(31, 44, 0),
463                opacity: 255,
464                corner_radius: 3,
465                shadow: Some(Shadow {
466                    color: Rgb565::BLACK,
467                    opacity: 120,
468                    offset_x: 2,
469                    offset_y: 2,
470                    spread: 3,
471                }),
472                border: Border::one(Rgb565::WHITE),
473                padding: EdgeInsets::all(4),
474            },
475            toast: Style {
476                background: Some(Rgb565::new(8, 10, 2)),
477                gradient: Some(LinearGradient::vertical(
478                    Rgb565::new(10, 14, 4),
479                    Rgb565::new(5, 6, 1),
480                )),
481                font: FontId::Medium4x7,
482                foreground: Rgb565::WHITE,
483                text: Rgb565::WHITE,
484                accent: Rgb565::new(31, 48, 0),
485                opacity: 255,
486                corner_radius: 2,
487                shadow: Some(Shadow {
488                    color: Rgb565::BLACK,
489                    opacity: 72,
490                    offset_x: 1,
491                    offset_y: 1,
492                    spread: 1,
493                }),
494                border: Border::one(Rgb565::new(18, 22, 6)),
495                padding: EdgeInsets::symmetric(4, 2),
496            },
497            tabs: Style::button(),
498            meter: Style::progress(),
499            focus_ring: Rgb565::new(31, 56, 0),
500        }
501    }
502}
503
504impl Default for Theme {
505    fn default() -> Self {
506        Self::dark()
507    }
508}
509
510pub fn lerp_style(a: Style, b: Style, t: f32) -> Style {
511    let t = t.clamp(0.0, 1.0);
512    let blend = |c1: Rgb565, c2: Rgb565| {
513        let lerp = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
514        Rgb565::new(
515            lerp(c1.r(), c2.r()),
516            lerp(c1.g(), c2.g()),
517            lerp(c1.b(), c2.b()),
518        )
519    };
520    Style {
521        background: Some(blend(
522            a.background.unwrap_or(Rgb565::BLACK),
523            b.background.unwrap_or(Rgb565::BLACK),
524        )),
525        gradient: a.gradient.or(b.gradient),
526        font: a.font,
527        foreground: blend(a.foreground, b.foreground),
528        text: blend(a.text, b.text),
529        accent: blend(a.accent, b.accent),
530        opacity: (a.opacity as f32 + (b.opacity as f32 - a.opacity as f32) * t) as u8,
531        corner_radius: (a.corner_radius as f32
532            + (b.corner_radius as f32 - a.corner_radius as f32) * t) as u8,
533        shadow: a.shadow.or(b.shadow),
534        border: Border {
535            color: blend(a.border.color, b.border.color),
536            width: (a.border.width as f32 + (b.border.width as f32 - a.border.width as f32) * t)
537                as u8,
538        },
539        padding: a.padding,
540    }
541}
542
543#[derive(Clone, Copy, Debug, PartialEq)]
544pub struct StyleTransition {
545    pub from: VisualState,
546    pub to: VisualState,
547    pub animation: crate::Animation,
548}
549
550impl StyleTransition {
551    pub const fn new(
552        from: VisualState,
553        to: VisualState,
554        duration_ms: u32,
555        easing: crate::Easing,
556    ) -> Self {
557        Self {
558            from,
559            to,
560            animation: crate::Animation::new(0.0, 1.0, duration_ms, easing),
561        }
562    }
563
564    pub fn tick(&mut self, dt_ms: u32) {
565        self.animation.tick(dt_ms);
566    }
567
568    pub fn style(&self, styles: WidgetStyle) -> Style {
569        styles.resolve_interpolated(self.from, self.to, self.animation.value())
570    }
571}
572
573#[derive(Clone, Copy, Debug, PartialEq, Eq)]
574pub enum VisualState {
575    Normal,
576    Focused,
577    Pressed,
578    Disabled,
579}