Skip to main content

rlvgl_core/
property.rs

1//! Typed property accessor model for widget introspection.
2//!
3//! This module provides [`PropertyValue`] and the [`Queryable`] trait, giving
4//! creator and playit tooling a uniform way to read and write named properties
5//! on any widget — without requiring a global property registry or
6//! object-identity infrastructure (LPAR-02/04 deferred).
7//!
8//! # Design
9//!
10//! Widgets opt in to property introspection by implementing [`Queryable`].
11//! Callers hold the widget directly and query it by name:
12//!
13//! ```rust
14//! use rlvgl_core::property::{Queryable, PropertyValue};
15//!
16//! struct MyWidget { label: String }
17//!
18//! impl Queryable for MyWidget {
19//!     fn get_property(&self, key: &str) -> Option<PropertyValue> {
20//!         match key {
21//!             "text" => Some(PropertyValue::Text(self.label.clone())),
22//!             _ => None,
23//!         }
24//!     }
25//! }
26//! ```
27//!
28//! Key strings follow LVGL's naming vocabulary (`"text"`, `"radius"`,
29//! `"angle_start"`, `"frame_index"`, `"canvas_width"`, etc.).  A typed key
30//! enum is deferred-Safe (Specification Required) and does not require changes
31//! to this trait.
32//!
33//! # `no_std` notes
34//!
35//! `PropertyValue::Text` owns a heap-allocated [`alloc::string::String`], so
36//! this module requires `alloc`.  All other variants are stack-resident.
37//! Targets without a heap allocator MUST NOT use `PropertyValue::Text`
38//! (they will fail to compile if they try to construct it).
39
40extern crate alloc;
41
42use alloc::string::String;
43
44use crate::widget::Color;
45
46/// A typed property value exchanged via [`Queryable`].
47///
48/// Variants mirror LVGL's `LV_PROPERTY_TYPE_*` set for the four types
49/// available in v1.  The enum is `#[non_exhaustive]` so future additions
50/// (e.g. `Float(f32)`, `TextRef(&'static str)`) are Specification Required
51/// amendments that do not break existing `match` arms with `..` guards.
52///
53/// # Type mapping
54///
55/// | Variant | LVGL equivalent |
56/// |---|---|
57/// | `Int(i32)` | `LV_PROPERTY_TYPE_INT` |
58/// | `Bool(bool)` | `LV_PROPERTY_TYPE_BOOL` |
59/// | `Color(Color)` | `LV_PROPERTY_TYPE_COLOR` |
60/// | `Text(String)` | `LV_PROPERTY_TYPE_TEXT` |
61#[non_exhaustive]
62#[derive(Debug, Clone, PartialEq)]
63pub enum PropertyValue {
64    /// A 32-bit signed integer property (e.g. `"radius"`, `"frame_index"`).
65    Int(i32),
66    /// A boolean flag property (e.g. `"checked"`, `"hidden"`).
67    Bool(bool),
68    /// An RGBA color property (e.g. `"bg_color"`, `"text_color"`).
69    Color(Color),
70    /// A UTF-8 text property (e.g. `"text"`, `"placeholder"`).
71    ///
72    /// Requires `alloc`.  Use `PropertyValue::Text(s.into())` to convert from
73    /// `&str`.
74    Text(String),
75}
76
77impl PropertyValue {
78    /// Extract the `Int` payload, returning `None` for other variants.
79    ///
80    /// ```rust
81    /// use rlvgl_core::property::PropertyValue;
82    /// assert_eq!(PropertyValue::Int(42).as_int(), Some(42));
83    /// assert_eq!(PropertyValue::Bool(true).as_int(), None);
84    /// ```
85    pub fn as_int(&self) -> Option<i32> {
86        match self {
87            Self::Int(v) => Some(*v),
88            _ => None,
89        }
90    }
91
92    /// Extract the `Bool` payload, returning `None` for other variants.
93    ///
94    /// ```rust
95    /// use rlvgl_core::property::PropertyValue;
96    /// assert_eq!(PropertyValue::Bool(true).as_bool(), Some(true));
97    /// assert_eq!(PropertyValue::Int(1).as_bool(), None);
98    /// ```
99    pub fn as_bool(&self) -> Option<bool> {
100        match self {
101            Self::Bool(v) => Some(*v),
102            _ => None,
103        }
104    }
105
106    /// Extract the `Color` payload, returning `None` for other variants.
107    ///
108    /// ```rust
109    /// use rlvgl_core::property::PropertyValue;
110    /// use rlvgl_core::widget::Color;
111    /// let c = Color(255, 0, 0, 255);
112    /// assert_eq!(PropertyValue::Color(c).as_color(), Some(c));
113    /// assert_eq!(PropertyValue::Int(0).as_color(), None);
114    /// ```
115    pub fn as_color(&self) -> Option<Color> {
116        match self {
117            Self::Color(v) => Some(*v),
118            _ => None,
119        }
120    }
121
122    /// Extract a reference to the `Text` payload, returning `None` for other
123    /// variants.
124    ///
125    /// ```rust
126    /// use rlvgl_core::property::PropertyValue;
127    /// assert_eq!(
128    ///     PropertyValue::Text("hello".into()).as_text(),
129    ///     Some("hello")
130    /// );
131    /// assert_eq!(PropertyValue::Bool(false).as_text(), None);
132    /// ```
133    pub fn as_text(&self) -> Option<&str> {
134        match self {
135            Self::Text(v) => Some(v.as_str()),
136            _ => None,
137        }
138    }
139}
140
141/// Per-widget, identity-free property accessor trait.
142///
143/// Widgets that want creator or playit introspection implement this trait.
144/// The trait is deliberately decoupled from object identity (no
145/// [`ObjectNode`](crate::object::ObjectNode) reference, no global registry) —
146/// callers hold the widget directly and call `get_property` / `set_property`
147/// on it.  This satisfies the LPAR-00 §9 mandate while deferring any
148/// object-identity dependency to LPAR-02/04.
149///
150/// # Key strings
151///
152/// Key strings follow LVGL naming conventions.  Each widget defines its own
153/// set; unknown keys return `None` / `false` without panicking.  A typed
154/// key enum per widget is a Specification Required addition that does not
155/// require changes to this trait.
156///
157/// # Default `set_property`
158///
159/// Read-only widgets need not override `set_property`; the default returns
160/// `false` for every key.
161pub trait Queryable {
162    /// Read a named property value.
163    ///
164    /// Returns `None` if the key is unknown.  Implementations MUST NOT
165    /// panic on unrecognised keys.
166    fn get_property(&self, key: &str) -> Option<PropertyValue>;
167
168    /// Write a named property value.
169    ///
170    /// Returns `true` if the property was recognised **and** the supplied
171    /// value was of the correct type and applied successfully.  Returns
172    /// `false` for unknown keys **and** for type mismatches — in the latter
173    /// case the widget state MUST NOT be corrupted.
174    ///
175    /// The default implementation returns `false` for every key, making
176    /// the trait safe to impl on read-only widgets without an override.
177    fn set_property(&mut self, key: &str, value: PropertyValue) -> bool {
178        let _ = (key, value);
179        false
180    }
181}
182
183// ---------------------------------------------------------------------------
184// Tests
185// ---------------------------------------------------------------------------
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190    use alloc::string::ToString;
191
192    // -----------------------------------------------------------------------
193    // A minimal widget for round-trip tests
194    // -----------------------------------------------------------------------
195
196    struct Counter {
197        count: i32,
198        enabled: bool,
199        label: String,
200        tint: Color,
201    }
202
203    impl Counter {
204        fn new() -> Self {
205            Self {
206                count: 0,
207                enabled: true,
208                label: "counter".into(),
209                tint: Color(0, 128, 255, 255),
210            }
211        }
212    }
213
214    impl Queryable for Counter {
215        fn get_property(&self, key: &str) -> Option<PropertyValue> {
216            match key {
217                "count" => Some(PropertyValue::Int(self.count)),
218                "enabled" => Some(PropertyValue::Bool(self.enabled)),
219                "label" => Some(PropertyValue::Text(self.label.clone())),
220                "tint" => Some(PropertyValue::Color(self.tint)),
221                _ => None,
222            }
223        }
224
225        fn set_property(&mut self, key: &str, value: PropertyValue) -> bool {
226            match (key, value) {
227                ("count", PropertyValue::Int(v)) => {
228                    self.count = v;
229                    true
230                }
231                ("enabled", PropertyValue::Bool(v)) => {
232                    self.enabled = v;
233                    true
234                }
235                ("label", PropertyValue::Text(v)) => {
236                    self.label = v;
237                    true
238                }
239                ("tint", PropertyValue::Color(v)) => {
240                    self.tint = v;
241                    true
242                }
243                // Type mismatch or unknown key — no mutation
244                _ => false,
245            }
246        }
247    }
248
249    // -----------------------------------------------------------------------
250    // get_property round-trip tests
251    // -----------------------------------------------------------------------
252
253    #[test]
254    fn get_int_property_round_trip() {
255        let c = Counter::new();
256        assert_eq!(c.get_property("count"), Some(PropertyValue::Int(0)));
257    }
258
259    #[test]
260    fn get_bool_property_round_trip() {
261        let c = Counter::new();
262        assert_eq!(c.get_property("enabled"), Some(PropertyValue::Bool(true)));
263    }
264
265    #[test]
266    fn get_text_property_round_trip() {
267        let c = Counter::new();
268        assert_eq!(
269            c.get_property("label"),
270            Some(PropertyValue::Text("counter".into()))
271        );
272    }
273
274    #[test]
275    fn get_color_property_round_trip() {
276        let c = Counter::new();
277        assert_eq!(
278            c.get_property("tint"),
279            Some(PropertyValue::Color(Color(0, 128, 255, 255)))
280        );
281    }
282
283    #[test]
284    fn get_unknown_property_returns_none() {
285        let c = Counter::new();
286        assert_eq!(c.get_property("nonexistent"), None);
287    }
288
289    // -----------------------------------------------------------------------
290    // set_property tests
291    // -----------------------------------------------------------------------
292
293    #[test]
294    fn set_int_property_accepted_and_applied() {
295        let mut c = Counter::new();
296        let accepted = c.set_property("count", PropertyValue::Int(99));
297        assert!(accepted);
298        assert_eq!(c.count, 99);
299    }
300
301    #[test]
302    fn set_bool_property_accepted_and_applied() {
303        let mut c = Counter::new();
304        let accepted = c.set_property("enabled", PropertyValue::Bool(false));
305        assert!(accepted);
306        assert!(!c.enabled);
307    }
308
309    #[test]
310    fn set_text_property_accepted_and_applied() {
311        let mut c = Counter::new();
312        let accepted = c.set_property("label", PropertyValue::Text("new".into()));
313        assert!(accepted);
314        assert_eq!(c.label, "new");
315    }
316
317    #[test]
318    fn set_color_property_accepted_and_applied() {
319        let mut c = Counter::new();
320        let new_color = Color(255, 0, 0, 255);
321        let accepted = c.set_property("tint", PropertyValue::Color(new_color));
322        assert!(accepted);
323        assert_eq!(c.tint, new_color);
324    }
325
326    #[test]
327    fn set_unknown_property_returns_false() {
328        let mut c = Counter::new();
329        let accepted = c.set_property("bogus", PropertyValue::Int(1));
330        assert!(!accepted);
331    }
332
333    #[test]
334    fn set_property_type_mismatch_returns_false_and_no_corruption() {
335        let mut c = Counter::new();
336        let original_count = c.count;
337        // Pass a Bool where Int is expected
338        let accepted = c.set_property("count", PropertyValue::Bool(true));
339        assert!(!accepted, "type mismatch must be rejected");
340        assert_eq!(c.count, original_count, "state must not be corrupted");
341    }
342
343    #[test]
344    fn set_property_type_mismatch_text_for_int_no_corruption() {
345        let mut c = Counter::new();
346        let original = c.count;
347        let accepted = c.set_property("count", PropertyValue::Text("five".into()));
348        assert!(!accepted);
349        assert_eq!(c.count, original);
350    }
351
352    // -----------------------------------------------------------------------
353    // PropertyValue accessor helpers
354    // -----------------------------------------------------------------------
355
356    #[test]
357    fn as_int_correct_variant() {
358        assert_eq!(PropertyValue::Int(7).as_int(), Some(7));
359    }
360
361    #[test]
362    fn as_int_wrong_variant() {
363        assert_eq!(PropertyValue::Bool(true).as_int(), None);
364    }
365
366    #[test]
367    fn as_bool_correct_variant() {
368        assert_eq!(PropertyValue::Bool(false).as_bool(), Some(false));
369    }
370
371    #[test]
372    fn as_bool_wrong_variant() {
373        assert_eq!(PropertyValue::Int(0).as_bool(), None);
374    }
375
376    #[test]
377    fn as_color_correct_variant() {
378        let c = Color(1, 2, 3, 4);
379        assert_eq!(PropertyValue::Color(c).as_color(), Some(c));
380    }
381
382    #[test]
383    fn as_color_wrong_variant() {
384        assert_eq!(PropertyValue::Text("x".into()).as_color(), None);
385    }
386
387    #[test]
388    fn as_text_correct_variant() {
389        assert_eq!(PropertyValue::Text("hello".into()).as_text(), Some("hello"));
390    }
391
392    #[test]
393    fn as_text_wrong_variant() {
394        assert_eq!(PropertyValue::Int(0).as_text(), None);
395    }
396
397    // -----------------------------------------------------------------------
398    // Default set_property on a read-only widget
399    // -----------------------------------------------------------------------
400
401    struct ReadOnly;
402
403    impl Queryable for ReadOnly {
404        fn get_property(&self, key: &str) -> Option<PropertyValue> {
405            match key {
406                "version" => Some(PropertyValue::Int(1)),
407                _ => None,
408            }
409        }
410        // set_property not overridden → default returns false
411    }
412
413    #[test]
414    fn default_set_property_returns_false() {
415        let mut ro = ReadOnly;
416        assert!(!ro.set_property("version", PropertyValue::Int(2)));
417        // And state is trivially unchanged (ReadOnly has no mutable state)
418    }
419
420    #[test]
421    fn property_value_clone_and_eq() {
422        let v = PropertyValue::Text("abc".to_string());
423        assert_eq!(v.clone(), v);
424    }
425}