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#[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 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 #[serde(default)]
25 pub inline_state_variants: HashMap<WidgetState, StyleProperties>,
26}
27
28#[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 ComboBox,
51 ProgressBar,
52 Tooltip,
53 Grid,
54 Canvas,
55 Float,
56 For,
58 If,
59 Custom(String),
60}
61
62#[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#[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#[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#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
172pub struct EventBinding {
173 pub event: EventKind,
174 pub handler: String,
175 pub param: Option<crate::expr::BindingExpr>,
177 pub span: Span,
178}
179
180#[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 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 pub fn is_custom(&self) -> bool {
229 matches!(self, WidgetKind::Custom(_))
230 }
231
232 pub fn minimum_version(&self) -> crate::ir::SchemaVersion {
257 match self {
260 WidgetKind::Canvas => crate::ir::SchemaVersion { major: 1, minor: 1 },
261 _ => crate::ir::SchemaVersion { major: 1, minor: 0 },
262 }
263 }
264
265 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 node.inline_state_variants.insert(
302 WidgetState::Hover,
303 StyleProperties {
304 opacity: Some(0.8),
305 ..Default::default()
306 },
307 );
308
309 let json = serde_json::to_string(&node).expect("Should serialize");
311 let deserialized: WidgetNode = serde_json::from_str(&json).expect("Should deserialize");
312
313 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}