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 CanvasRect,
56 CanvasCircle,
57 CanvasLine,
58 CanvasText,
59 CanvasGroup,
60 DatePicker,
62 TimePicker,
64 ColorPicker,
66 Menu,
67 MenuItem,
68 MenuSeparator,
69 ContextMenu,
70 Float,
71 DataTable,
73 DataColumn,
74 TreeView,
76 TreeNode,
77 For,
79 If,
80 Custom(String),
81}
82
83#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
85pub enum AttributeValue {
86 Static(String),
87 Binding(crate::expr::BindingExpr),
88 Interpolated(Vec<InterpolatedPart>),
89}
90
91impl Default for AttributeValue {
92 fn default() -> Self {
93 AttributeValue::Static(String::new())
94 }
95}
96
97#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
99pub enum InterpolatedPart {
100 Literal(String),
101 Binding(crate::expr::BindingExpr),
102}
103
104impl Default for InterpolatedPart {
105 fn default() -> Self {
106 InterpolatedPart::Literal(String::new())
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
112pub struct ComboBoxAttributes {
113 pub options: Vec<String>,
114 pub selected: Option<crate::expr::BindingExpr>,
115 pub placeholder: Option<String>,
116 pub on_select: Option<String>,
117}
118
119#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
120pub struct PickListAttributes {
121 pub options: Vec<String>,
122 pub selected: Option<crate::expr::BindingExpr>,
123 pub placeholder: Option<String>,
124 pub on_select: Option<String>,
125}
126
127#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
128pub struct CanvasAttributes {
129 pub width: f32,
130 pub height: f32,
131 pub program: Option<crate::expr::BindingExpr>,
132 pub on_click: Option<String>,
133}
134
135#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
136pub struct ProgressBarAttributes {
137 pub min: Option<f32>,
138 pub max: Option<f32>,
139 pub value: crate::expr::BindingExpr,
140 pub style: Option<ProgressBarStyle>,
141}
142
143#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
144pub enum ProgressBarStyle {
145 Primary,
146 Success,
147 Warning,
148 Danger,
149 Secondary,
150}
151
152#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
153pub struct TooltipAttributes {
154 pub message: String,
155 pub position: Option<TooltipPosition>,
156 pub delay: Option<u64>,
157}
158
159#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
160pub enum TooltipPosition {
161 FollowCursor,
162 Top,
163 Bottom,
164 Left,
165 Right,
166}
167
168#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
169pub struct GridAttributes {
170 pub columns: u32,
171 pub spacing: Option<f32>,
172 pub padding: Option<f32>,
173}
174
175#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
176pub struct FloatAttributes {
177 pub position: Option<FloatPosition>,
178 pub offset_x: Option<f32>,
179 pub offset_y: Option<f32>,
180 pub z_index: Option<u32>,
181}
182
183#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
184pub enum FloatPosition {
185 TopLeft,
186 TopRight,
187 BottomLeft,
188 BottomRight,
189}
190
191#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize, Default)]
193pub struct EventBinding {
194 pub event: EventKind,
195 pub handler: String,
196 pub param: Option<crate::expr::BindingExpr>,
198 pub span: Span,
199}
200
201#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize, Default)]
203pub enum EventKind {
204 #[default]
205 Click,
206 Press,
207 Release,
208 Change,
209 Input,
210 Submit,
211 Select,
212 Toggle,
213 Scroll,
214 CanvasClick,
215 CanvasDrag,
216 CanvasMove,
217 CanvasRelease,
218 RowClick,
219 Cancel,
220 Open,
221 Close,
222}
223
224impl std::fmt::Display for WidgetKind {
225 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
226 let name = match self {
227 WidgetKind::Column => "column",
228 WidgetKind::Row => "row",
229 WidgetKind::Container => "container",
230 WidgetKind::Scrollable => "scrollable",
231 WidgetKind::Stack => "stack",
232 WidgetKind::Text => "text",
233 WidgetKind::Image => "image",
234 WidgetKind::Svg => "svg",
235 WidgetKind::Button => "button",
236 WidgetKind::TextInput => "text_input",
237 WidgetKind::Checkbox => "checkbox",
238 WidgetKind::Slider => "slider",
239 WidgetKind::PickList => "pick_list",
240 WidgetKind::Toggler => "toggler",
241 WidgetKind::Space => "space",
242 WidgetKind::Rule => "rule",
243 WidgetKind::Radio => "radio",
244 WidgetKind::ComboBox => "combobox",
245 WidgetKind::ProgressBar => "progress_bar",
246 WidgetKind::Tooltip => "tooltip",
247 WidgetKind::Grid => "grid",
248 WidgetKind::Canvas => "canvas",
249 WidgetKind::CanvasRect => "rect",
250 WidgetKind::CanvasCircle => "circle",
251 WidgetKind::CanvasLine => "line",
252 WidgetKind::CanvasText => "canvas_text",
253 WidgetKind::CanvasGroup => "group",
254 WidgetKind::DatePicker => "date_picker",
255 WidgetKind::TimePicker => "time_picker",
256 WidgetKind::ColorPicker => "color_picker",
257 WidgetKind::Menu => "menu",
258 WidgetKind::MenuItem => "menu_item",
259 WidgetKind::MenuSeparator => "menu_separator",
260 WidgetKind::ContextMenu => "context_menu",
261 WidgetKind::Float => "float",
262 WidgetKind::DataTable => "data_table",
263 WidgetKind::DataColumn => "data_column",
264 WidgetKind::TreeView => "tree_view",
265 WidgetKind::TreeNode => "tree_node",
266 WidgetKind::For => "for",
267 WidgetKind::If => "if",
268 WidgetKind::Custom(name) => return write!(f, "{}", name),
269 };
270 write!(f, "{}", name)
271 }
272}
273
274impl WidgetKind {
275 pub fn all_standard() -> &'static [&'static str] {
277 &[
278 "column",
279 "row",
280 "container",
281 "scrollable",
282 "stack",
283 "text",
284 "image",
285 "svg",
286 "button",
287 "text_input",
288 "checkbox",
289 "slider",
290 "pick_list",
291 "toggler",
292 "space",
293 "rule",
294 "radio",
295 "combobox",
296 "progress_bar",
297 "tooltip",
298 "grid",
299 "canvas",
300 "rect",
301 "circle",
302 "line",
303 "canvas_text",
304 "group",
305 "date_picker",
306 "time_picker",
307 "color_picker",
308 "menu",
309 "menu_item",
310 "menu_separator",
311 "context_menu",
312 "float",
313 "data_table",
314 "data_column",
315 "tree_view",
316 "tree_node",
317 "for",
318 "if",
319 ]
320 }
321
322 pub fn is_custom(&self) -> bool {
324 matches!(self, WidgetKind::Custom(_))
325 }
326
327 pub fn minimum_version(&self) -> crate::ir::SchemaVersion {
352 match self {
355 WidgetKind::Canvas => crate::ir::SchemaVersion { major: 1, minor: 1 },
356 WidgetKind::DatePicker
357 | WidgetKind::TimePicker
358 | WidgetKind::ColorPicker
359 | WidgetKind::Menu
360 | WidgetKind::MenuItem
361 | WidgetKind::MenuSeparator
362 | WidgetKind::ContextMenu
363 | WidgetKind::DataTable
364 | WidgetKind::DataColumn
365 | WidgetKind::TreeView
366 | WidgetKind::TreeNode => crate::ir::SchemaVersion { major: 1, minor: 1 },
367 _ => crate::ir::SchemaVersion { major: 1, minor: 0 },
368 }
369 }
370
371 pub fn schema(&self) -> crate::schema::WidgetSchema {
373 crate::schema::get_widget_schema(self)
374 }
375}
376
377#[cfg(test)]
378mod tests {
379 use super::*;
380 use crate::ir::style::StyleProperties;
381 use crate::ir::theme::WidgetState;
382
383 #[test]
384 fn test_widget_node_default_has_empty_inline_state_variants() {
385 let node = WidgetNode::default();
386 assert!(node.inline_state_variants.is_empty());
387 }
388
389 #[test]
390 fn test_widget_node_inline_state_variants_serialization() {
391 let mut node = WidgetNode {
392 kind: WidgetKind::Button,
393 id: Some("test-button".to_string()),
394 attributes: Default::default(),
395 events: Default::default(),
396 children: Default::default(),
397 span: Default::default(),
398 style: Default::default(),
399 layout: Default::default(),
400 theme_ref: Default::default(),
401 classes: Default::default(),
402 breakpoint_attributes: Default::default(),
403 inline_state_variants: Default::default(),
404 };
405
406 node.inline_state_variants.insert(
408 WidgetState::Hover,
409 StyleProperties {
410 opacity: Some(0.8),
411 ..Default::default()
412 },
413 );
414
415 let json = serde_json::to_string(&node).expect("Should serialize");
417 let deserialized: WidgetNode = serde_json::from_str(&json).expect("Should deserialize");
418
419 assert_eq!(deserialized.inline_state_variants.len(), 1);
421 assert!(
422 deserialized
423 .inline_state_variants
424 .contains_key(&WidgetState::Hover)
425 );
426 }
427
428 #[test]
429 fn test_widget_node_inline_state_variants_multiple_states() {
430 let mut node = WidgetNode::default();
431
432 node.inline_state_variants.insert(
433 WidgetState::Hover,
434 StyleProperties {
435 opacity: Some(0.9),
436 ..Default::default()
437 },
438 );
439
440 node.inline_state_variants.insert(
441 WidgetState::Active,
442 StyleProperties {
443 opacity: Some(0.7),
444 ..Default::default()
445 },
446 );
447
448 node.inline_state_variants.insert(
449 WidgetState::Disabled,
450 StyleProperties {
451 opacity: Some(0.5),
452 ..Default::default()
453 },
454 );
455
456 assert_eq!(node.inline_state_variants.len(), 3);
457 assert!(node.inline_state_variants.contains_key(&WidgetState::Hover));
458 assert!(
459 node.inline_state_variants
460 .contains_key(&WidgetState::Active)
461 );
462 assert!(
463 node.inline_state_variants
464 .contains_key(&WidgetState::Disabled)
465 );
466 }
467}
468
469#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
473pub enum CanvasShape {
474 Rect(RectShape),
476 Circle(CircleShape),
478 Line(LineShape),
480 Text(TextShape),
482 Group(GroupShape),
484}
485
486#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
488pub struct RectShape {
489 pub x: AttributeValue,
491 pub y: AttributeValue,
493 pub width: AttributeValue,
495 pub height: AttributeValue,
497 pub fill: Option<AttributeValue>,
499 pub stroke: Option<AttributeValue>,
501 pub stroke_width: Option<AttributeValue>,
503 pub radius: Option<AttributeValue>,
505}
506
507#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
509pub struct CircleShape {
510 pub cx: AttributeValue,
512 pub cy: AttributeValue,
514 pub radius: AttributeValue,
516 pub fill: Option<AttributeValue>,
518 pub stroke: Option<AttributeValue>,
520 pub stroke_width: Option<AttributeValue>,
522}
523
524#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
526pub struct LineShape {
527 pub x1: AttributeValue,
529 pub y1: AttributeValue,
531 pub x2: AttributeValue,
533 pub y2: AttributeValue,
535 pub stroke: Option<AttributeValue>,
537 pub stroke_width: Option<AttributeValue>,
539}
540
541#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
543pub struct TextShape {
544 pub x: AttributeValue,
546 pub y: AttributeValue,
548 pub content: AttributeValue,
550 pub size: Option<AttributeValue>,
552 pub color: Option<AttributeValue>,
554}
555
556#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
558pub struct GroupShape {
559 pub transform: Option<Transform>,
561 pub children: Vec<CanvasShape>,
563}
564
565#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
567pub enum Transform {
568 Translate(f32, f32),
570 Rotate(f32),
572 Scale(f32),
574 ScaleXY(f32, f32),
576 Matrix([f32; 6]),
578}