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}