Skip to main content

rlvgl_ui/
direct_props.rs

1// SPDX-License-Identifier: MIT
2//! Prop extensions for directly re-exported LVGL-parity widgets.
3//!
4//! Some mature widgets are re-exported from `rlvgl-widgets` rather than wrapped
5//! in new `rlvgl-ui` types. This module adds local fluent props for those
6//! widgets without introducing conflicting wrapper names.
7
8use rlvgl_widgets::{
9    calendar::Calendar, message_box::MessageBox, spinbox::Spinbox, table::Table, window::Window,
10};
11
12use crate::{
13    style::Color,
14    theme::{ColorScheme, ComponentSize, ComponentStyle, SchemeColors, Theme, Variant},
15};
16
17/// Fluent themed part colors for direct LVGL-parity widget re-exports.
18///
19/// Direct widgets already support generic [`ThemeProps`](crate::ThemeProps)
20/// because their main `style` fields are public. Use `themed_parts` when the
21/// widget also has public colors for selected rows, headers, cursors, or
22/// header buttons.
23pub trait ThemedPartsProps: Sized {
24    /// Apply the theme to the main style and direct widget-specific part colors.
25    fn themed_parts(
26        self,
27        theme: &Theme,
28        scheme: ColorScheme,
29        variant: Variant,
30        size: ComponentSize,
31    ) -> Self;
32}
33
34impl ThemedPartsProps for Calendar {
35    fn themed_parts(
36        mut self,
37        theme: &Theme,
38        scheme: ColorScheme,
39        variant: Variant,
40        size: ComponentSize,
41    ) -> Self {
42        let resolved = theme.component_style(scheme, variant, size);
43        let colors = theme.scheme(scheme);
44        self.style = resolved.style;
45        self.day_cell_color = panel_color(resolved, colors);
46        self.selected_color = resolved.accent_color;
47        self.today_color = theme.scheme(ColorScheme::Warning).solid;
48        self.text_color = resolved.text_color;
49        self.header_text_color = colors.solid;
50        self.overflow_text_color = colors.border;
51        self
52    }
53}
54
55impl ThemedPartsProps for MessageBox {
56    fn themed_parts(
57        mut self,
58        theme: &Theme,
59        scheme: ColorScheme,
60        variant: Variant,
61        size: ComponentSize,
62    ) -> Self {
63        let resolved = theme.component_style(scheme, variant, size);
64        self.style = resolved.style;
65        self.text_color = resolved.text_color;
66        self.button_text_color = resolved.accent_color;
67        self
68    }
69}
70
71impl ThemedPartsProps for Spinbox {
72    fn themed_parts(
73        mut self,
74        theme: &Theme,
75        scheme: ColorScheme,
76        variant: Variant,
77        size: ComponentSize,
78    ) -> Self {
79        let resolved = theme.component_style(scheme, variant, size);
80        self.style = resolved.style;
81        self.text_color = resolved.text_color;
82        self.cursor_color = resolved.accent_color;
83        self
84    }
85}
86
87impl ThemedPartsProps for Table {
88    fn themed_parts(
89        mut self,
90        theme: &Theme,
91        scheme: ColorScheme,
92        variant: Variant,
93        size: ComponentSize,
94    ) -> Self {
95        let resolved = theme.component_style(scheme, variant, size);
96        let colors = theme.scheme(scheme);
97        self.style = resolved.style;
98        self.cell_bg_color = panel_color(resolved, colors);
99        self.selected_color = resolved.accent_color;
100        self.text_color = resolved.text_color;
101        self.grid_color = colors.border;
102        self
103    }
104}
105
106impl ThemedPartsProps for Window {
107    fn themed_parts(
108        mut self,
109        theme: &Theme,
110        scheme: ColorScheme,
111        variant: Variant,
112        size: ComponentSize,
113    ) -> Self {
114        let resolved = theme.component_style(scheme, variant, size);
115        let colors = theme.scheme(scheme);
116        self.style = resolved.style;
117        self.header_color = resolved.accent_color;
118        self.title_color = colors.contrast;
119        self.button_color = colors.muted;
120        self.button_text_color = colors.solid;
121        self
122    }
123}
124
125fn panel_color(resolved: ComponentStyle, colors: SchemeColors) -> Color {
126    if resolved.style.bg_color.3 == 0 {
127        colors.subtle
128    } else {
129        resolved.style.bg_color
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    use super::*;
136    use rlvgl_core::widget::Rect;
137    use rlvgl_widgets::window::DEFAULT_HEADER_HEIGHT;
138
139    #[test]
140    fn table_themed_parts_apply_cell_and_selection_colors() {
141        let theme = Theme::material_light();
142        let table = Table::new(rect(0, 0, 100, 60)).themed_parts(
143            &theme,
144            ColorScheme::Info,
145            Variant::Outline,
146            ComponentSize::Lg,
147        );
148
149        assert_eq!(table.style.border_width, 2);
150        assert_eq!(table.cell_bg_color, theme.scheme(ColorScheme::Info).subtle);
151        assert_eq!(table.selected_color, theme.scheme(ColorScheme::Info).solid);
152        assert_eq!(table.grid_color, theme.scheme(ColorScheme::Info).border);
153    }
154
155    #[test]
156    fn calendar_themed_parts_apply_calendar_specific_colors() {
157        let theme = Theme::material_light();
158        let calendar = Calendar::new(rect(0, 0, 140, 120)).themed_parts(
159            &theme,
160            ColorScheme::Success,
161            Variant::Subtle,
162            ComponentSize::Md,
163        );
164
165        assert_eq!(
166            calendar.selected_color,
167            theme.scheme(ColorScheme::Success).solid
168        );
169        assert_eq!(
170            calendar.today_color,
171            theme.scheme(ColorScheme::Warning).solid
172        );
173        assert_eq!(
174            calendar.header_text_color,
175            theme.scheme(ColorScheme::Success).solid
176        );
177    }
178
179    #[test]
180    fn text_entry_direct_widgets_receive_accent_colors() {
181        let theme = Theme::material_light();
182        let spinbox = Spinbox::new(rect(0, 0, 80, 24)).themed_parts(
183            &theme,
184            ColorScheme::Danger,
185            Variant::Ghost,
186            ComponentSize::Sm,
187        );
188        let message_box = MessageBox::new(rect(0, 0, 120, 80), "Title", "Body", &["OK"])
189            .themed_parts(
190                &theme,
191                ColorScheme::Danger,
192                Variant::Ghost,
193                ComponentSize::Sm,
194            );
195
196        assert_eq!(
197            spinbox.cursor_color,
198            theme.scheme(ColorScheme::Danger).solid
199        );
200        assert_eq!(
201            message_box.button_text_color,
202            theme.scheme(ColorScheme::Danger).solid
203        );
204    }
205
206    #[test]
207    fn window_themed_parts_apply_header_palette() {
208        let theme = Theme::material_light();
209        let window = Window::new(rect(0, 0, 120, 80), DEFAULT_HEADER_HEIGHT).themed_parts(
210            &theme,
211            ColorScheme::Primary,
212            Variant::Solid,
213            ComponentSize::Md,
214        );
215
216        assert_eq!(
217            window.header_color,
218            theme.scheme(ColorScheme::Primary).solid
219        );
220        assert_eq!(
221            window.title_color,
222            theme.scheme(ColorScheme::Primary).contrast
223        );
224        assert_eq!(
225            window.button_color,
226            theme.scheme(ColorScheme::Primary).muted
227        );
228    }
229
230    fn rect(x: i32, y: i32, width: i32, height: i32) -> Rect {
231        Rect {
232            x,
233            y,
234            width,
235            height,
236        }
237    }
238}