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}