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    If,
59    Custom(String),
60}
61
62/// A value that can be either static or dynamically bound
63#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
64pub enum AttributeValue {
65    Static(String),
66    Binding(crate::expr::BindingExpr),
67    Interpolated(Vec<InterpolatedPart>),
68}
69
70impl Default for AttributeValue {
71    fn default() -> Self {
72        AttributeValue::Static(String::new())
73    }
74}
75
76/// Part of an interpolated string
77#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
78pub enum InterpolatedPart {
79    Literal(String),
80    Binding(crate::expr::BindingExpr),
81}
82
83impl Default for InterpolatedPart {
84    fn default() -> Self {
85        InterpolatedPart::Literal(String::new())
86    }
87}
88
89/// Attribute structures for advanced widgets
90#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
91pub struct ComboBoxAttributes {
92    pub options: Vec<String>,
93    pub selected: Option<crate::expr::BindingExpr>,
94    pub placeholder: Option<String>,
95    pub on_select: Option<String>,
96}
97
98#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
99pub struct PickListAttributes {
100    pub options: Vec<String>,
101    pub selected: Option<crate::expr::BindingExpr>,
102    pub placeholder: Option<String>,
103    pub on_select: Option<String>,
104}
105
106#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
107pub struct CanvasAttributes {
108    pub width: f32,
109    pub height: f32,
110    pub program: Option<crate::expr::BindingExpr>,
111    pub on_click: Option<String>,
112}
113
114#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
115pub struct ProgressBarAttributes {
116    pub min: Option<f32>,
117    pub max: Option<f32>,
118    pub value: crate::expr::BindingExpr,
119    pub style: Option<ProgressBarStyle>,
120}
121
122#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
123pub enum ProgressBarStyle {
124    Primary,
125    Success,
126    Warning,
127    Danger,
128    Secondary,
129}
130
131#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
132pub struct TooltipAttributes {
133    pub message: String,
134    pub position: Option<TooltipPosition>,
135    pub delay: Option<u64>,
136}
137
138#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
139pub enum TooltipPosition {
140    FollowCursor,
141    Top,
142    Bottom,
143    Left,
144    Right,
145}
146
147#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
148pub struct GridAttributes {
149    pub columns: u32,
150    pub spacing: Option<f32>,
151    pub padding: Option<f32>,
152}
153
154#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
155pub struct FloatAttributes {
156    pub position: Option<FloatPosition>,
157    pub offset_x: Option<f32>,
158    pub offset_y: Option<f32>,
159    pub z_index: Option<u32>,
160}
161
162#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
163pub enum FloatPosition {
164    TopLeft,
165    TopRight,
166    BottomLeft,
167    BottomRight,
168}
169
170/// An event binding from XML to a Rust handler
171#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
172pub struct EventBinding {
173    pub event: EventKind,
174    pub handler: String,
175    /// Optional parameter expression (e.g., for on_click="delete:{item.id}")
176    pub param: Option<crate::expr::BindingExpr>,
177    pub span: Span,
178}
179
180/// Supported event types
181#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default)]
182pub enum EventKind {
183    #[default]
184    Click,
185    Press,
186    Release,
187    Change,
188    Input,
189    Submit,
190    Select,
191    Toggle,
192    Scroll,
193}
194
195impl WidgetKind {
196    /// Returns a list of all standard widget tag names.
197    pub fn all_standard() -> &'static [&'static str] {
198        &[
199            "column",
200            "row",
201            "container",
202            "scrollable",
203            "stack",
204            "text",
205            "image",
206            "svg",
207            "button",
208            "text_input",
209            "checkbox",
210            "slider",
211            "pick_list",
212            "toggler",
213            "space",
214            "rule",
215            "radio",
216            "combobox",
217            "progress_bar",
218            "tooltip",
219            "grid",
220            "canvas",
221            "float",
222            "for",
223            "if",
224        ]
225    }
226
227    /// Returns true if this is a custom widget.
228    pub fn is_custom(&self) -> bool {
229        matches!(self, WidgetKind::Custom(_))
230    }
231
232    /// Returns the minimum schema version required for this widget type.
233    ///
234    /// This method provides infrastructure for version-gating widgets in future releases.
235    /// Currently, all widgets return version 1.0 as they are part of the initial release.
236    ///
237    /// # Future Usage
238    ///
239    /// When new widgets are added in future schema versions (e.g., 1.1, 1.2), this method
240    /// will be updated to return the appropriate minimum version for those widgets.
241    /// The parser can then validate that documents declaring older schema versions
242    /// do not use widgets that were introduced in later versions.
243    ///
244    /// # Examples
245    ///
246    /// ```
247    /// use dampen_core::{WidgetKind, SchemaVersion};
248    ///
249    /// let column = WidgetKind::Column;
250    /// assert_eq!(column.minimum_version(), SchemaVersion { major: 1, minor: 0 });
251    /// ```
252    ///
253    /// # Returns
254    ///
255    /// The minimum `SchemaVersion` required to use this widget type.
256    pub fn minimum_version(&self) -> crate::ir::SchemaVersion {
257        // Canvas is a v1.1 widget (experimental, not fully functional)
258        // All other widgets are part of v1.0
259        match self {
260            WidgetKind::Canvas => crate::ir::SchemaVersion { major: 1, minor: 1 },
261            _ => crate::ir::SchemaVersion { major: 1, minor: 0 },
262        }
263    }
264
265    /// Returns the validation schema for this widget type.
266    pub fn schema(&self) -> crate::schema::WidgetSchema {
267        crate::schema::get_widget_schema(self)
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274    use crate::ir::style::StyleProperties;
275    use crate::ir::theme::WidgetState;
276
277    #[test]
278    fn test_widget_node_default_has_empty_inline_state_variants() {
279        let node = WidgetNode::default();
280        assert!(node.inline_state_variants.is_empty());
281    }
282
283    #[test]
284    fn test_widget_node_inline_state_variants_serialization() {
285        let mut node = WidgetNode {
286            kind: WidgetKind::Button,
287            id: Some("test-button".to_string()),
288            attributes: Default::default(),
289            events: Default::default(),
290            children: Default::default(),
291            span: Default::default(),
292            style: Default::default(),
293            layout: Default::default(),
294            theme_ref: Default::default(),
295            classes: Default::default(),
296            breakpoint_attributes: Default::default(),
297            inline_state_variants: Default::default(),
298        };
299
300        // Add state variant
301        node.inline_state_variants.insert(
302            WidgetState::Hover,
303            StyleProperties {
304                opacity: Some(0.8),
305                ..Default::default()
306            },
307        );
308
309        // Serialize and deserialize
310        let json = serde_json::to_string(&node).expect("Should serialize");
311        let deserialized: WidgetNode = serde_json::from_str(&json).expect("Should deserialize");
312
313        // Verify field preserved
314        assert_eq!(deserialized.inline_state_variants.len(), 1);
315        assert!(
316            deserialized
317                .inline_state_variants
318                .contains_key(&WidgetState::Hover)
319        );
320    }
321
322    #[test]
323    fn test_widget_node_inline_state_variants_multiple_states() {
324        let mut node = WidgetNode::default();
325
326        node.inline_state_variants.insert(
327            WidgetState::Hover,
328            StyleProperties {
329                opacity: Some(0.9),
330                ..Default::default()
331            },
332        );
333
334        node.inline_state_variants.insert(
335            WidgetState::Active,
336            StyleProperties {
337                opacity: Some(0.7),
338                ..Default::default()
339            },
340        );
341
342        node.inline_state_variants.insert(
343            WidgetState::Disabled,
344            StyleProperties {
345                opacity: Some(0.5),
346                ..Default::default()
347            },
348        );
349
350        assert_eq!(node.inline_state_variants.len(), 3);
351        assert!(node.inline_state_variants.contains_key(&WidgetState::Hover));
352        assert!(
353            node.inline_state_variants
354                .contains_key(&WidgetState::Active)
355        );
356        assert!(
357            node.inline_state_variants
358                .contains_key(&WidgetState::Disabled)
359        );
360    }
361}