rlvgl_core/observer.rs
1//! Value-binding `Subject<T>` for deterministic observer notifications.
2//!
3//! This module provides a typed publish/subscribe primitive that is **separate
4//! from** the LPAR-04 [`ObjectEvent`](crate::object::ObjectEvent) dispatch
5//! system. It carries scalar data values (`i32`, `bool`, [`Color`](crate::widget::Color),
6//! `String`) between arbitrary owners through explicit subscribe/notify
7//! callbacks, without routing through `ObjectNode` or emitting `ObjectEvent`.
8//!
9//! # Relationship to the LPAR-04 event system
10//!
11//! | Dimension | LPAR-04 `ObjectEvent` | `Subject<T>` |
12//! |---|---|---|
13//! | What it carries | Semantic gestures, lifecycle signals | Scalar data values |
14//! | Routing | Tree dispatch through `ObjectNode` | Direct callback invocation |
15//! | Coupling | Requires `ObjectNode` hierarchy | Caller holds both ends |
16//! | v1 integration | Not altered | Added orthogonally |
17//!
18//! A widget MAY both receive `ObjectEvent::Clicked` (dispatched by the tree
19//! router) **and** update a `Subject<bool>` it owns. No framework mechanism
20//! connects them; wiring is the caller's responsibility.
21//!
22//! # `no_std` notes
23//!
24//! [`Subject<T>`] requires `alloc` (it owns a `Vec<Box<dyn FnMut>>` observer
25//! list). Targets without a heap allocator MUST NOT instantiate subjects;
26//! the type compiles under `no_std + alloc` but panics if the allocator
27//! aborts.
28//!
29//! # Reentrancy
30//!
31//! Observers receive `&T`, not `&mut Subject<T>`, so direct recursive
32//! mutation of the *same* subject from within its own observer is
33//! structurally prevented. A second `Subject` may be mutated from within an
34//! observer โ this is the intended cross-subject binding pattern (see
35//! [`tests::cross_subject_observer`](crate::observer) below).
36//!
37//! A lightweight `notifying: bool` sentinel is included as a defensive guard.
38//! If a re-entrant [`Subject::notify`] call somehow occurs (e.g. via unsafe
39//! aliasing outside this module), the inner call is silently skipped rather
40//! than potentially overflowing the stack or double-borrowing. This is
41//! documented and testable; it does **not** imply that re-entrant use is
42//! supported.
43
44extern crate alloc;
45
46use alloc::boxed::Box;
47use alloc::vec::Vec;
48
49/// Heap-allocated, mutable observer callback for a value of type `T`.
50///
51/// Factored out to satisfy `clippy::type_complexity`; the underlying type is
52/// `Box<dyn FnMut(&T)>`.
53pub type ObserverFn<T> = Box<dyn FnMut(&T)>;
54
55/// A typed value holder with an observer callback list.
56///
57/// `Subject<T>` stores a current and previous value and a list of callbacks.
58/// Calling [`set`](Subject::set) atomically updates the value and notifies
59/// all registered observers synchronously, in subscription order.
60///
61/// ## Type coverage in v1
62///
63/// `Subject<i32>`, `Subject<bool>`, `Subject<Color>`, and `Subject<String>`
64/// cover the four [`PropertyValue`](crate::property::PropertyValue) variants.
65/// Subjects are intentionally typed โ callers subscribe to a `Subject<i32>`,
66/// not to a `Subject<PropertyValue>`. Bridging the two is application-level
67/// code.
68///
69/// ## Object-identity-free
70///
71/// `Subject<T>` does not require object ids, `WidgetId`, or `ObjectNode`
72/// references. Subjects are owned values; callers hold them directly. This
73/// satisfies the LPAR-15 ยง5.I binding invariant.
74pub struct Subject<T: Clone> {
75 /// Current value.
76 value: T,
77 /// Value before the most recent [`set`](Subject::set) call. Equals
78 /// `value` on construction (no previous state exists yet).
79 prev_value: T,
80 /// Registered observer callbacks, called in subscription order.
81 observers: Vec<ObserverFn<T>>,
82 /// Reentrancy sentinel: `true` while [`notify`](Subject::notify) is
83 /// dispatching. A re-entrant `notify` call skips execution rather than
84 /// recursing. Callers MUST NOT rely on re-entrant notifications being
85 /// delivered; this guard is purely defensive.
86 notifying: bool,
87}
88
89impl<T: Clone> Subject<T> {
90 /// Create a new `Subject` with the given initial value.
91 ///
92 /// `prev_value` is initialised to a clone of `initial`; no observers are
93 /// registered.
94 ///
95 /// ```rust
96 /// use rlvgl_core::observer::Subject;
97 /// let s: Subject<i32> = Subject::new(0);
98 /// assert_eq!(*s.get(), 0);
99 /// assert_eq!(*s.prev(), 0);
100 /// ```
101 pub fn new(initial: T) -> Self {
102 Self {
103 prev_value: initial.clone(),
104 value: initial,
105 observers: Vec::new(),
106 notifying: false,
107 }
108 }
109
110 /// Borrow the current value.
111 pub fn get(&self) -> &T {
112 &self.value
113 }
114
115 /// Borrow the value from before the most recent [`set`](Subject::set) call.
116 ///
117 /// On a freshly constructed subject `prev() == get()`.
118 pub fn prev(&self) -> &T {
119 &self.prev_value
120 }
121
122 /// Store a new value, copy the old value to `prev`, then notify all
123 /// observers synchronously in subscription order.
124 ///
125 /// Observers receive a shared reference `&T` to the **new** value. They
126 /// MUST NOT attempt to call `set` on *this same* subject; that is
127 /// structurally prevented because they do not hold a `&mut Subject`.
128 /// Cross-subject `set` (observer of A mutates B) is the intended
129 /// composition pattern and works correctly.
130 pub fn set(&mut self, value: T) {
131 self.prev_value = self.value.clone();
132 self.value = value;
133 self.notify();
134 }
135
136 /// Notify all observers with the current value without changing it.
137 ///
138 /// `prev_value` is also left unchanged. Useful for re-broadcasting after
139 /// an external mutation or initialisation.
140 ///
141 /// If this method is already executing on the call stack (re-entrant call),
142 /// the inner invocation is silently skipped. See the module-level
143 /// reentrancy note for rationale.
144 pub fn notify(&mut self) {
145 if self.notifying {
146 return;
147 }
148 self.notifying = true;
149 // Snapshot the current value so that observers see a consistent view.
150 let current = self.value.clone();
151 for cb in &mut self.observers {
152 cb(¤t);
153 }
154 self.notifying = false;
155 }
156
157 /// Register an observer callback.
158 ///
159 /// Callbacks are invoked in subscription order on every [`set`] or
160 /// explicit [`notify`](Subject::notify) call. There is no unsubscribe
161 /// mechanism in v1; adding one is a Specification Required amendment that
162 /// does not break existing call sites.
163 ///
164 /// `F: 'static` is required because the closure is heap-allocated behind a
165 /// `Box<dyn FnMut>` and may outlive the calling scope.
166 pub fn subscribe<F: FnMut(&T) + 'static>(&mut self, cb: F) {
167 self.observers.push(Box::new(cb));
168 }
169}
170
171// ---------------------------------------------------------------------------
172// Tests
173// ---------------------------------------------------------------------------
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use alloc::rc::Rc;
179 use alloc::string::String;
180 use alloc::vec;
181 use core::cell::Cell;
182 use core::cell::RefCell;
183
184 // -----------------------------------------------------------------------
185 // Basic get / prev after construction
186 // -----------------------------------------------------------------------
187
188 #[test]
189 fn new_get_and_prev_equal_initial() {
190 let s: Subject<i32> = Subject::new(42);
191 assert_eq!(*s.get(), 42);
192 assert_eq!(*s.prev(), 42);
193 }
194
195 // -----------------------------------------------------------------------
196 // set() notifies observers and updates prev
197 // -----------------------------------------------------------------------
198
199 #[test]
200 fn set_updates_value_and_prev() {
201 let mut s: Subject<i32> = Subject::new(10);
202 s.set(20);
203 assert_eq!(*s.get(), 20);
204 assert_eq!(*s.prev(), 10);
205 }
206
207 #[test]
208 fn set_notifies_single_observer() {
209 let mut s: Subject<i32> = Subject::new(0);
210 let received = Rc::new(Cell::new(-1_i32));
211 let received_clone = received.clone();
212 s.subscribe(move |v| received_clone.set(*v));
213 s.set(7);
214 assert_eq!(received.get(), 7);
215 }
216
217 #[test]
218 fn set_notifies_multiple_observers_in_order() {
219 let mut s: Subject<i32> = Subject::new(0);
220 let log = Rc::new(RefCell::new(Vec::<(usize, i32)>::new()));
221
222 for idx in 0..3 {
223 let log_clone = log.clone();
224 s.subscribe(move |v| log_clone.borrow_mut().push((idx, *v)));
225 }
226
227 s.set(5);
228
229 let entries = log.borrow();
230 assert_eq!(*entries, vec![(0, 5), (1, 5), (2, 5)]);
231 }
232
233 // -----------------------------------------------------------------------
234 // notify() fires without changing value or prev
235 // -----------------------------------------------------------------------
236
237 #[test]
238 fn notify_fires_without_changing_value() {
239 let mut s: Subject<i32> = Subject::new(3);
240 s.set(9); // prev = 3, value = 9
241 let call_count = Rc::new(Cell::new(0_u32));
242 let call_count_clone = call_count.clone();
243 s.subscribe(move |_| call_count_clone.set(call_count_clone.get() + 1));
244
245 s.notify();
246 assert_eq!(*s.get(), 9, "value unchanged by notify");
247 assert_eq!(*s.prev(), 3, "prev unchanged by notify");
248 assert_eq!(call_count.get(), 1, "observer fired once");
249 }
250
251 // -----------------------------------------------------------------------
252 // Multiple sequential set() calls
253 // -----------------------------------------------------------------------
254
255 #[test]
256 fn multiple_set_calls_track_prev_correctly() {
257 let mut s: Subject<i32> = Subject::new(1);
258 s.set(2);
259 assert_eq!(*s.prev(), 1);
260 assert_eq!(*s.get(), 2);
261 s.set(3);
262 assert_eq!(*s.prev(), 2);
263 assert_eq!(*s.get(), 3);
264 }
265
266 // -----------------------------------------------------------------------
267 // Cross-subject observer: observer of A updates B
268 // -----------------------------------------------------------------------
269
270 #[test]
271 fn cross_subject_observer() {
272 let mut a: Subject<i32> = Subject::new(0);
273 let b = Rc::new(RefCell::new(Subject::<i32>::new(0)));
274
275 let b_clone = b.clone();
276 a.subscribe(move |v| {
277 b_clone.borrow_mut().set(*v * 2);
278 });
279
280 a.set(5);
281
282 assert_eq!(*a.get(), 5);
283 assert_eq!(*b.borrow().get(), 10);
284 assert_eq!(*b.borrow().prev(), 0);
285 }
286
287 // -----------------------------------------------------------------------
288 // Reentrancy sentinel: re-entrant notify is skipped, no panic/recursion
289 // -----------------------------------------------------------------------
290
291 #[test]
292 fn reentrant_notify_is_skipped_no_panic() {
293 // We use a raw pointer trick to demonstrate the sentinel: we capture a
294 // pointer to the subject and attempt a notify inside the observer. The
295 // sentinel `notifying = true` prevents recursion. This is intentionally
296 // unsafe in the test only to exercise the guard; production code cannot
297 // trigger this path because observers receive `&T`, not `&mut Subject`.
298 let mut s: Subject<i32> = Subject::new(0);
299 let call_count = Rc::new(Cell::new(0_u32));
300 let call_count_clone = call_count.clone();
301
302 // SAFETY (test-only): we hold a raw pointer to `s` solely to invoke
303 // `notify` inside the observer and exercise the reentrancy guard.
304 // `s` outlives the closure; the closure is dropped before `s`.
305 // No other aliasing occurs; the sentinel prevents actual re-entry.
306 let s_ptr = &mut s as *mut Subject<i32>;
307 s.subscribe(move |_| {
308 call_count_clone.set(call_count_clone.get() + 1);
309 // Attempt a re-entrant notify
310 // SAFETY: see above.
311 unsafe { (*s_ptr).notify() };
312 });
313
314 s.set(1);
315
316 // The outer notify fires the observer once. The inner notify is
317 // skipped by the sentinel, so the observer is NOT called a second time.
318 assert_eq!(call_count.get(), 1, "re-entrant notify must be skipped");
319 assert_eq!(*s.get(), 1);
320 }
321
322 // -----------------------------------------------------------------------
323 // Subject<bool>
324 // -----------------------------------------------------------------------
325
326 #[test]
327 fn bool_subject_set_and_notify() {
328 let mut s: Subject<bool> = Subject::new(false);
329 let seen = Rc::new(Cell::new(false));
330 let seen_clone = seen.clone();
331 s.subscribe(move |v| seen_clone.set(*v));
332 s.set(true);
333 assert!(seen.get());
334 }
335
336 // -----------------------------------------------------------------------
337 // Subject<String>
338 // -----------------------------------------------------------------------
339
340 #[test]
341 fn string_subject_tracks_value() {
342 let mut s: Subject<String> = Subject::new("hello".into());
343 s.set("world".into());
344 assert_eq!(s.get().as_str(), "world");
345 assert_eq!(s.prev().as_str(), "hello");
346 }
347
348 // -----------------------------------------------------------------------
349 // Zero observers โ set is a no-op for notifications
350 // -----------------------------------------------------------------------
351
352 #[test]
353 fn set_with_no_observers_does_not_panic() {
354 let mut s: Subject<i32> = Subject::new(0);
355 s.set(42); // must not panic
356 assert_eq!(*s.get(), 42);
357 }
358}