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}