dampen_core/codegen/
subscription.rs

1//! Subscription code generation for production builds
2//!
3//! This module generates subscription code for system theme detection
4//! and other event subscriptions that need to work in production builds.
5
6use proc_macro2::TokenStream;
7use quote::quote;
8
9use crate::ir::theme::ThemeDocument;
10
11/// Configuration for subscription code generation
12#[derive(Debug, Clone)]
13pub struct SubscriptionConfig {
14    /// Whether to generate system theme subscription
15    pub system_theme: bool,
16    /// Name of the message enum
17    pub message_name: String,
18    /// Variant name for system theme changed message (e.g., "SystemThemeChanged")
19    pub system_theme_variant: Option<String>,
20}
21
22impl Default for SubscriptionConfig {
23    fn default() -> Self {
24        Self {
25            system_theme: false,
26            message_name: "Message".to_string(),
27            system_theme_variant: None,
28        }
29    }
30}
31
32impl SubscriptionConfig {
33    /// Create subscription config from a theme document
34    ///
35    /// If the theme document has `follow_system: true`, system theme subscription
36    /// will be enabled.
37    pub fn from_theme_document(theme_doc: Option<&ThemeDocument>, message_name: &str) -> Self {
38        let system_theme = theme_doc.map(|doc| doc.follow_system).unwrap_or(false);
39
40        Self {
41            system_theme,
42            message_name: message_name.to_string(),
43            system_theme_variant: if system_theme {
44                Some("SystemThemeChanged".to_string())
45            } else {
46                None
47            },
48        }
49    }
50
51    /// Set the system theme variant name
52    pub fn with_system_theme_variant(mut self, variant: impl Into<String>) -> Self {
53        self.system_theme_variant = Some(variant.into());
54        self.system_theme = true;
55        self
56    }
57}
58
59/// Generate the subscription function for the application
60///
61/// This generates a `subscription_model()` function that creates Iced subscriptions
62/// for system theme changes when `follow_system` is enabled in the theme configuration.
63///
64/// # Arguments
65///
66/// * `config` - Subscription configuration
67///
68/// # Returns
69///
70/// TokenStream containing the subscription function
71///
72/// # Example Output
73///
74/// ```rust,ignore
75/// pub fn subscription_model() -> iced::Subscription<Message> {
76///     if app_follows_system() {
77///         dampen_iced::watch_system_theme()
78///             .map(Message::SystemThemeChanged)
79///     } else {
80///         iced::Subscription::none()
81///     }
82/// }
83/// ```
84pub fn generate_subscription_function(config: &SubscriptionConfig) -> TokenStream {
85    let message_ident = syn::Ident::new(&config.message_name, proc_macro2::Span::call_site());
86
87    if let Some(ref variant_name) = config.system_theme_variant {
88        let variant_ident = syn::Ident::new(variant_name, proc_macro2::Span::call_site());
89
90        quote! {
91            /// Get the application subscription for system events
92            ///
93            /// This function returns a subscription that monitors system theme changes
94            /// when `follow_system` is enabled in the theme configuration.
95            pub fn subscription_model() -> iced::Subscription<#message_ident> {
96                if app_follows_system() {
97                    dampen_iced::watch_system_theme()
98                        .map(#message_ident::#variant_ident)
99                } else {
100                    iced::Subscription::none()
101                }
102            }
103        }
104    } else {
105        quote! {
106            /// Get the application subscription for system events
107            ///
108            /// Returns no subscription when system theme following is disabled.
109            pub fn subscription_model() -> iced::Subscription<#message_ident> {
110                iced::Subscription::none()
111            }
112        }
113    }
114}
115
116/// Generate the SystemThemeChanged variant for the Message enum
117///
118/// This should be called when generating the Message enum to add the
119/// SystemThemeChanged variant if system theme following is enabled.
120///
121/// # Arguments
122///
123/// * `config` - Subscription configuration
124///
125/// # Returns
126///
127/// Option containing the TokenStream for the variant, or None if not needed
128pub fn generate_system_theme_variant(config: &SubscriptionConfig) -> Option<TokenStream> {
129    config.system_theme_variant.as_ref().map(|variant_name| {
130        let variant_ident = syn::Ident::new(variant_name, proc_macro2::Span::call_site());
131        quote! {
132            /// System theme changed event
133            #variant_ident(String)
134        }
135    })
136}
137
138/// Generate the update match arm for SystemThemeChanged
139///
140/// This generates the match arm that handles the SystemThemeChanged message
141/// by updating the current theme based on the system preference.
142///
143/// # Arguments
144///
145/// * `config` - Subscription configuration
146///
147/// # Returns
148///
149/// Option containing the TokenStream for the match arm, or None if not needed
150///
151/// # Example Output
152///
153/// ```rust,ignore
154/// Message::SystemThemeChanged(theme_name) => {
155///     // Theme is automatically selected via app_theme_named()
156///     // The application should store the current theme name if needed
157///     iced::Task::none()
158/// }
159/// ```
160pub fn generate_system_theme_update_arm(config: &SubscriptionConfig) -> Option<TokenStream> {
161    let message_ident = syn::Ident::new(&config.message_name, proc_macro2::Span::call_site());
162
163    config.system_theme_variant.as_ref().map(|variant_name| {
164        let variant_ident = syn::Ident::new(variant_name, proc_macro2::Span::call_site());
165
166        quote! {
167            #message_ident::#variant_ident(theme_name) => {
168                // Update current theme based on system preference
169                app_set_current_theme(&theme_name);
170                iced::Task::none()
171            }
172        }
173    })
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179    use crate::ir::theme::{SpacingScale, Theme, ThemeDocument, ThemePalette, Typography};
180    use std::collections::HashMap;
181
182    fn create_test_theme_document(follow_system: bool) -> ThemeDocument {
183        let mut themes = HashMap::new();
184        themes.insert(
185            "light".to_string(),
186            Theme {
187                name: "light".to_string(),
188                palette: ThemePalette::light(),
189                typography: Typography {
190                    font_family: None,
191                    font_size_base: Some(16.0),
192                    font_size_small: Some(12.0),
193                    font_size_large: Some(24.0),
194                    font_weight: crate::ir::theme::FontWeight::Normal,
195                    line_height: Some(1.5),
196                },
197                spacing: SpacingScale { unit: Some(8.0) },
198                base_styles: HashMap::new(),
199                extends: None,
200            },
201        );
202
203        ThemeDocument {
204            themes,
205            default_theme: Some("light".to_string()),
206            follow_system,
207        }
208    }
209
210    #[test]
211    fn test_subscription_config_from_theme_document() {
212        let doc = create_test_theme_document(true);
213        let config = SubscriptionConfig::from_theme_document(Some(&doc), "Message");
214
215        assert!(config.system_theme);
216        assert_eq!(
217            config.system_theme_variant,
218            Some("SystemThemeChanged".to_string())
219        );
220    }
221
222    #[test]
223    fn test_subscription_config_no_follow_system() {
224        let doc = create_test_theme_document(false);
225        let config = SubscriptionConfig::from_theme_document(Some(&doc), "Message");
226
227        assert!(!config.system_theme);
228        assert_eq!(config.system_theme_variant, None);
229    }
230
231    #[test]
232    fn test_generate_subscription_function_with_system_theme() {
233        let config = SubscriptionConfig {
234            system_theme: true,
235            message_name: "Message".to_string(),
236            system_theme_variant: Some("SystemThemeChanged".to_string()),
237        };
238
239        let tokens = generate_subscription_function(&config);
240        let code = tokens.to_string();
241
242        assert!(code.contains("subscription_model"), "code: {}", code);
243        assert!(code.contains("app_follows_system"), "code: {}", code);
244        // quote! generates "dampen_iced :: watch_system_theme" with spaces
245        assert!(code.contains("watch_system_theme"), "code: {}", code);
246        assert!(code.contains("SystemThemeChanged"), "code: {}", code);
247    }
248
249    #[test]
250    fn test_generate_subscription_function_without_system_theme() {
251        let config = SubscriptionConfig::default();
252
253        let tokens = generate_subscription_function(&config);
254        let code = tokens.to_string();
255
256        assert!(code.contains("subscription_model"), "code: {}", code);
257        // quote! generates "Subscription :: none" with spaces
258        assert!(
259            code.contains("Subscription") && code.contains("none"),
260            "code: {}",
261            code
262        );
263        assert!(!code.contains("watch_system_theme"), "code: {}", code);
264    }
265
266    #[test]
267    fn test_generate_system_theme_variant() {
268        let config = SubscriptionConfig {
269            system_theme: true,
270            message_name: "Message".to_string(),
271            system_theme_variant: Some("SystemThemeChanged".to_string()),
272        };
273
274        let tokens = generate_system_theme_variant(&config);
275        assert!(tokens.is_some());
276
277        let code = tokens.unwrap().to_string();
278        assert!(code.contains("SystemThemeChanged"));
279        assert!(code.contains("String"));
280    }
281
282    #[test]
283    fn test_generate_system_theme_update_arm() {
284        let config = SubscriptionConfig {
285            system_theme: true,
286            message_name: "Message".to_string(),
287            system_theme_variant: Some("SystemThemeChanged".to_string()),
288        };
289
290        let tokens = generate_system_theme_update_arm(&config);
291        assert!(tokens.is_some());
292
293        let code = tokens.unwrap().to_string();
294        // quote! generates "Message :: SystemThemeChanged" with spaces
295        assert!(
296            code.contains("Message") && code.contains("SystemThemeChanged"),
297            "code: {}",
298            code
299        );
300        assert!(code.contains("theme_name"), "code: {}", code);
301        // quote! generates "Task :: none" with spaces
302        assert!(
303            code.contains("Task") && code.contains("none"),
304            "code: {}",
305            code
306        );
307    }
308}