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 Style {
82    pub background: Option<Rgb565>,
83    pub gradient: Option<LinearGradient>,
84    pub font: FontId,
85    pub foreground: Rgb565,
86    pub text: Rgb565,
87    pub accent: Rgb565,
88    pub opacity: u8,
89    pub corner_radius: u8,
90    pub shadow: Option<Shadow>,
91    pub border: Border,
92    pub padding: EdgeInsets,
93}
94
95impl Style {
96    pub const fn new() -> Self {
97        Self {
98            background: None,
99            gradient: None,
100            font: FontId::Tiny3x5,
101            foreground: Rgb565::WHITE,
102            text: Rgb565::WHITE,
103            accent: Rgb565::new(0, 42, 31),
104            opacity: 255,
105            corner_radius: 0,
106            shadow: Shadow::none(),
107            border: Border::none(),
108            padding: EdgeInsets::all(0),
109        }
110    }
111
112    pub const fn panel() -> Self {
113        Self {
114            background: Some(Rgb565::new(2, 4, 8)),
115            gradient: Some(LinearGradient::vertical(
116                Rgb565::new(4, 8, 12),
117                Rgb565::new(1, 2, 5),
118            )),
119            font: FontId::Tiny3x5,
120            foreground: Rgb565::WHITE,
121            text: Rgb565::WHITE,
122            accent: Rgb565::new(0, 42, 31),
123            opacity: 255,
124            corner_radius: 2,
125            shadow: Some(Shadow::soft()),
126            border: Border::one(Rgb565::new(8, 16, 20)),
127            padding: EdgeInsets::all(2),
128        }
129    }
130
131    pub const fn label() -> Self {
132        Self {
133            background: None,
134            gradient: None,
135            font: FontId::Tiny3x5,
136            foreground: Rgb565::WHITE,
137            text: Rgb565::WHITE,
138            accent: Rgb565::new(0, 42, 31),
139            opacity: 255,
140            corner_radius: 0,
141            shadow: Shadow::none(),
142            border: Border::none(),
143            padding: EdgeInsets::all(0),
144        }
145    }
146
147    pub const fn button() -> Self {
148        Self {
149            background: Some(Rgb565::new(4, 8, 12)),
150            gradient: Some(LinearGradient::vertical(
151                Rgb565::new(6, 12, 16),
152                Rgb565::new(2, 4, 8),
153            )),
154            font: FontId::Medium4x7,
155            foreground: Rgb565::WHITE,
156            text: Rgb565::WHITE,
157            accent: Rgb565::new(0, 48, 40),
158            opacity: 255,
159            corner_radius: 2,
160            shadow: Some(Shadow {
161                color: Rgb565::BLACK,
162                opacity: 88,
163                offset_x: 1,
164                offset_y: 1,
165                spread: 1,
166            }),
167            border: Border::one(Rgb565::new(12, 24, 28)),
168            padding: EdgeInsets::symmetric(3, 2),
169        }
170    }
171
172    pub const fn progress() -> Self {
173        Self {
174            background: Some(Rgb565::new(3, 4, 5)),
175            gradient: Some(LinearGradient::horizontal(
176                Rgb565::new(3, 5, 6),
177                Rgb565::new(1, 2, 3),
178            )),
179            font: FontId::Tiny3x5,
180            foreground: Rgb565::new(0, 50, 18),
181            text: Rgb565::WHITE,
182            accent: Rgb565::new(0, 50, 18),
183            opacity: 255,
184            corner_radius: 1,
185            shadow: Shadow::none(),
186            border: Border::one(Rgb565::new(9, 14, 14)),
187            padding: EdgeInsets::all(1),
188        }
189    }
190
191    pub const fn selected(mut self, selected: bool) -> Self {
192        if selected {
193            self.background = Some(self.accent);
194            self.border = Border::one(Rgb565::WHITE);
195        }
196        self
197    }
198}
199
200impl Default for Style {
201    fn default() -> Self {
202        Self::new()
203    }
204}
205
206#[derive(Clone, Copy, Debug, PartialEq, Eq)]
207pub struct StateStyle {
208    pub style: Style,
209}
210
211impl StateStyle {
212    pub const fn new(style: Style) -> Self {
213        Self { style }
214    }
215}
216
217#[derive(Clone, Copy, Debug, PartialEq, Eq)]
218pub struct WidgetStyle {
219    pub normal: Style,
220    pub focused: Style,
221    pub pressed: Style,
222    pub disabled: Style,
223}
224
225impl WidgetStyle {
226    pub const fn new(normal: Style) -> Self {
227        Self {
228            normal,
229            focused: normal.selected(true),
230            pressed: normal.selected(true),
231            disabled: Style {
232                background: normal.background,
233                gradient: normal.gradient,
234                font: normal.font,
235                foreground: Rgb565::new(8, 12, 12),
236                text: Rgb565::new(12, 18, 18),
237                accent: normal.accent,
238                opacity: 170,
239                corner_radius: normal.corner_radius,
240                shadow: normal.shadow,
241                border: normal.border,
242                padding: normal.padding,
243            },
244        }
245    }
246
247    pub const fn with_focused(mut self, focused: Style) -> Self {
248        self.focused = focused;
249        self
250    }
251
252    pub const fn with_pressed(mut self, pressed: Style) -> Self {
253        self.pressed = pressed;
254        self
255    }
256
257    pub const fn with_disabled(mut self, disabled: Style) -> Self {
258        self.disabled = disabled;
259        self
260    }
261
262    pub const fn resolve(self, state: VisualState) -> Style {
263        match state {
264            VisualState::Normal => self.normal,
265            VisualState::Focused => self.focused,
266            VisualState::Pressed => self.pressed,
267            VisualState::Disabled => self.disabled,
268        }
269    }
270
271    pub const fn with_state_override(mut self, state: VisualState, style: Style) -> Self {
272        match state {
273            VisualState::Normal => self.normal = style,
274            VisualState::Focused => self.focused = style,
275            VisualState::Pressed => self.pressed = style,
276            VisualState::Disabled => self.disabled = style,
277        }
278        self
279    }
280
281    pub fn resolve_interpolated(self, from: VisualState, to: VisualState, t: f32) -> Style {
282        let a = self.resolve(from);
283        let b = self.resolve(to);
284        lerp_style(a, b, t)
285    }
286}
287
288impl From<Style> for WidgetStyle {
289    fn from(style: Style) -> Self {
290        Self::new(style)
291    }
292}
293
294impl From<StateStyle> for WidgetStyle {
295    fn from(style: StateStyle) -> Self {
296        Self::new(style.style)
297    }
298}
299
300#[derive(Clone, Copy, Debug, PartialEq, Eq)]
301pub struct Theme {
302    pub panel: Style,
303    pub label: Style,
304    pub button: Style,
305    pub progress: Style,
306    pub toggle: Style,
307    pub checkbox: Style,
308    pub slider: Style,
309    pub value_label: Style,
310    pub icon_button: Style,
311    pub list: Style,
312    pub dialog: Style,
313    pub toast: Style,
314    pub tabs: Style,
315    pub meter: Style,
316    pub focus_ring: Rgb565,
317}
318
319impl Theme {
320    pub const fn dark() -> Self {
321        Self {
322            panel: Style::panel(),
323            label: Style::label(),
324            button: Style::button(),
325            progress: Style::progress(),
326            toggle: Style::button(),
327            checkbox: Style::button(),
328            slider: Style::button(),
329            value_label: Style::panel(),
330            icon_button: Style::button(),
331            list: Style::button(),
332            dialog: Style {
333                background: Some(Rgb565::new(5, 8, 14)),
334                gradient: Some(LinearGradient::vertical(
335                    Rgb565::new(7, 12, 18),
336                    Rgb565::new(2, 4, 8),
337                )),
338                font: FontId::Scaled6x10,
339                foreground: Rgb565::WHITE,
340                text: Rgb565::WHITE,
341                accent: Rgb565::new(31, 44, 0),
342                opacity: 255,
343                corner_radius: 3,
344                shadow: Some(Shadow {
345                    color: Rgb565::BLACK,
346                    opacity: 120,
347                    offset_x: 2,
348                    offset_y: 2,
349                    spread: 3,
350                }),
351                border: Border::one(Rgb565::WHITE),
352                padding: EdgeInsets::all(4),
353            },
354            toast: Style {
355                background: Some(Rgb565::new(8, 10, 2)),
356                gradient: Some(LinearGradient::vertical(
357                    Rgb565::new(10, 14, 4),
358                    Rgb565::new(5, 6, 1),
359                )),
360                font: FontId::Medium4x7,
361                foreground: Rgb565::WHITE,
362                text: Rgb565::WHITE,
363                accent: Rgb565::new(31, 48, 0),
364                opacity: 255,
365                corner_radius: 2,
366                shadow: Some(Shadow {
367                    color: Rgb565::BLACK,
368                    opacity: 72,
369                    offset_x: 1,
370                    offset_y: 1,
371                    spread: 1,
372                }),
373                border: Border::one(Rgb565::new(18, 22, 6)),
374                padding: EdgeInsets::symmetric(4, 2),
375            },
376            tabs: Style::button(),
377            meter: Style::progress(),
378            focus_ring: Rgb565::new(31, 56, 0),
379        }
380    }
381}
382
383impl Default for Theme {
384    fn default() -> Self {
385        Self::dark()
386    }
387}
388
389pub fn lerp_style(a: Style, b: Style, t: f32) -> Style {
390    let t = t.clamp(0.0, 1.0);
391    let blend = |c1: Rgb565, c2: Rgb565| {
392        let lerp = |x: u8, y: u8| (x as f32 + (y as f32 - x as f32) * t) as u8;
393        Rgb565::new(
394            lerp(c1.r(), c2.r()),
395            lerp(c1.g(), c2.g()),
396            lerp(c1.b(), c2.b()),
397        )
398    };
399    Style {
400        background: Some(blend(
401            a.background.unwrap_or(Rgb565::BLACK),
402            b.background.unwrap_or(Rgb565::BLACK),
403        )),
404        gradient: a.gradient.or(b.gradient),
405        font: a.font,
406        foreground: blend(a.foreground, b.foreground),
407        text: blend(a.text, b.text),
408        accent: blend(a.accent, b.accent),
409        opacity: (a.opacity as f32 + (b.opacity as f32 - a.opacity as f32) * t) as u8,
410        corner_radius: (a.corner_radius as f32
411            + (b.corner_radius as f32 - a.corner_radius as f32) * t) as u8,
412        shadow: a.shadow.or(b.shadow),
413        border: Border {
414            color: blend(a.border.color, b.border.color),
415            width: (a.border.width as f32 + (b.border.width as f32 - a.border.width as f32) * t)
416                as u8,
417        },
418        padding: a.padding,
419    }
420}
421
422#[derive(Clone, Copy, Debug, PartialEq)]
423pub struct StyleTransition {
424    pub from: VisualState,
425    pub to: VisualState,
426    pub animation: crate::Animation,
427}
428
429impl StyleTransition {
430    pub const fn new(
431        from: VisualState,
432        to: VisualState,
433        duration_ms: u32,
434        easing: crate::Easing,
435    ) -> Self {
436        Self {
437            from,
438            to,
439            animation: crate::Animation::new(0.0, 1.0, duration_ms, easing),
440        }
441    }
442
443    pub fn tick(&mut self, dt_ms: u32) {
444        self.animation.tick(dt_ms);
445    }
446
447    pub fn style(&self, styles: WidgetStyle) -> Style {
448        styles.resolve_interpolated(self.from, self.to, self.animation.value())
449    }
450}
451
452#[derive(Clone, Copy, Debug, PartialEq, Eq)]
453pub enum VisualState {
454    Normal,
455    Focused,
456    Pressed,
457    Disabled,
458}