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