Skip to main content

rlvgl_ui/
props.rs

1// SPDX-License-Identifier: MIT
2//! Shared Chakra-inspired props for rlvgl-ui components.
3//!
4//! These traits provide fluent application-facing setters while leaving the
5//! actual rendering and event behavior in `rlvgl-core` and `rlvgl-widgets`.
6
7use rlvgl_core::{
8    style::Style,
9    widget::{Color, Rect, Widget},
10};
11
12use crate::theme::{ColorScheme, ComponentSize, Theme, Variant};
13
14/// Fluent style props for widgets that expose a mutable core [`Style`].
15///
16/// The prop names intentionally use Rust `snake_case` while following common
17/// Chakra-style vocabulary such as `bg`, `rounded`, and `opacity`.
18pub trait StyleProps: Sized {
19    /// Return the mutable style used by this widget's main visual part.
20    fn style_mut(&mut self) -> &mut Style;
21
22    /// Replace the complete main style.
23    fn with_style(mut self, style: Style) -> Self {
24        *self.style_mut() = style;
25        self
26    }
27
28    /// Set the background color.
29    fn bg(mut self, color: Color) -> Self {
30        self.style_mut().bg_color = color;
31        self
32    }
33
34    /// Set the background color.
35    fn bg_color(self, color: Color) -> Self {
36        self.bg(color)
37    }
38
39    /// Set the border color.
40    fn border_color(mut self, color: Color) -> Self {
41        self.style_mut().border_color = color;
42        self
43    }
44
45    /// Set the border width in pixels.
46    fn border_width(mut self, width: u8) -> Self {
47        self.style_mut().border_width = width;
48        self
49    }
50
51    /// Set the corner radius in pixels.
52    fn radius(mut self, radius: u8) -> Self {
53        self.style_mut().radius = radius;
54        self
55    }
56
57    /// Set the corner radius in pixels.
58    fn rounded(self, radius: u8) -> Self {
59        self.radius(radius)
60    }
61
62    /// Set widget opacity as `0..=255`.
63    fn opacity(mut self, alpha: u8) -> Self {
64        self.style_mut().alpha = alpha;
65        self
66    }
67
68    /// Set widget opacity as `0..=255`.
69    fn alpha(self, alpha: u8) -> Self {
70        self.opacity(alpha)
71    }
72}
73
74/// Fluent bounds prop for widgets that adopt layout-provided bounds.
75///
76/// The default [`Widget::set_bounds`] implementation is a no-op, so this prop
77/// is most useful for resize-aware widgets.
78pub trait BoundsProps: Widget + Sized {
79    /// Apply a new bounding rectangle and return the widget.
80    fn bounds(mut self, bounds: Rect) -> Self {
81        self.set_bounds(bounds);
82        self
83    }
84}
85
86impl<T> BoundsProps for T where T: Widget + Sized {}
87
88/// Fluent themed props for any widget that implements [`StyleProps`].
89pub trait ThemeProps: StyleProps {
90    /// Apply a complete themed style.
91    fn themed(
92        mut self,
93        theme: &Theme,
94        scheme: ColorScheme,
95        variant: Variant,
96        size: ComponentSize,
97    ) -> Self {
98        let resolved = theme.component_style(scheme, variant, size);
99        *self.style_mut() = resolved.style;
100        self
101    }
102
103    /// Apply a themed variant with medium sizing.
104    fn variant(self, theme: &Theme, scheme: ColorScheme, variant: Variant) -> Self {
105        self.themed(theme, scheme, variant, ComponentSize::Md)
106    }
107
108    /// Apply only the sizing-affecting style fields.
109    fn component_size(mut self, theme: &Theme, size: ComponentSize) -> Self {
110        let resolved = theme.component_size(size);
111        self.style_mut().radius = resolved.radius;
112        self.style_mut().border_width = resolved.border_width;
113        self
114    }
115}
116
117impl<T> ThemeProps for T where T: StyleProps {}
118
119macro_rules! impl_style_props_via_method {
120    ($($ty:path),+ $(,)?) => {
121        $(
122            impl StyleProps for $ty {
123                fn style_mut(&mut self) -> &mut Style {
124                    <$ty>::style_mut(self)
125                }
126            }
127        )+
128    };
129}
130
131macro_rules! impl_style_props_via_field {
132    ($($ty:path),+ $(,)?) => {
133        $(
134            impl StyleProps for $ty {
135                fn style_mut(&mut self) -> &mut Style {
136                    &mut self.style
137                }
138            }
139        )+
140    };
141}
142
143impl_style_props_via_method!(
144    crate::alert::Alert,
145    crate::badge::Badge,
146    crate::bar::Bar,
147    crate::button::IconButton,
148    crate::button_matrix::ButtonMatrix,
149    crate::checkbox::Checkbox,
150    crate::divider::Divider,
151    crate::drawer::Drawer,
152    crate::event::Slider,
153    crate::input::Input,
154    crate::input::Textarea,
155    crate::layout::BoxLayout,
156    crate::led::Led,
157    crate::list::List,
158    crate::modal::Modal,
159    crate::progress::Progress,
160    crate::radio::Radio,
161    crate::select::Select,
162    crate::spinner::Spinner,
163    crate::switch::Switch,
164    crate::tabs::Tabs,
165    crate::tag::Tag,
166    crate::text::Heading,
167    crate::text::Text,
168    crate::toast::Toast,
169    rlvgl_widgets::button::Button,
170);
171
172impl_style_props_via_field!(
173    rlvgl_widgets::calendar::Calendar,
174    rlvgl_widgets::message_box::MessageBox,
175    rlvgl_widgets::spinbox::Spinbox,
176    rlvgl_widgets::table::Table,
177    rlvgl_widgets::window::Window,
178);
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183    use crate::Progress;
184    use crate::theme::{ColorScheme, ComponentSize, Variant};
185
186    #[test]
187    fn style_props_update_main_style() {
188        let progress = Progress::new(
189            Rect {
190                x: 0,
191                y: 0,
192                width: 80,
193                height: 8,
194            },
195            0,
196            100,
197        )
198        .bg(Color(1, 2, 3, 255))
199        .border_color(Color(4, 5, 6, 255))
200        .border_width(2)
201        .rounded(4)
202        .opacity(128);
203
204        assert_eq!(progress.style().bg_color, Color(1, 2, 3, 255));
205        assert_eq!(progress.style().border_color, Color(4, 5, 6, 255));
206        assert_eq!(progress.style().border_width, 2);
207        assert_eq!(progress.style().radius, 4);
208        assert_eq!(progress.style().alpha, 128);
209    }
210
211    #[test]
212    fn theme_props_apply_variant_style() {
213        let theme = Theme::material_light();
214        let progress = Progress::new(
215            Rect {
216                x: 0,
217                y: 0,
218                width: 80,
219                height: 8,
220            },
221            0,
222            100,
223        )
224        .themed(
225            &theme,
226            ColorScheme::Danger,
227            Variant::Outline,
228            ComponentSize::Lg,
229        );
230
231        assert_eq!(progress.style().bg_color.3, 0);
232        assert_eq!(progress.style().border_width, 2);
233        assert_eq!(progress.style().radius, theme.tokens.radii.lg);
234    }
235}