dampen_core/ir/
node.rs

1use crate::ir::layout::{Breakpoint, LayoutConstraints};
2use crate::ir::span::Span;
3use crate::ir::style::StyleProperties;
4use crate::ir::theme::WidgetState;
5use std::collections::HashMap;
6
7/// A node in the widget tree
8#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
9pub struct WidgetNode {
10    pub kind: WidgetKind,
11    pub id: Option<String>,
12    pub attributes: HashMap<String, AttributeValue>,
13    pub events: Vec<EventBinding>,
14    pub children: Vec<WidgetNode>,
15    pub span: Span,
16
17    // Styling extensions
18    pub style: Option<StyleProperties>,
19    pub layout: Option<LayoutConstraints>,
20    pub theme_ref: Option<AttributeValue>,
21    pub classes: Vec<String>,
22    pub breakpoint_attributes: HashMap<Breakpoint, HashMap<String, AttributeValue>>,
23    /// State-specific styles from inline attributes (e.g., hover:background="#ff0000")
24    #[serde(default)]
25    pub inline_state_variants: HashMap<WidgetState, StyleProperties>,
26}
27
28/// Enumeration of all supported widget types
29#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default)]
30pub enum WidgetKind {
31    #[default]
32    Column,
33    Row,
34    Container,
35    Scrollable,
36    Stack,
37    Text,
38    Image,
39    Svg,
40    Button,
41    TextInput,
42    Checkbox,
43    Slider,
44    PickList,
45    Toggler,
46    Space,
47    Rule,
48    Radio,
49    // Advanced widgets
50    ComboBox,
51    ProgressBar,
52    Tooltip,
53    Grid,
54    Canvas,
55    Float,
56    // Control flow
57    For,
58    Custom(String),
59}
60
61/// A value that can be either static or dynamically bound
62#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
63pub enum AttributeValue {
64    Static(String),
65    Binding(crate::expr::BindingExpr),
66    Interpolated(Vec<InterpolatedPart>),
67}
68
69impl Default for AttributeValue {
70    fn default() -> Self {
71        AttributeValue::Static(String::new())
72    }
73}
74
75/// Part of an interpolated string
76#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
77pub enum InterpolatedPart {
78    Literal(String),
79    Binding(crate::expr::BindingExpr),
80}
81
82impl Default for InterpolatedPart {
83    fn default() -> Self {
84        InterpolatedPart::Literal(String::new())
85    }
86}
87
88/// Attribute structures for advanced widgets
89#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
90pub struct ComboBoxAttributes {
91    pub options: Vec<String>,
92    pub selected: Option<crate::expr::BindingExpr>,
93    pub placeholder: Option<String>,
94    pub on_select: Option<String>,
95}
96
97#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
98pub struct PickListAttributes {
99    pub options: Vec<String>,
100    pub selected: Option<crate::expr::BindingExpr>,
101    pub placeholder: Option<String>,
102    pub on_select: Option<String>,
103}
104
105#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
106pub struct CanvasAttributes {
107    pub width: f32,
108    pub height: f32,
109    pub program: Option<crate::expr::BindingExpr>,
110    pub on_click: Option<String>,
111}
112
113#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
114pub struct ProgressBarAttributes {
115    pub min: Option<f32>,
116    pub max: Option<f32>,
117    pub value: crate::expr::BindingExpr,
118    pub style: Option<ProgressBarStyle>,
119}
120
121#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
122pub enum ProgressBarStyle {
123    Primary,
124    Success,
125    Warning,
126    Danger,
127    Secondary,
128}
129
130#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
131pub struct TooltipAttributes {
132    pub message: String,
133    pub position: Option<TooltipPosition>,
134    pub delay: Option<u64>,
135}
136
137#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
138pub enum TooltipPosition {
139    FollowCursor,
140    Top,
141    Bottom,
142    Left,
143    Right,
144}
145
146#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
147pub struct GridAttributes {
148    pub columns: u32,
149    pub spacing: Option<f32>,
150    pub padding: Option<f32>,
151}
152
153#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
154pub struct FloatAttributes {
155    pub position: Option<FloatPosition>,
156    pub offset_x: Option<f32>,
157    pub offset_y: Option<f32>,
158    pub z_index: Option<u32>,
159}
160
161#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
162pub enum FloatPosition {
163    TopLeft,
164    TopRight,
165    BottomLeft,
166    BottomRight,
167}
168
169/// An event binding from XML to a Rust handler
170#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
171pub struct EventBinding {
172    pub event: EventKind,
173    pub handler: String,
174    /// Optional parameter expression (e.g., for on_click="delete:{item.id}")
175    pub param: Option<crate::expr::BindingExpr>,
176    pub span: Span,
177}
178
179/// Supported event types
180#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default)]
181pub enum EventKind {
182    #[default]
183    Click,
184    Press,
185    Release,
186    Change,
187    Input,
188    Submit,
189    Select,
190    Toggle,
191    Scroll,
192}
193
194impl WidgetKind {
195    /// Returns the minimum schema version required for this widget type.
196    ///
197    /// This method provides infrastructure for version-gating widgets in future releases.
198    /// Currently, all widgets return version 1.0 as they are part of the initial release.
199    ///
200    /// # Future Usage
201    ///
202    /// When new widgets are added in future schema versions (e.g., 1.1, 1.2), this method
203    /// will be updated to return the appropriate minimum version for those widgets.
204    /// The parser can then validate that documents declaring older schema versions
205    /// do not use widgets that were introduced in later versions.
206    ///
207    /// # Examples
208    ///
209    /// ```
210    /// use dampen_core::{WidgetKind, SchemaVersion};
211    ///
212    /// let column = WidgetKind::Column;
213    /// assert_eq!(column.minimum_version(), SchemaVersion { major: 1, minor: 0 });
214    /// ```
215    ///
216    /// # Returns
217    ///
218    /// The minimum `SchemaVersion` required to use this widget type.
219    pub fn minimum_version(&self) -> crate::ir::SchemaVersion {
220        // Canvas is a v1.1 widget (experimental, not fully functional)
221        // All other widgets are part of v1.0
222        match self {
223            WidgetKind::Canvas => crate::ir::SchemaVersion { major: 1, minor: 1 },
224            _ => crate::ir::SchemaVersion { major: 1, minor: 0 },
225        }
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use super::*;
232    use crate::ir::style::StyleProperties;
233    use crate::ir::theme::WidgetState;
234
235    #[test]
236    fn test_widget_node_default_has_empty_inline_state_variants() {
237        let node = WidgetNode::default();
238        assert!(node.inline_state_variants.is_empty());
239    }
240
241    #[test]
242    fn test_widget_node_inline_state_variants_serialization() {
243        let mut node = WidgetNode {
244            kind: WidgetKind::Button,
245            id: Some("test-button".to_string()),
246            attributes: Default::default(),
247            events: Default::default(),
248            children: Default::default(),
249            span: Default::default(),
250            style: Default::default(),
251            layout: Default::default(),
252            theme_ref: Default::default(),
253            classes: Default::default(),
254            breakpoint_attributes: Default::default(),
255            inline_state_variants: Default::default(),
256        };
257
258        // Add state variant
259        node.inline_state_variants.insert(
260            WidgetState::Hover,
261            StyleProperties {
262                opacity: Some(0.8),
263                ..Default::default()
264            },
265        );
266
267        // Serialize and deserialize
268        let json = serde_json::to_string(&node).expect("Should serialize");
269        let deserialized: WidgetNode = serde_json::from_str(&json).expect("Should deserialize");
270
271        // Verify field preserved
272        assert_eq!(deserialized.inline_state_variants.len(), 1);
273        assert!(
274            deserialized
275                .inline_state_variants
276                .contains_key(&WidgetState::Hover)
277        );
278    }
279
280    #[test]
281    fn test_widget_node_inline_state_variants_multiple_states() {
282        let mut node = WidgetNode::default();
283
284        node.inline_state_variants.insert(
285            WidgetState::Hover,
286            StyleProperties {
287                opacity: Some(0.9),
288                ..Default::default()
289            },
290        );
291
292        node.inline_state_variants.insert(
293            WidgetState::Active,
294            StyleProperties {
295                opacity: Some(0.7),
296                ..Default::default()
297            },
298        );
299
300        node.inline_state_variants.insert(
301            WidgetState::Disabled,
302            StyleProperties {
303                opacity: Some(0.5),
304                ..Default::default()
305            },
306        );
307
308        assert_eq!(node.inline_state_variants.len(), 3);
309        assert!(node.inline_state_variants.contains_key(&WidgetState::Hover));
310        assert!(
311            node.inline_state_variants
312                .contains_key(&WidgetState::Active)
313        );
314        assert!(
315            node.inline_state_variants
316                .contains_key(&WidgetState::Disabled)
317        );
318    }
319}