dampen_core/codegen/
status_mapping.rs

1//! Status mapping code generation for widget state styling
2//!
3//! This module generates Rust code that maps Iced widget-specific status enums
4//! to Dampen's unified WidgetState enum. This enables state-aware styling in
5//! codegen mode (matching the behavior of interpreted mode).
6//!
7//! # Architecture
8//!
9//! The generated code mirrors the runtime status mapping logic from
10//! `dampen-iced::style_mapping`, but produces compile-time Rust code instead
11//! of runtime function calls.
12//!
13//! # Example Generated Code
14//!
15//! For a button with hover styling:
16//! ```text
17//! let style = move |_theme: &Theme, status: iced::widget::button::Status| {
18//!     use iced::widget::button::Status;
19//!     let widget_state = match status {
20//!         Status::Active => None,
21//!         Status::Hovered => Some(dampen_core::ir::WidgetState::Hover),
22//!         Status::Pressed => Some(dampen_core::ir::WidgetState::Active),
23//!         Status::Disabled => Some(dampen_core::ir::WidgetState::Disabled),
24//!     };
25//!
26//!     if let Some(state) = widget_state {
27//!         // Apply state-specific styling
28//!     } else {
29//!         // Apply base styling
30//!     }
31//! };
32//! button.style(style)
33//! ```
34
35use proc_macro2::TokenStream;
36use quote::quote;
37
38use crate::WidgetKind;
39
40/// Generate status mapping code for a specific widget kind
41///
42/// Returns a TokenStream that generates a `match` expression mapping
43/// the widget's Iced status enum to `Option<WidgetState>`.
44///
45/// Returns `None` for the base/default state, and `Some(WidgetState::X)`
46/// for hover/focus/active/disabled states.
47///
48/// # Arguments
49/// * `widget_kind` - The type of widget to generate mapping for
50/// * `status_ident` - The identifier for the status variable (usually `status`)
51///
52/// # Returns
53/// * `Some(TokenStream)` - Generated match expression for widgets with state support
54/// * `None` - For widgets that don't support state styling (e.g., Text, Space)
55///
56/// # Example
57/// ```
58/// use dampen_core::codegen::status_mapping::generate_status_mapping;
59/// use dampen_core::WidgetKind;
60/// use quote::format_ident;
61///
62/// let status_ident = format_ident!("status");
63/// let mapping = generate_status_mapping(&WidgetKind::Button, &status_ident);
64/// assert!(mapping.is_some());
65/// ```
66pub fn generate_status_mapping(
67    widget_kind: &WidgetKind,
68    status_ident: &syn::Ident,
69) -> Option<TokenStream> {
70    match widget_kind {
71        WidgetKind::Button => Some(generate_button_status_mapping(status_ident)),
72        WidgetKind::TextInput => Some(generate_text_input_status_mapping(status_ident)),
73        WidgetKind::Checkbox => Some(generate_checkbox_status_mapping(status_ident)),
74        WidgetKind::Radio => Some(generate_radio_status_mapping(status_ident)),
75        WidgetKind::Toggler => Some(generate_toggler_status_mapping(status_ident)),
76        WidgetKind::Slider => Some(generate_slider_status_mapping(status_ident)),
77        WidgetKind::PickList => Some(generate_picklist_status_mapping(status_ident)),
78        WidgetKind::ComboBox => Some(generate_combo_box_status_mapping(status_ident)),
79        _ => None, // Widgets without state support
80    }
81}
82
83/// Generate button status mapping code
84///
85/// Maps `iced::widget::button::Status` to `Option<WidgetState>`:
86/// - `Active` → `None` (base state)
87/// - `Hovered` → `Some(Hover)`
88/// - `Pressed` → `Some(Active)`
89/// - `Disabled` → `Some(Disabled)`
90fn generate_button_status_mapping(status_ident: &syn::Ident) -> TokenStream {
91    quote! {
92        {
93            use iced::widget::button::Status;
94            match #status_ident {
95                Status::Active => None,
96                Status::Hovered => Some(dampen_core::ir::WidgetState::Hover),
97                Status::Pressed => Some(dampen_core::ir::WidgetState::Active),
98                Status::Disabled => Some(dampen_core::ir::WidgetState::Disabled),
99            }
100        }
101    }
102}
103
104/// Generate text input status mapping code
105///
106/// Maps `iced::widget::text_input::Status` to `Option<WidgetState>`:
107/// - `Active` → `None` (base state)
108/// - `Hovered` → `Some(Hover)`
109/// - `Focused { .. }` → `Some(Focus)`
110/// - `Disabled` → `Some(Disabled)`
111fn generate_text_input_status_mapping(status_ident: &syn::Ident) -> TokenStream {
112    quote! {
113        {
114            use iced::widget::text_input::Status;
115            match #status_ident {
116                Status::Active => None,
117                Status::Hovered => Some(dampen_core::ir::WidgetState::Hover),
118                Status::Focused { .. } => Some(dampen_core::ir::WidgetState::Focus),
119                Status::Disabled => Some(dampen_core::ir::WidgetState::Disabled),
120            }
121        }
122    }
123}
124
125/// Generate checkbox status mapping code
126///
127/// Maps `iced::widget::checkbox::Status` to `Option<WidgetState>`:
128/// - `Active { .. }` → `None` (base state)
129/// - `Hovered { .. }` → `Some(Hover)`
130/// - `Disabled { .. }` → `Some(Disabled)`
131///
132/// Note: The `is_checked` field in each variant is ignored for state mapping.
133fn generate_checkbox_status_mapping(status_ident: &syn::Ident) -> TokenStream {
134    quote! {
135        {
136            use iced::widget::checkbox::Status;
137            match #status_ident {
138                Status::Active { .. } => None,
139                Status::Hovered { .. } => Some(dampen_core::ir::WidgetState::Hover),
140                Status::Disabled { .. } => Some(dampen_core::ir::WidgetState::Disabled),
141            }
142        }
143    }
144}
145
146/// Generate radio button status mapping code
147///
148/// Maps `iced::widget::radio::Status` to `Option<WidgetState>`:
149/// - `Active { .. }` → `None` (base state)
150/// - `Hovered { .. }` → `Some(Hover)`
151///
152/// Note: Radio buttons don't have a Disabled status in Iced 0.14.
153/// The `is_selected` field is ignored for state mapping.
154fn generate_radio_status_mapping(status_ident: &syn::Ident) -> TokenStream {
155    quote! {
156        {
157            use iced::widget::radio::Status;
158            match #status_ident {
159                Status::Active { .. } => None,
160                Status::Hovered { .. } => Some(dampen_core::ir::WidgetState::Hover),
161            }
162        }
163    }
164}
165
166/// Generate toggler status mapping code
167///
168/// Maps `iced::widget::toggler::Status` to `Option<WidgetState>`:
169/// - `Active { .. }` → `None` (base state)
170/// - `Hovered { .. }` → `Some(Hover)`
171/// - `Disabled { .. }` → `Some(Disabled)`
172///
173/// Note: The `is_toggled` field is ignored for state mapping.
174fn generate_toggler_status_mapping(status_ident: &syn::Ident) -> TokenStream {
175    quote! {
176        {
177            use iced::widget::toggler::Status;
178            match #status_ident {
179                Status::Active { .. } => None,
180                Status::Hovered { .. } => Some(dampen_core::ir::WidgetState::Hover),
181                Status::Disabled { .. } => Some(dampen_core::ir::WidgetState::Disabled),
182            }
183        }
184    }
185}
186
187/// Generate slider status mapping code
188///
189/// Maps `iced::widget::slider::Status` to `Option<WidgetState>`:
190/// - `Active` → `None` (base state)
191/// - `Hovered` → `Some(Hover)`
192/// - `Dragged` → `Some(Active)`
193///
194/// Note: Iced 0.14's slider::Status does NOT have a Disabled variant.
195/// Disabled state must be checked separately via the `disabled` attribute.
196fn generate_slider_status_mapping(status_ident: &syn::Ident) -> TokenStream {
197    quote! {
198        {
199            use iced::widget::slider::Status;
200            match #status_ident {
201                Status::Active => None,
202                Status::Hovered => Some(dampen_core::ir::WidgetState::Hover),
203                Status::Dragged => Some(dampen_core::ir::WidgetState::Active),
204            }
205        }
206    }
207}
208
209/// Generate pick list status mapping code
210///
211/// Maps `iced::widget::pick_list::Status` to `Option<WidgetState>`:
212/// - `Active` → `None` (base state)
213/// - `Hovered` → `Some(Hover)`
214/// - `Opened { .. }` → `Some(Focus)` (dropdown menu is open)
215fn generate_picklist_status_mapping(status_ident: &syn::Ident) -> TokenStream {
216    quote! {
217        {
218            use iced::widget::pick_list::Status;
219            match #status_ident {
220                Status::Active => None,
221                Status::Hovered => Some(dampen_core::ir::WidgetState::Hover),
222                Status::Opened { .. } => Some(dampen_core::ir::WidgetState::Focus),
223            }
224        }
225    }
226}
227
228/// Generate combo box status mapping code
229///
230/// ComboBox uses `text_input::Status`, so we reuse the text input mapping.
231fn generate_combo_box_status_mapping(status_ident: &syn::Ident) -> TokenStream {
232    generate_text_input_status_mapping(status_ident)
233}
234
235#[cfg(test)]
236mod tests {
237    use super::*;
238    use quote::format_ident;
239
240    #[test]
241    fn test_button_status_mapping_generated() {
242        let status = format_ident!("status");
243        let mapping = generate_status_mapping(&WidgetKind::Button, &status);
244        assert!(mapping.is_some());
245
246        let code = mapping.unwrap().to_string();
247        // Check for key status variants (format may vary due to quote!)
248        assert!(code.contains("Active"));
249        assert!(code.contains("Hovered"));
250        assert!(code.contains("Pressed"));
251        assert!(code.contains("Disabled"));
252        assert!(code.contains("WidgetState"));
253    }
254
255    #[test]
256    fn test_text_input_status_mapping_generated() {
257        let status = format_ident!("status");
258        let mapping = generate_status_mapping(&WidgetKind::TextInput, &status);
259        assert!(mapping.is_some());
260
261        let code = mapping.unwrap().to_string();
262        assert!(code.contains("Focused"));
263        assert!(code.contains("WidgetState"));
264    }
265
266    #[test]
267    fn test_checkbox_status_mapping_generated() {
268        let status = format_ident!("status");
269        let mapping = generate_status_mapping(&WidgetKind::Checkbox, &status);
270        assert!(mapping.is_some());
271
272        let code = mapping.unwrap().to_string();
273        assert!(code.contains("Active"));
274        assert!(code.contains("Hovered"));
275        assert!(code.contains("WidgetState"));
276    }
277
278    #[test]
279    fn test_unsupported_widget_returns_none() {
280        let status = format_ident!("status");
281        let mapping = generate_status_mapping(&WidgetKind::Text, &status);
282        assert!(mapping.is_none());
283    }
284
285    #[test]
286    fn test_slider_status_mapping_generated() {
287        let status = format_ident!("status");
288        let mapping = generate_status_mapping(&WidgetKind::Slider, &status);
289        assert!(mapping.is_some());
290
291        let code = mapping.unwrap().to_string();
292        assert!(code.contains("Dragged"));
293        assert!(code.contains("WidgetState"));
294    }
295
296    #[test]
297    fn test_combo_box_uses_text_input_mapping() {
298        let status = format_ident!("status");
299        let combo_mapping = generate_status_mapping(&WidgetKind::ComboBox, &status);
300        let text_mapping = generate_status_mapping(&WidgetKind::TextInput, &status);
301
302        assert!(combo_mapping.is_some());
303        assert!(text_mapping.is_some());
304
305        // ComboBox should generate the same code as TextInput
306        let combo_code = combo_mapping.unwrap().to_string();
307        let text_code = text_mapping.unwrap().to_string();
308        assert_eq!(combo_code, text_code);
309    }
310}