dampen_core/state/
mod.rs

1//! Application state container for Dampen UI views.
2//!
3//! This module provides the [`AppState`] struct that combines a parsed UI document
4//! with application state and event handlers into a cohesive structure.
5//!
6//! # Overview
7//!
8//! `AppState<M, S>` is a generic container where:
9//! - `document`: The parsed [`DampenDocument`](crate::ir::DampenDocument) (mandatory)
10//! - `model`: Application state model implementing [`UiBindable`](crate::binding::UiBindable) (optional, defaults to `()`)
11//! - `handler_registry`: Event handler registry (optional, defaults to empty)
12//! - `shared_context`: Optional reference to shared state across views (defaults to `None`)
13//!
14//! # Examples
15//!
16//! Basic usage with document only:
17//!
18//! ```rust,ignore
19//! use dampen_core::{parse, AppState};
20//!
21//! let xml = r#"<column><text value="Hello!" /></column>"#;
22//! let document = parse(xml).unwrap();
23//! let state = AppState::new(document);
24//! ```
25//!
26//! With a custom model:
27//!
28//! ```rust,ignore
29//! use dampen_core::{parse, AppState};
30//! use dampen_macros::UiModel;
31//!
32//! #[derive(UiModel, Default)]
33//! struct MyModel {
34//!     count: i32,
35//! }
36//!
37//! let xml = r#"<column><text value="Hello!" /></column>"#;
38//! let document = parse(xml).unwrap();
39//! let state = AppState::with_model(document, MyModel { count: 0 });
40//! ```
41//!
42//! With shared state for inter-window communication:
43//!
44//! ```rust,ignore
45//! use dampen_core::{parse, AppState, SharedContext};
46//! use dampen_macros::UiModel;
47//!
48//! #[derive(UiModel, Default)]
49//! struct MyModel { count: i32 }
50//!
51//! #[derive(UiModel, Default, Clone)]
52//! struct SharedState { theme: String }
53//!
54//! let xml = r#"<column><text value="Hello!" /></column>"#;
55//! let document = parse(xml).unwrap();
56//! let shared = SharedContext::new(SharedState::default());
57//! let state = AppState::with_shared(document, MyModel::default(), HandlerRegistry::new(), shared);
58//! ```
59//!
60//! # See Also
61//!
62//! - [`DampenDocument`](crate::ir::DampenDocument) - The parsed UI document
63//! - [`HandlerRegistry`](crate::handler::HandlerRegistry) - Event handler registry
64//! - [`UiBindable`](crate::binding::UiBindable) - Trait for bindable models
65//! - [`SharedContext`](crate::shared::SharedContext) - Shared state container
66
67mod theme_context;
68
69pub use theme_context::ThemeContext;
70
71use std::marker::PhantomData;
72use std::sync::{RwLockReadGuard, RwLockWriteGuard};
73
74use crate::shared::SharedContext;
75use crate::{binding::UiBindable, handler::HandlerRegistry, ir::DampenDocument};
76
77/// Application state container for a Dampen UI view.
78///
79/// This struct combines the parsed UI document with application state and event handlers.
80/// It is the central state structure used throughout Dampen applications.
81///
82/// # Type Parameters
83///
84/// * `M` - The local model type implementing [`UiBindable`](crate::binding::UiBindable). Defaults to unit type `()`.
85/// * `S` - The shared state type implementing [`UiBindable`](crate::binding::UiBindable) + `Send + Sync`. Defaults to unit type `()`.
86///
87/// # Backward Compatibility
88///
89/// For applications not using shared state, `S` defaults to `()` and
90/// `shared_context` is `None`. All existing code continues to work unchanged.
91///
92/// # Fields
93///
94/// * `document` - The parsed UI document containing widget tree and themes
95/// * `model` - Application state model for data bindings
96/// * `handler_registry` - Registry of event handlers for UI interactions
97/// * `shared_context` - Optional reference to shared state across views
98/// * `theme_context` - Optional theme context for theming support
99#[derive(Debug, Clone)]
100pub struct AppState<M: UiBindable = (), S: UiBindable + Send + Sync + 'static = ()> {
101    /// The parsed UI document containing widget tree and themes.
102    pub document: DampenDocument,
103
104    /// Application state model for data bindings.
105    /// Generic over `UiBindable` for type-safe field access.
106    pub model: M,
107
108    /// Registry of event handlers for UI interactions.
109    pub handler_registry: HandlerRegistry,
110
111    /// Optional reference to shared context for inter-window communication.
112    pub shared_context: Option<SharedContext<S>>,
113
114    /// Optional theme context for theming support.
115    /// None when no theme.dampen file is present.
116    pub theme_context: Option<ThemeContext>,
117
118    /// Type marker to capture the generic parameters.
119    _marker: PhantomData<(M, S)>,
120}
121
122// ============================================
123// Constructors for backward compatibility (M only, S = ())
124// ============================================
125
126impl<M: UiBindable> AppState<M, ()> {
127    /// Creates a new AppState with default model and empty handler registry.
128    ///
129    /// This is the simplest constructor for static UIs that don't use data binding
130    /// or shared state.
131    ///
132    /// # Examples
133    ///
134    /// ```rust,ignore
135    /// use dampen_core::{parse, AppState};
136    ///
137    /// let xml = r#"<column><text value="Hello!" /></column>"#;
138    /// let document = parse(xml).unwrap();
139    /// let state = AppState::new(document);
140    /// ```
141    pub fn new(document: DampenDocument) -> Self
142    where
143        M: Default,
144    {
145        Self {
146            document,
147            model: M::default(),
148            handler_registry: HandlerRegistry::default(),
149            shared_context: None,
150            theme_context: None,
151            _marker: PhantomData,
152        }
153    }
154
155    /// Creates an AppState with a custom model and default handler registry.
156    ///
157    /// # Examples
158    ///
159    /// ```rust,ignore
160    /// use dampen_core::{parse, AppState};
161    /// use dampen_macros::UiModel;
162    ///
163    /// #[derive(UiModel, Default)]
164    /// struct MyModel {
165    ///     count: i32,
166    /// }
167    ///
168    /// let xml = r#"<column><text value="Hello!" /></column>"#;
169    /// let document = parse(xml).unwrap();
170    /// let model = MyModel { count: 42 };
171    /// let state = AppState::with_model(document, model);
172    /// ```
173    pub fn with_model(document: DampenDocument, model: M) -> Self {
174        Self {
175            document,
176            model,
177            handler_registry: HandlerRegistry::default(),
178            shared_context: None,
179            theme_context: None,
180            _marker: PhantomData,
181        }
182    }
183
184    /// Creates an AppState with a custom handler registry and default model.
185    ///
186    /// # Examples
187    ///
188    /// ```rust,ignore
189    /// use dampen_core::{parse, AppState, HandlerRegistry};
190    ///
191    /// let xml = r#"<column><text value="Hello!" /></column>"#;
192    /// let document = parse(xml).unwrap();
193    /// let mut registry = HandlerRegistry::new();
194    /// registry.register_simple("greet", |_model| {
195    ///     println!("Button clicked!");
196    /// });
197    /// let state = AppState::with_handlers(document, registry);
198    /// ```
199    pub fn with_handlers(document: DampenDocument, handler_registry: HandlerRegistry) -> Self
200    where
201        M: Default,
202    {
203        Self {
204            document,
205            model: M::default(),
206            handler_registry,
207            shared_context: None,
208            theme_context: None,
209            _marker: PhantomData,
210        }
211    }
212
213    /// Creates an AppState with custom model and handler registry.
214    ///
215    /// This is the most flexible constructor for apps without shared state,
216    /// allowing you to specify all components of the application state.
217    /// Useful for hot-reload scenarios where both model and handlers need to be specified.
218    ///
219    /// # Examples
220    ///
221    /// ```rust,ignore
222    /// use dampen_core::{parse, AppState, HandlerRegistry};
223    /// use dampen_macros::UiModel;
224    ///
225    /// #[derive(UiModel, Default)]
226    /// struct MyModel {
227    ///     count: i32,
228    /// }
229    ///
230    /// let xml = r#"<column><text value="Hello!" /></column>"#;
231    /// let document = parse(xml).unwrap();
232    /// let model = MyModel { count: 42 };
233    /// let mut registry = HandlerRegistry::new();
234    /// registry.register_simple("increment", |model| {
235    ///     let model = model.downcast_mut::<MyModel>().unwrap();
236    ///     model.count += 1;
237    /// });
238    /// let state = AppState::with_all(document, model, registry);
239    /// ```
240    pub fn with_all(document: DampenDocument, model: M, handler_registry: HandlerRegistry) -> Self {
241        Self {
242            document,
243            model,
244            handler_registry,
245            shared_context: None,
246            theme_context: None,
247            _marker: PhantomData,
248        }
249    }
250}
251
252// ============================================
253// Constructors and methods for shared state (M and S)
254// ============================================
255
256impl<M: UiBindable, S: UiBindable + Send + Sync + 'static> AppState<M, S> {
257    /// Creates an AppState with shared context for inter-window communication.
258    ///
259    /// This constructor is used when your application needs to share state
260    /// across multiple views.
261    ///
262    /// # Arguments
263    ///
264    /// * `document` - Parsed UI document
265    /// * `model` - Local model for this view
266    /// * `handler_registry` - Event handlers for this view
267    /// * `shared_context` - Shared state accessible from all views
268    ///
269    /// # Examples
270    ///
271    /// ```rust,ignore
272    /// use dampen_core::{parse, AppState, SharedContext, HandlerRegistry};
273    /// use dampen_macros::UiModel;
274    ///
275    /// #[derive(UiModel, Default)]
276    /// struct MyModel { count: i32 }
277    ///
278    /// #[derive(UiModel, Default, Clone)]
279    /// struct SharedState { theme: String }
280    ///
281    /// let xml = r#"<column><text value="{shared.theme}" /></column>"#;
282    /// let document = parse(xml).unwrap();
283    /// let shared = SharedContext::new(SharedState { theme: "dark".to_string() });
284    ///
285    /// let state = AppState::with_shared(
286    ///     document,
287    ///     MyModel::default(),
288    ///     HandlerRegistry::new(),
289    ///     shared,
290    /// );
291    /// ```
292    pub fn with_shared(
293        document: DampenDocument,
294        model: M,
295        handler_registry: HandlerRegistry,
296        shared_context: SharedContext<S>,
297    ) -> Self {
298        Self {
299            document,
300            model,
301            handler_registry,
302            shared_context: Some(shared_context),
303            theme_context: None,
304            _marker: PhantomData,
305        }
306    }
307
308    /// Get read access to shared state (if configured).
309    ///
310    /// Returns `None` if this AppState was created without shared context.
311    ///
312    /// # Examples
313    ///
314    /// ```rust,ignore
315    /// if let Some(guard) = state.shared() {
316    ///     println!("Current theme: {}", guard.theme);
317    /// }
318    /// ```
319    pub fn shared(&self) -> Option<RwLockReadGuard<'_, S>> {
320        self.shared_context.as_ref().map(|ctx| ctx.read())
321    }
322
323    /// Get write access to shared state (if configured).
324    ///
325    /// Returns `None` if this AppState was created without shared context.
326    ///
327    /// # Examples
328    ///
329    /// ```rust,ignore
330    /// if let Some(mut guard) = state.shared_mut() {
331    ///     guard.theme = "light".to_string();
332    /// }
333    /// ```
334    pub fn shared_mut(&self) -> Option<RwLockWriteGuard<'_, S>> {
335        self.shared_context.as_ref().map(|ctx| ctx.write())
336    }
337
338    /// Check if this AppState has shared context configured.
339    ///
340    /// # Examples
341    ///
342    /// ```rust,ignore
343    /// if state.has_shared() {
344    ///     // Use shared state features
345    /// }
346    /// ```
347    pub fn has_shared(&self) -> bool {
348        self.shared_context.is_some()
349    }
350
351    /// Hot-reload: updates the UI document while preserving the model, handlers, and shared context.
352    ///
353    /// This method is designed for development mode hot-reload scenarios where the UI
354    /// definition (XML) changes but the application state (model and shared state)
355    /// should be preserved.
356    ///
357    /// # Examples
358    ///
359    /// ```rust,ignore
360    /// use dampen_core::{parse, AppState};
361    /// use dampen_macros::UiModel;
362    ///
363    /// #[derive(UiModel, Default)]
364    /// struct MyModel {
365    ///     count: i32,
366    /// }
367    ///
368    /// let xml_v1 = r#"<column><text value="Old UI" /></column>"#;
369    /// let document_v1 = parse(xml_v1).unwrap();
370    /// let mut state = AppState::with_model(document_v1, MyModel { count: 42 });
371    ///
372    /// // Later, the UI file changes...
373    /// let xml_v2 = r#"<column><text value="New UI" /></column>"#;
374    /// let document_v2 = parse(xml_v2).unwrap();
375    /// state.hot_reload(document_v2);
376    ///
377    /// // Model state (count: 42) is preserved
378    /// assert_eq!(state.model.count, 42);
379    /// ```
380    pub fn hot_reload(&mut self, new_document: DampenDocument) {
381        self.document = new_document;
382    }
383
384    /// Set the theme context for this AppState.
385    ///
386    /// This is used by the application initialization code to load themes
387    /// from theme.dampen files and apply them to the application.
388    ///
389    /// # Arguments
390    ///
391    /// * `theme_context` - The theme context to use for this application state
392    pub fn set_theme_context(&mut self, theme_context: ThemeContext) {
393        self.theme_context = Some(theme_context);
394    }
395
396    /// Get a reference to the theme context if available.
397    ///
398    /// # Returns
399    ///
400    /// Reference to the theme context, or None if no theme is loaded
401    pub fn theme_context(&self) -> Option<&ThemeContext> {
402        self.theme_context.as_ref()
403    }
404
405    /// Get a mutable reference to the theme context if available.
406    ///
407    /// # Returns
408    ///
409    /// Mutable reference to the theme context, or None if no theme is loaded
410    pub fn theme_context_mut(&mut self) -> Option<&mut ThemeContext> {
411        self.theme_context.as_mut()
412    }
413}