1use crate::style::Color;
6use rlvgl_core::style::Style;
7use rlvgl_core::widget::Rect;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub enum ColorScheme {
12 #[default]
14 Primary,
15 Neutral,
17 Info,
19 Success,
21 Warning,
23 Danger,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
29pub enum Variant {
30 #[default]
32 Solid,
33 Subtle,
35 Outline,
37 Ghost,
39}
40
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
43pub enum ComponentSize {
44 Xs,
46 Sm,
48 #[default]
50 Md,
51 Lg,
53}
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub struct SchemeColors {
58 pub solid: Color,
60 pub contrast: Color,
62 pub subtle: Color,
64 pub muted: Color,
66 pub border: Color,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct ComponentSizeTokens {
73 pub min_height: i32,
75 pub gap: i32,
77 pub radius: u8,
79 pub border_width: u8,
81}
82
83impl ComponentSizeTokens {
84 pub const fn rect(self, x: i32, y: i32, width: i32) -> Rect {
86 Rect {
87 x,
88 y,
89 width,
90 height: self.min_height,
91 }
92 }
93
94 pub const fn origin_rect(self, width: i32) -> Rect {
96 self.rect(0, 0, width)
97 }
98}
99
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102pub struct ComponentStyle {
103 pub style: Style,
105 pub text_color: Color,
107 pub accent_color: Color,
109 pub size: ComponentSizeTokens,
111}
112
113#[derive(Debug, Clone, Copy)]
115pub struct Spacing {
116 pub xs: u8,
118 pub sm: u8,
120 pub md: u8,
122 pub lg: u8,
124 pub xl: u8,
126}
127
128impl Default for Spacing {
129 fn default() -> Self {
130 Self {
131 xs: 2,
132 sm: 4,
133 md: 8,
134 lg: 16,
135 xl: 24,
136 }
137 }
138}
139
140#[derive(Debug, Clone, Copy)]
142pub struct Radii {
143 pub none: u8,
145 pub sm: u8,
147 pub md: u8,
149 pub lg: u8,
151 pub full: u8,
153}
154
155impl Default for Radii {
156 fn default() -> Self {
157 Self {
158 none: 0,
159 sm: 2,
160 md: 4,
161 lg: 8,
162 full: 255,
163 }
164 }
165}
166
167#[derive(Debug, Clone, Copy)]
169pub struct Colors {
170 pub primary: Color,
172 pub background: Color,
174 pub text: Color,
176}
177
178impl Default for Colors {
179 fn default() -> Self {
180 Self {
181 primary: Color(98, 0, 238, 255),
182 background: Color(255, 255, 255, 255),
183 text: Color(0, 0, 0, 255),
184 }
185 }
186}
187
188#[derive(Debug, Clone, Copy)]
190pub struct Fonts {
191 pub small: &'static str,
193 pub body: &'static str,
195 pub heading: &'static str,
197}
198
199impl Default for Fonts {
200 fn default() -> Self {
201 Self {
202 small: "tiny",
203 body: "default",
204 heading: "bold",
205 }
206 }
207}
208
209#[derive(Debug, Clone, Copy, Default)]
211pub struct Tokens {
212 pub spacing: Spacing,
214 pub colors: Colors,
216 pub radii: Radii,
218 pub fonts: Fonts,
220}
221
222#[derive(Debug, Clone, Copy)]
224pub struct Theme {
225 pub tokens: Tokens,
227}
228
229impl Theme {
230 pub fn material_light() -> Self {
232 Self {
233 tokens: Tokens::default(),
234 }
235 }
236
237 pub fn scheme(&self, scheme: ColorScheme) -> SchemeColors {
239 let solid = match scheme {
240 ColorScheme::Primary => self.tokens.colors.primary,
241 ColorScheme::Neutral => Color(86, 94, 104, 255),
242 ColorScheme::Info => Color(0, 122, 255, 255),
243 ColorScheme::Success => Color(24, 150, 90, 255),
244 ColorScheme::Warning => Color(230, 162, 0, 255),
245 ColorScheme::Danger => Color(210, 60, 60, 255),
246 };
247 let contrast = match scheme {
248 ColorScheme::Warning => Color(25, 25, 25, 255),
249 _ => Color(255, 255, 255, 255),
250 };
251 let bg = self.tokens.colors.background;
252 SchemeColors {
253 solid,
254 contrast,
255 subtle: solid.lerp(bg, 4, 5),
256 muted: solid.lerp(bg, 3, 5),
257 border: solid.lerp(bg, 1, 2),
258 }
259 }
260
261 pub fn component_size(&self, size: ComponentSize) -> ComponentSizeTokens {
263 match size {
264 ComponentSize::Xs => ComponentSizeTokens {
265 min_height: 14,
266 gap: i32::from(self.tokens.spacing.xs),
267 radius: self.tokens.radii.sm,
268 border_width: 1,
269 },
270 ComponentSize::Sm => ComponentSizeTokens {
271 min_height: 18,
272 gap: i32::from(self.tokens.spacing.sm),
273 radius: self.tokens.radii.sm,
274 border_width: 1,
275 },
276 ComponentSize::Md => ComponentSizeTokens {
277 min_height: 24,
278 gap: i32::from(self.tokens.spacing.md),
279 radius: self.tokens.radii.md,
280 border_width: 1,
281 },
282 ComponentSize::Lg => ComponentSizeTokens {
283 min_height: 32,
284 gap: i32::from(self.tokens.spacing.lg),
285 radius: self.tokens.radii.lg,
286 border_width: 2,
287 },
288 }
289 }
290
291 pub fn control_rect(&self, x: i32, y: i32, width: i32, size: ComponentSize) -> Rect {
293 self.component_size(size).rect(x, y, width)
294 }
295
296 pub fn origin_control_rect(&self, width: i32, size: ComponentSize) -> Rect {
298 self.component_size(size).origin_rect(width)
299 }
300
301 pub fn component_style(
303 &self,
304 scheme: ColorScheme,
305 variant: Variant,
306 size: ComponentSize,
307 ) -> ComponentStyle {
308 let colors = self.scheme(scheme);
309 let size_tokens = self.component_size(size);
310 let transparent_bg = transparent(self.tokens.colors.background);
311 let transparent_border = transparent(colors.solid);
312 let (bg_color, border_color, border_width, text_color, accent_color) = match variant {
313 Variant::Solid => (colors.solid, colors.solid, 0, colors.contrast, colors.solid),
314 Variant::Subtle => (colors.subtle, colors.border, 0, colors.solid, colors.solid),
315 Variant::Outline => (
316 transparent_bg,
317 colors.solid,
318 size_tokens.border_width,
319 colors.solid,
320 colors.solid,
321 ),
322 Variant::Ghost => (
323 transparent_bg,
324 transparent_border,
325 0,
326 colors.solid,
327 colors.solid,
328 ),
329 };
330
331 ComponentStyle {
332 style: Style {
333 bg_color,
334 border_color,
335 border_width,
336 alpha: 255,
337 radius: size_tokens.radius,
338 },
339 text_color,
340 accent_color,
341 size: size_tokens,
342 }
343 }
344
345 #[deprecated(
354 since = "0.2.2",
355 note = "use LparTheme::apply_to_node; see LPAR-07 §9.4"
356 )]
357 pub fn apply_global(&self) {
358 }
360}
361
362fn transparent(color: Color) -> Color {
363 Color(color.0, color.1, color.2, 0)
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn component_style_resolves_solid_variant() {
372 let theme = Theme::material_light();
373 let style = theme.component_style(ColorScheme::Primary, Variant::Solid, ComponentSize::Md);
374
375 assert_eq!(style.style.bg_color, theme.tokens.colors.primary);
376 assert_eq!(style.style.border_width, 0);
377 assert_eq!(style.text_color, Color(255, 255, 255, 255));
378 assert_eq!(style.size.min_height, 24);
379 }
380
381 #[test]
382 fn component_style_resolves_outline_variant() {
383 let theme = Theme::material_light();
384 let style = theme.component_style(ColorScheme::Danger, Variant::Outline, ComponentSize::Lg);
385
386 assert_eq!(style.style.bg_color.3, 0);
387 assert_eq!(style.style.border_width, 2);
388 assert_eq!(style.text_color, style.accent_color);
389 assert_eq!(style.size.radius, theme.tokens.radii.lg);
390 }
391
392 #[test]
393 fn component_size_tokens_create_rects() {
394 let theme = Theme::material_light();
395 let size = theme.component_size(ComponentSize::Sm);
396
397 assert_eq!(
398 size.rect(1, 2, 30),
399 Rect {
400 x: 1,
401 y: 2,
402 width: 30,
403 height: 18
404 }
405 );
406 assert_eq!(size.origin_rect(40).height, 18);
407 }
408
409 #[test]
410 fn theme_creates_control_rects_from_sizes() {
411 let theme = Theme::material_light();
412
413 assert_eq!(
414 theme.control_rect(4, 5, 60, ComponentSize::Lg),
415 Rect {
416 x: 4,
417 y: 5,
418 width: 60,
419 height: 32
420 }
421 );
422 assert_eq!(theme.origin_control_rect(60, ComponentSize::Xs).height, 14);
423 }
424}