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}