dampen_core/shared/
mod.rs

1//! Shared state container for inter-window communication.
2//!
3//! This module provides the [`SharedContext`] type for sharing state across
4//! multiple views in a Dampen application.
5//!
6//! # Overview
7//!
8//! `SharedContext<S>` is a thread-safe, reference-counted container that allows
9//! multiple views to read and write a shared state. When one view modifies the
10//! shared state, all other views immediately see the change.
11//!
12//! # Example
13//!
14//! ```rust
15//! use dampen_core::SharedContext;
16//! use dampen_core::UiBindable;
17//! use dampen_core::BindingValue;
18//!
19//! #[derive(Default, Clone)]
20//! struct SharedState {
21//!     theme: String,
22//!     language: String,
23//! }
24//!
25//! impl UiBindable for SharedState {
26//!     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
27//!         match path {
28//!             ["theme"] => Some(BindingValue::String(self.theme.clone())),
29//!             ["language"] => Some(BindingValue::String(self.language.clone())),
30//!             _ => None,
31//!         }
32//!     }
33//!     fn available_fields() -> Vec<String> {
34//!         vec!["theme".to_string(), "language".to_string()]
35//!     }
36//! }
37//!
38//! // Create shared context
39//! let ctx = SharedContext::new(SharedState::default());
40//!
41//! // Clone for another view (same underlying state)
42//! let ctx2 = ctx.clone();
43//!
44//! // Modify in one view
45//! ctx.write().theme = "dark".to_string();
46//!
47//! // See change in another view
48//! assert_eq!(ctx2.read().theme, "dark");
49//! ```
50//!
51//! # Thread Safety
52//!
53//! `SharedContext` uses `Arc<RwLock<S>>` internally, making it safe to share
54//! across threads. Multiple readers can access the state simultaneously, but
55//! writers get exclusive access.
56//!
57//! # See Also
58//!
59//! - [`AppState`](crate::state::AppState) - Per-view state container that can hold a `SharedContext`
60//! - [`UiBindable`](crate::binding::UiBindable) - Trait required for shared state types
61
62use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
63
64use crate::binding::{BindingValue, UiBindable};
65
66/// Thread-safe shared state container.
67///
68/// `SharedContext` wraps user-defined shared state in an `Arc<RwLock<S>>`,
69/// enabling safe concurrent access from multiple views. Each view receives
70/// a cloned reference to the same underlying state.
71///
72/// # Type Parameters
73///
74/// * `S` - The shared state type. Must implement:
75///   - [`UiBindable`] for XML binding access (e.g., `{shared.field}`)
76///   - `Send + Sync` for thread safety
77///   - `'static` for Arc storage
78///
79/// # Example
80///
81/// ```rust
82/// use dampen_core::SharedContext;
83/// use dampen_core::{UiBindable, BindingValue};
84///
85/// #[derive(Default, Clone)]
86/// struct SharedState {
87///     theme: String,
88///     user_name: Option<String>,
89/// }
90///
91/// impl UiBindable for SharedState {
92///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
93///         match path {
94///             ["theme"] => Some(BindingValue::String(self.theme.clone())),
95///             ["user_name"] => match &self.user_name {
96///                 Some(name) => Some(BindingValue::String(name.clone())),
97///                 None => Some(BindingValue::None),
98///             },
99///             _ => None,
100///         }
101///     }
102///     fn available_fields() -> Vec<String> {
103///         vec!["theme".to_string(), "user_name".to_string()]
104///     }
105/// }
106///
107/// let ctx = SharedContext::new(SharedState::default());
108/// let ctx2 = ctx.clone(); // Same underlying state
109///
110/// ctx.write().theme = "dark".to_string();
111/// assert_eq!(ctx2.read().theme, "dark");
112/// ```
113///
114/// # Lock Poisoning
115///
116/// If a thread panics while holding a write lock, the lock becomes "poisoned".
117/// The `read()` and `write()` methods will panic in this case. Use `try_read()`
118/// and `try_write()` for fallible access.
119#[derive(Debug)]
120pub struct SharedContext<S>
121where
122    S: UiBindable + Send + Sync + 'static,
123{
124    state: Arc<RwLock<S>>,
125}
126
127impl<S> SharedContext<S>
128where
129    S: UiBindable + Send + Sync + 'static,
130{
131    /// Create a new SharedContext with initial state.
132    ///
133    /// # Arguments
134    ///
135    /// * `initial` - The initial shared state value
136    ///
137    /// # Example
138    ///
139    /// ```rust
140    /// use dampen_core::SharedContext;
141    /// use dampen_core::{UiBindable, BindingValue};
142    ///
143    /// #[derive(Default)]
144    /// struct MyState { counter: i32 }
145    ///
146    /// impl UiBindable for MyState {
147    ///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
148    ///         match path {
149    ///             ["counter"] => Some(BindingValue::Integer(self.counter as i64)),
150    ///             _ => None,
151    ///         }
152    ///     }
153    ///     fn available_fields() -> Vec<String> { vec!["counter".to_string()] }
154    /// }
155    ///
156    /// let ctx = SharedContext::new(MyState { counter: 42 });
157    /// assert_eq!(ctx.read().counter, 42);
158    /// ```
159    pub fn new(initial: S) -> Self {
160        Self {
161            state: Arc::new(RwLock::new(initial)),
162        }
163    }
164
165    /// Acquire read access to shared state.
166    ///
167    /// Returns a guard that provides immutable access to the shared state.
168    /// Multiple readers can hold guards simultaneously.
169    ///
170    /// # Panics
171    ///
172    /// Panics if the lock is poisoned (a thread panicked while holding write lock).
173    /// Use [`try_read`](Self::try_read) for fallible access.
174    ///
175    /// # Example
176    ///
177    /// ```rust
178    /// use dampen_core::SharedContext;
179    /// use dampen_core::{UiBindable, BindingValue};
180    ///
181    /// #[derive(Default)]
182    /// struct MyState { value: String }
183    ///
184    /// impl UiBindable for MyState {
185    ///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
186    ///         match path {
187    ///             ["value"] => Some(BindingValue::String(self.value.clone())),
188    ///             _ => None,
189    ///         }
190    ///     }
191    ///     fn available_fields() -> Vec<String> { vec!["value".to_string()] }
192    /// }
193    ///
194    /// let ctx = SharedContext::new(MyState { value: "hello".to_string() });
195    /// let guard = ctx.read();
196    /// assert_eq!(guard.value, "hello");
197    /// ```
198    #[allow(clippy::expect_used)]
199    pub fn read(&self) -> RwLockReadGuard<'_, S> {
200        self.state.read().expect("SharedContext lock poisoned")
201    }
202
203    /// Acquire write access to shared state.
204    ///
205    /// Returns a guard that provides mutable access to the shared state.
206    /// Only one writer can hold the guard at a time, and no readers can
207    /// access the state while a write guard is held.
208    ///
209    /// # Panics
210    ///
211    /// Panics if the lock is poisoned.
212    /// Use [`try_write`](Self::try_write) for fallible access.
213    ///
214    /// # Example
215    ///
216    /// ```rust
217    /// use dampen_core::SharedContext;
218    /// use dampen_core::{UiBindable, BindingValue};
219    ///
220    /// #[derive(Default)]
221    /// struct MyState { counter: i32 }
222    ///
223    /// impl UiBindable for MyState {
224    ///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
225    ///         match path {
226    ///             ["counter"] => Some(BindingValue::Integer(self.counter as i64)),
227    ///             _ => None,
228    ///         }
229    ///     }
230    ///     fn available_fields() -> Vec<String> { vec!["counter".to_string()] }
231    /// }
232    ///
233    /// let ctx = SharedContext::new(MyState { counter: 0 });
234    /// ctx.write().counter += 1;
235    /// assert_eq!(ctx.read().counter, 1);
236    /// ```
237    #[allow(clippy::expect_used)]
238    pub fn write(&self) -> RwLockWriteGuard<'_, S> {
239        self.state.write().expect("SharedContext lock poisoned")
240    }
241
242    /// Try to acquire read access without blocking.
243    ///
244    /// Returns `None` if the lock is currently held for writing or is poisoned.
245    /// This is useful when you want to avoid blocking on a potentially
246    /// long-held write lock.
247    ///
248    /// # Example
249    ///
250    /// ```rust
251    /// use dampen_core::SharedContext;
252    /// use dampen_core::{UiBindable, BindingValue};
253    ///
254    /// #[derive(Default)]
255    /// struct MyState { value: i32 }
256    ///
257    /// impl UiBindable for MyState {
258    ///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
259    ///         match path {
260    ///             ["value"] => Some(BindingValue::Integer(self.value as i64)),
261    ///             _ => None,
262    ///         }
263    ///     }
264    ///     fn available_fields() -> Vec<String> { vec!["value".to_string()] }
265    /// }
266    ///
267    /// let ctx = SharedContext::new(MyState { value: 42 });
268    /// if let Some(guard) = ctx.try_read() {
269    ///     assert_eq!(guard.value, 42);
270    /// }
271    /// ```
272    pub fn try_read(&self) -> Option<RwLockReadGuard<'_, S>> {
273        self.state.try_read().ok()
274    }
275
276    /// Try to acquire write access without blocking.
277    ///
278    /// Returns `None` if the lock is currently held (for reading or writing)
279    /// or is poisoned.
280    ///
281    /// # Example
282    ///
283    /// ```rust
284    /// use dampen_core::SharedContext;
285    /// use dampen_core::{UiBindable, BindingValue};
286    ///
287    /// #[derive(Default)]
288    /// struct MyState { value: i32 }
289    ///
290    /// impl UiBindable for MyState {
291    ///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
292    ///         match path {
293    ///             ["value"] => Some(BindingValue::Integer(self.value as i64)),
294    ///             _ => None,
295    ///         }
296    ///     }
297    ///     fn available_fields() -> Vec<String> { vec!["value".to_string()] }
298    /// }
299    ///
300    /// let ctx = SharedContext::new(MyState { value: 0 });
301    /// if let Some(mut guard) = ctx.try_write() {
302    ///     guard.value = 100;
303    /// }
304    /// ```
305    pub fn try_write(&self) -> Option<RwLockWriteGuard<'_, S>> {
306        self.state.try_write().ok()
307    }
308}
309
310impl<S> Clone for SharedContext<S>
311where
312    S: UiBindable + Send + Sync + 'static,
313{
314    /// Clone the SharedContext.
315    ///
316    /// This creates a new `SharedContext` that references the same underlying
317    /// state. Modifications through one clone are visible to all others.
318    fn clone(&self) -> Self {
319        Self {
320            state: Arc::clone(&self.state),
321        }
322    }
323}
324
325/// Implement UiBindable for SharedContext by delegating to the inner state.
326///
327/// This allows SharedContext to be used directly in widget builders and bindings
328/// without needing to extract the inner state first.
329///
330/// # Example
331///
332/// ```rust,ignore
333/// use dampen_core::{SharedContext, UiBindable, BindingValue};
334///
335/// #[derive(Default)]
336/// struct MyState { value: i32 }
337///
338/// impl UiBindable for MyState {
339///     fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
340///         match path {
341///             ["value"] => Some(BindingValue::Integer(self.value as i64)),
342///             _ => None,
343///         }
344///     }
345///     fn available_fields() -> Vec<String> { vec!["value".to_string()] }
346/// }
347///
348/// let ctx = SharedContext::new(MyState { value: 42 });
349/// // Can use ctx directly as &dyn UiBindable
350/// assert_eq!(ctx.get_field(&["value"]), Some(BindingValue::Integer(42)));
351/// ```
352impl<S> UiBindable for SharedContext<S>
353where
354    S: UiBindable + Send + Sync + 'static,
355{
356    fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
357        // Acquire read lock and delegate to inner state
358        let guard = self.read();
359        guard.get_field(path)
360    }
361
362    fn available_fields() -> Vec<String> {
363        // Delegate to the inner type's available fields
364        S::available_fields()
365    }
366}
367
368/// Special implementation for unit type when shared state is not used.
369///
370/// This allows applications that don't use shared state to still compile
371/// without requiring a real shared state type.
372impl SharedContext<()> {
373    /// Create an empty shared context (no-op).
374    ///
375    /// Used internally when an application doesn't configure shared state.
376    /// The resulting context holds unit type `()` and is essentially a no-op.
377    ///
378    /// # Example
379    ///
380    /// ```rust
381    /// use dampen_core::SharedContext;
382    ///
383    /// let ctx = SharedContext::<()>::empty();
384    /// // Can still call read/write, but they just return ()
385    /// let _guard = ctx.read();
386    /// ```
387    pub fn empty() -> Self {
388        Self::new(())
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395    use crate::BindingValue;
396    use std::sync::atomic::{AtomicUsize, Ordering};
397    use std::thread;
398
399    /// Test helper: Simple shared state
400    #[derive(Default, Clone)]
401    struct TestState {
402        counter: i32,
403        name: String,
404    }
405
406    impl UiBindable for TestState {
407        fn get_field(&self, path: &[&str]) -> Option<BindingValue> {
408            match path {
409                ["counter"] => Some(BindingValue::Integer(self.counter as i64)),
410                ["name"] => Some(BindingValue::String(self.name.clone())),
411                _ => None,
412            }
413        }
414
415        fn available_fields() -> Vec<String> {
416            vec!["counter".to_string(), "name".to_string()]
417        }
418    }
419
420    // ========================================
421    // T011: Test SharedContext read/write access
422    // ========================================
423
424    #[test]
425    fn test_shared_context_new() {
426        let ctx = SharedContext::new(TestState {
427            counter: 42,
428            name: "test".to_string(),
429        });
430        assert_eq!(ctx.read().counter, 42);
431        assert_eq!(ctx.read().name, "test");
432    }
433
434    #[test]
435    fn test_shared_context_read() {
436        let ctx = SharedContext::new(TestState {
437            counter: 10,
438            name: "hello".to_string(),
439        });
440
441        let guard = ctx.read();
442        assert_eq!(guard.counter, 10);
443        assert_eq!(guard.name, "hello");
444    }
445
446    #[test]
447    fn test_shared_context_write() {
448        let ctx = SharedContext::new(TestState::default());
449
450        {
451            let mut guard = ctx.write();
452            guard.counter = 100;
453            guard.name = "updated".to_string();
454        }
455
456        assert_eq!(ctx.read().counter, 100);
457        assert_eq!(ctx.read().name, "updated");
458    }
459
460    #[test]
461    fn test_shared_context_try_read() {
462        let ctx = SharedContext::new(TestState {
463            counter: 5,
464            ..Default::default()
465        });
466
467        // Should succeed when no write lock is held
468        let guard = ctx.try_read();
469        assert!(guard.is_some());
470        assert_eq!(guard.unwrap().counter, 5);
471    }
472
473    #[test]
474    fn test_shared_context_try_write() {
475        let ctx = SharedContext::new(TestState::default());
476
477        // Should succeed when no lock is held
478        let guard = ctx.try_write();
479        assert!(guard.is_some());
480    }
481
482    // ========================================
483    // T012: Test SharedContext clone shares state
484    // ========================================
485
486    #[test]
487    fn test_shared_context_clone_shares_state() {
488        let ctx1 = SharedContext::new(TestState {
489            counter: 0,
490            name: "original".to_string(),
491        });
492
493        // Clone the context
494        let ctx2 = ctx1.clone();
495
496        // Modify through ctx1
497        ctx1.write().counter = 42;
498        ctx1.write().name = "modified".to_string();
499
500        // Verify ctx2 sees the changes
501        assert_eq!(ctx2.read().counter, 42);
502        assert_eq!(ctx2.read().name, "modified");
503
504        // Modify through ctx2
505        ctx2.write().counter = 100;
506
507        // Verify ctx1 sees the changes
508        assert_eq!(ctx1.read().counter, 100);
509    }
510
511    #[test]
512    fn test_shared_context_multiple_clones() {
513        let original = SharedContext::new(TestState {
514            counter: 1,
515            ..Default::default()
516        });
517
518        let clone1 = original.clone();
519        let clone2 = original.clone();
520        let clone3 = clone1.clone();
521
522        // All clones point to the same state
523        original.write().counter = 999;
524
525        assert_eq!(clone1.read().counter, 999);
526        assert_eq!(clone2.read().counter, 999);
527        assert_eq!(clone3.read().counter, 999);
528    }
529
530    // ========================================
531    // T013: Test SharedContext thread safety with concurrent access
532    // ========================================
533
534    #[test]
535    fn test_shared_context_concurrent_reads() {
536        let ctx = SharedContext::new(TestState {
537            counter: 42,
538            name: "concurrent".to_string(),
539        });
540
541        let read_count = Arc::new(AtomicUsize::new(0));
542        let mut handles = vec![];
543
544        // Spawn multiple reader threads
545        for _ in 0..10 {
546            let ctx_clone = ctx.clone();
547            let count = Arc::clone(&read_count);
548
549            let handle = thread::spawn(move || {
550                let guard = ctx_clone.read();
551                assert_eq!(guard.counter, 42);
552                assert_eq!(guard.name, "concurrent");
553                count.fetch_add(1, Ordering::SeqCst);
554            });
555
556            handles.push(handle);
557        }
558
559        // Wait for all threads
560        for handle in handles {
561            handle.join().expect("Thread panicked");
562        }
563
564        assert_eq!(read_count.load(Ordering::SeqCst), 10);
565    }
566
567    #[test]
568    fn test_shared_context_concurrent_writes() {
569        let ctx = SharedContext::new(TestState::default());
570        let mut handles = vec![];
571
572        // Spawn multiple writer threads, each incrementing counter
573        for i in 0..10 {
574            let ctx_clone = ctx.clone();
575
576            let handle = thread::spawn(move || {
577                let mut guard = ctx_clone.write();
578                guard.counter += 1;
579                guard.name = format!("writer-{}", i);
580            });
581
582            handles.push(handle);
583        }
584
585        // Wait for all threads
586        for handle in handles {
587            handle.join().expect("Thread panicked");
588        }
589
590        // Counter should be exactly 10 (each thread incremented once)
591        assert_eq!(ctx.read().counter, 10);
592    }
593
594    #[test]
595    fn test_shared_context_mixed_read_write() {
596        let ctx = SharedContext::new(TestState {
597            counter: 0,
598            ..Default::default()
599        });
600
601        let mut handles = vec![];
602
603        // Spawn writer threads
604        for _ in 0..5 {
605            let ctx_clone = ctx.clone();
606            let handle = thread::spawn(move || {
607                for _ in 0..100 {
608                    ctx_clone.write().counter += 1;
609                }
610            });
611            handles.push(handle);
612        }
613
614        // Spawn reader threads
615        for _ in 0..5 {
616            let ctx_clone = ctx.clone();
617            let handle = thread::spawn(move || {
618                for _ in 0..100 {
619                    let _ = ctx_clone.read().counter;
620                }
621            });
622            handles.push(handle);
623        }
624
625        // Wait for all threads
626        for handle in handles {
627            handle.join().expect("Thread panicked");
628        }
629
630        // Counter should be exactly 500 (5 writers * 100 increments each)
631        assert_eq!(ctx.read().counter, 500);
632    }
633
634    // ========================================
635    // Additional tests for edge cases
636    // ========================================
637
638    #[test]
639    fn test_shared_context_empty() {
640        let ctx = SharedContext::<()>::empty();
641        // Should be able to read and write without panicking
642        let _read_guard = ctx.read();
643        drop(_read_guard);
644        let _write_guard = ctx.write();
645        drop(_write_guard);
646    }
647
648    #[test]
649    fn test_shared_context_ui_bindable_integration() {
650        let ctx = SharedContext::new(TestState {
651            counter: 123,
652            name: "bindable".to_string(),
653        });
654
655        // Test UiBindable through the SharedContext
656        let guard = ctx.read();
657        assert_eq!(
658            guard.get_field(&["counter"]),
659            Some(BindingValue::Integer(123))
660        );
661        assert_eq!(
662            guard.get_field(&["name"]),
663            Some(BindingValue::String("bindable".to_string()))
664        );
665        assert_eq!(guard.get_field(&["nonexistent"]), None);
666    }
667}