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}