1use serde::{Deserialize, Serialize};
4
5#[cfg(feature = "schema")]
6use schemars::JsonSchema;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
15#[cfg_attr(feature = "schema", derive(JsonSchema))]
16#[serde(rename_all = "camelCase")]
17pub struct WidgetWindowConfig {
18 pub label: String,
20 #[serde(default, skip_serializing_if = "Option::is_none")]
23 pub url: Option<String>,
24 pub width: f64,
26 pub height: f64,
28 #[serde(default, skip_serializing_if = "Option::is_none")]
30 pub x: Option<f64>,
31 #[serde(default, skip_serializing_if = "Option::is_none")]
33 pub y: Option<f64>,
34 #[serde(default)]
36 pub always_on_top: bool,
37 #[serde(default = "default_true")]
39 pub skip_taskbar: bool,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub group: Option<String>,
44 #[serde(default, skip_serializing_if = "Option::is_none")]
46 pub widget_id: Option<String>,
47 #[serde(default, skip_serializing_if = "Option::is_none")]
50 pub size: Option<String>,
51}
52
53fn default_true() -> bool {
54 true
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
61#[cfg_attr(feature = "schema", derive(JsonSchema))]
62#[serde(rename_all = "camelCase")]
63pub struct WidgetConfig {
64 #[serde(default = "default_version")]
66 pub version: u32,
67 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub small: Option<WidgetElement>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
72 pub medium: Option<WidgetElement>,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub large: Option<WidgetElement>,
76}
77
78impl Default for WidgetConfig {
79 fn default() -> Self {
80 Self {
81 version: 1,
82 small: None,
83 medium: None,
84 large: None,
85 }
86 }
87}
88
89impl WidgetConfig {
90 pub fn small(el: impl Into<WidgetElement>) -> Self {
92 Self {
93 small: Some(el.into()),
94 ..Default::default()
95 }
96 }
97
98 pub fn with_medium(mut self, el: impl Into<WidgetElement>) -> Self {
100 self.medium = Some(el.into());
101 self
102 }
103
104 pub fn with_large(mut self, el: impl Into<WidgetElement>) -> Self {
106 self.large = Some(el.into());
107 self
108 }
109}
110
111fn default_version() -> u32 {
112 1
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117#[cfg_attr(feature = "schema", derive(JsonSchema))]
118#[serde(tag = "type", rename_all = "camelCase")]
119pub enum WidgetElement {
120 #[serde(rename = "vstack")]
122 VStack(VStackElement),
123 #[serde(rename = "hstack")]
125 HStack(HStackElement),
126 #[serde(rename = "zstack")]
128 ZStack(ZStackElement),
129 #[serde(rename = "grid")]
131 Grid(GridElement),
132 #[serde(rename = "container")]
134 Container(ContainerElement),
135 #[serde(rename = "text")]
137 Text(TextElement),
138 #[serde(rename = "image")]
140 Image(ImageElement),
141 #[serde(rename = "progress")]
143 Progress(ProgressElement),
144 #[serde(rename = "gauge")]
146 Gauge(GaugeElement),
147 #[serde(rename = "button")]
149 Button(ButtonElement),
150 #[serde(rename = "toggle")]
152 Toggle(ToggleElement),
153 #[serde(rename = "divider")]
155 Divider(DividerElement),
156 #[serde(rename = "spacer")]
158 Spacer(SpacerElement),
159 #[serde(rename = "date")]
161 Date(DateElement),
162 #[serde(rename = "chart")]
164 Chart(ChartElement),
165 #[serde(rename = "list")]
167 List(ListElement),
168 #[serde(rename = "link")]
170 Link(LinkElement),
171 #[serde(rename = "shape")]
173 Shape(ShapeElement),
174 #[serde(rename = "timer")]
176 Timer(TimerElement),
177 #[serde(rename = "canvas")]
179 Canvas(CanvasElement),
180 #[serde(rename = "label")]
182 Label(LabelElement),
183}
184
185#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187#[cfg_attr(feature = "schema", derive(JsonSchema))]
188#[serde(rename_all = "camelCase")]
189pub struct VStackElement {
190 #[serde(default, skip_serializing_if = "Vec::is_empty")]
192 pub children: Vec<WidgetElement>,
193 #[serde(default, skip_serializing_if = "Option::is_none")]
195 pub spacing: Option<f64>,
196 #[serde(default, skip_serializing_if = "Option::is_none")]
198 pub alignment: Option<HorizontalAlignment>,
199 #[serde(flatten)]
200 pub style: ElementStyle,
202}
203
204#[derive(Debug, Clone, Serialize, Deserialize, Default)]
206#[cfg_attr(feature = "schema", derive(JsonSchema))]
207#[serde(rename_all = "camelCase")]
208pub struct HStackElement {
209 #[serde(default, skip_serializing_if = "Vec::is_empty")]
211 pub children: Vec<WidgetElement>,
212 #[serde(default, skip_serializing_if = "Option::is_none")]
214 pub spacing: Option<f64>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
217 pub alignment: Option<VerticalAlignment>,
218 #[serde(flatten)]
219 pub style: ElementStyle,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize, Default)]
225#[cfg_attr(feature = "schema", derive(JsonSchema))]
226#[serde(rename_all = "camelCase")]
227pub struct ZStackElement {
228 #[serde(default, skip_serializing_if = "Vec::is_empty")]
230 pub children: Vec<WidgetElement>,
231 #[serde(default, skip_serializing_if = "Option::is_none")]
233 pub alignment: Option<String>,
234 #[serde(flatten)]
235 pub style: ElementStyle,
237}
238
239#[derive(Debug, Clone, Serialize, Deserialize, Default)]
241#[cfg_attr(feature = "schema", derive(JsonSchema))]
242#[serde(rename_all = "camelCase")]
243pub struct GridElement {
244 #[serde(default, skip_serializing_if = "Vec::is_empty")]
246 pub children: Vec<WidgetElement>,
247 #[serde(default = "default_columns")]
249 pub columns: u32,
250 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub spacing: Option<f64>,
253 #[serde(
255 rename = "rowSpacing",
256 default,
257 skip_serializing_if = "Option::is_none"
258 )]
259 pub row_spacing: Option<f64>,
260 #[serde(flatten)]
261 pub style: ElementStyle,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize, Default)]
267#[cfg_attr(feature = "schema", derive(JsonSchema))]
268#[serde(rename_all = "camelCase")]
269pub struct ContainerElement {
270 #[serde(default, skip_serializing_if = "Vec::is_empty")]
272 pub children: Vec<WidgetElement>,
273 #[serde(
275 rename = "contentAlignment",
276 default,
277 skip_serializing_if = "Option::is_none"
278 )]
279 pub content_alignment: Option<String>,
280 #[serde(flatten)]
281 pub style: ElementStyle,
283}
284
285#[derive(Debug, Clone, Serialize, Deserialize, Default)]
287#[cfg_attr(feature = "schema", derive(JsonSchema))]
288#[serde(rename_all = "camelCase")]
289pub struct TextElement {
290 pub content: String,
292 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
294 pub font_size: Option<f64>,
295 #[serde(
297 rename = "fontWeight",
298 default,
299 skip_serializing_if = "Option::is_none"
300 )]
301 pub font_weight: Option<FontWeight>,
302 #[serde(
304 rename = "fontDesign",
305 default,
306 skip_serializing_if = "Option::is_none"
307 )]
308 pub font_design: Option<FontDesign>,
309 #[serde(rename = "textStyle", default, skip_serializing_if = "Option::is_none")]
312 pub text_style: Option<TextStyle>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
315 pub color: Option<ColorValue>,
316 #[serde(default, skip_serializing_if = "Option::is_none")]
318 pub alignment: Option<TextAlignment>,
319 #[serde(rename = "lineLimit", default, skip_serializing_if = "Option::is_none")]
321 pub line_limit: Option<u32>,
322 #[serde(flatten)]
323 pub style: ElementStyle,
325}
326
327impl TextElement {
328 pub fn font_size(mut self, size: f64) -> Self {
330 self.font_size = Some(size);
331 self
332 }
333
334 pub fn font_weight(mut self, weight: FontWeight) -> Self {
336 self.font_weight = Some(weight);
337 self
338 }
339
340 pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
342 self.color = Some(color.into());
343 self
344 }
345}
346
347#[derive(Debug, Clone, Serialize, Deserialize, Default)]
349#[cfg_attr(feature = "schema", derive(JsonSchema))]
350#[serde(rename_all = "camelCase")]
351pub struct ImageElement {
352 #[serde(
354 rename = "systemName",
355 default,
356 skip_serializing_if = "Option::is_none"
357 )]
358 pub system_name: Option<String>,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
361 pub data: Option<String>,
362 #[serde(default, skip_serializing_if = "Option::is_none")]
364 pub url: Option<String>,
365 #[serde(default, skip_serializing_if = "Option::is_none")]
367 pub size: Option<f64>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub color: Option<ColorValue>,
371 #[serde(
373 rename = "contentMode",
374 default,
375 skip_serializing_if = "Option::is_none"
376 )]
377 pub content_mode: Option<ContentMode>,
378 #[serde(flatten)]
379 pub style: ElementStyle,
381}
382
383#[derive(Debug, Clone, Serialize, Deserialize)]
385#[cfg_attr(feature = "schema", derive(JsonSchema))]
386#[serde(rename_all = "camelCase")]
387pub struct ProgressElement {
388 pub value: f64,
390 #[serde(default = "default_total")]
392 pub total: f64,
393 #[serde(default, skip_serializing_if = "Option::is_none")]
395 pub label: Option<String>,
396 #[serde(default, skip_serializing_if = "Option::is_none")]
398 pub tint: Option<ColorValue>,
399 #[serde(default, skip_serializing_if = "Option::is_none")]
401 pub color: Option<ColorValue>,
402 #[serde(
404 rename = "barStyle",
405 default,
406 skip_serializing_if = "Option::is_none"
407 )]
408 pub bar_style: Option<ProgressStyle>,
409 #[serde(flatten)]
410 pub style: ElementStyle,
412}
413
414impl Default for ProgressElement {
415 fn default() -> Self {
416 Self {
417 value: 0.0,
418 total: 1.0,
419 label: None,
420 tint: None,
421 color: None,
422 bar_style: None,
423 style: ElementStyle::default(),
424 }
425 }
426}
427
428#[derive(Debug, Clone, Serialize, Deserialize, Default)]
430#[cfg_attr(feature = "schema", derive(JsonSchema))]
431#[serde(rename_all = "camelCase")]
432pub struct GaugeElement {
433 pub value: f64,
435 #[serde(default, skip_serializing_if = "Option::is_none")]
437 pub min: Option<f64>,
438 #[serde(default, skip_serializing_if = "Option::is_none")]
440 pub max: Option<f64>,
441 #[serde(default, skip_serializing_if = "Option::is_none")]
443 pub label: Option<String>,
444 #[serde(
446 rename = "currentValueLabel",
447 default,
448 skip_serializing_if = "Option::is_none"
449 )]
450 pub current_value_label: Option<String>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
453 pub tint: Option<ColorValue>,
454 #[serde(default, skip_serializing_if = "Option::is_none")]
456 pub color: Option<ColorValue>,
457 #[serde(
459 rename = "gaugeStyle",
460 default,
461 skip_serializing_if = "Option::is_none"
462 )]
463 pub gauge_style: Option<GaugeStyle>,
464 #[serde(flatten)]
465 pub style: ElementStyle,
467}
468
469#[derive(Debug, Clone, Serialize, Deserialize, Default)]
471#[cfg_attr(feature = "schema", derive(JsonSchema))]
472#[serde(rename_all = "camelCase")]
473pub struct ButtonElement {
474 pub label: String,
476 #[serde(default, skip_serializing_if = "Option::is_none")]
478 pub url: Option<String>,
479 #[serde(default, skip_serializing_if = "Option::is_none")]
481 pub action: Option<String>,
482 #[serde(default, skip_serializing_if = "Option::is_none")]
484 pub color: Option<ColorValue>,
485 #[serde(
487 rename = "backgroundColor",
488 default,
489 skip_serializing_if = "Option::is_none"
490 )]
491 pub background_color: Option<ColorValue>,
492 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
494 pub font_size: Option<f64>,
495 #[serde(
497 rename = "textAlignment",
498 default,
499 skip_serializing_if = "Option::is_none"
500 )]
501 pub text_alignment: Option<TextAlignment>,
502 #[serde(flatten)]
503 pub style: ElementStyle,
505}
506
507#[derive(Debug, Clone, Serialize, Deserialize, Default)]
509#[cfg_attr(feature = "schema", derive(JsonSchema))]
510#[serde(rename_all = "camelCase")]
511pub struct ToggleElement {
512 #[serde(rename = "isOn")]
514 pub is_on: bool,
515 #[serde(default, skip_serializing_if = "Option::is_none")]
517 pub label: Option<String>,
518 #[serde(default, skip_serializing_if = "Option::is_none")]
520 pub tint: Option<ColorValue>,
521 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub action: Option<String>,
524 #[serde(flatten)]
525 pub style: ElementStyle,
527}
528
529#[derive(Debug, Clone, Serialize, Deserialize, Default)]
531#[cfg_attr(feature = "schema", derive(JsonSchema))]
532#[serde(rename_all = "camelCase")]
533pub struct DividerElement {
534 #[serde(default, skip_serializing_if = "Option::is_none")]
536 pub color: Option<ColorValue>,
537 #[serde(default, skip_serializing_if = "Option::is_none")]
539 pub thickness: Option<f64>,
540 #[serde(flatten)]
541 pub style: ElementStyle,
543}
544
545#[derive(Debug, Clone, Serialize, Deserialize, Default)]
547#[cfg_attr(feature = "schema", derive(JsonSchema))]
548#[serde(rename_all = "camelCase")]
549pub struct SpacerElement {
550 #[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
552 pub min_length: Option<f64>,
553}
554
555#[derive(Debug, Clone, Serialize, Deserialize, Default)]
557#[cfg_attr(feature = "schema", derive(JsonSchema))]
558#[serde(rename_all = "camelCase")]
559pub struct DateElement {
560 pub date: String,
562 #[serde(rename = "dateStyle", default, skip_serializing_if = "Option::is_none")]
564 pub date_style: Option<DateStyle>,
565 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
567 pub font_size: Option<f64>,
568 #[serde(default, skip_serializing_if = "Option::is_none")]
570 pub color: Option<ColorValue>,
571 #[serde(flatten)]
572 pub style: ElementStyle,
574}
575
576#[derive(Debug, Clone, Serialize, Deserialize, Default)]
578#[cfg_attr(feature = "schema", derive(JsonSchema))]
579#[serde(rename_all = "camelCase")]
580pub struct ChartElement {
581 #[serde(rename = "chartType")]
583 pub chart_type: ChartType,
584 #[serde(default, rename = "chartData", skip_serializing_if = "Vec::is_empty")]
586 pub chart_data: Vec<ChartDataPoint>,
587 #[serde(default, skip_serializing_if = "Option::is_none")]
589 pub tint: Option<ColorValue>,
590 #[serde(flatten)]
591 pub style: ElementStyle,
593}
594
595#[derive(Debug, Clone, Serialize, Deserialize, Default)]
597#[cfg_attr(feature = "schema", derive(JsonSchema))]
598#[serde(rename_all = "camelCase")]
599pub struct ListElement {
600 #[serde(default, skip_serializing_if = "Vec::is_empty")]
602 pub items: Vec<ListItem>,
603 #[serde(default, skip_serializing_if = "Option::is_none")]
605 pub spacing: Option<f64>,
606 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
608 pub font_size: Option<f64>,
609 #[serde(default, skip_serializing_if = "Option::is_none")]
611 pub color: Option<ColorValue>,
612 #[serde(flatten)]
613 pub style: ElementStyle,
615}
616
617#[derive(Debug, Clone, Serialize, Deserialize, Default)]
619#[cfg_attr(feature = "schema", derive(JsonSchema))]
620#[serde(rename_all = "camelCase")]
621pub struct LinkElement {
622 #[serde(default, skip_serializing_if = "Vec::is_empty")]
624 pub children: Vec<WidgetElement>,
625 #[serde(default, skip_serializing_if = "Option::is_none")]
627 pub url: Option<String>,
628 #[serde(default, skip_serializing_if = "Option::is_none")]
630 pub action: Option<String>,
631 #[serde(flatten)]
632 pub style: ElementStyle,
634}
635
636#[derive(Debug, Clone, Serialize, Deserialize, Default)]
638#[cfg_attr(feature = "schema", derive(JsonSchema))]
639#[serde(rename_all = "camelCase")]
640pub struct ShapeElement {
641 #[serde(rename = "shapeType")]
643 pub shape_type: ShapeType,
644 #[serde(default, skip_serializing_if = "Option::is_none")]
646 pub fill: Option<ColorValue>,
647 #[serde(default, skip_serializing_if = "Option::is_none")]
649 pub stroke: Option<ColorValue>,
650 #[serde(
652 rename = "strokeWidth",
653 default,
654 skip_serializing_if = "Option::is_none"
655 )]
656 pub stroke_width: Option<f64>,
657 #[serde(default, skip_serializing_if = "Option::is_none")]
659 pub size: Option<f64>,
660 #[serde(flatten)]
661 pub style: ElementStyle,
663}
664
665#[derive(Debug, Clone, Serialize, Deserialize, Default)]
667#[cfg_attr(feature = "schema", derive(JsonSchema))]
668#[serde(rename_all = "camelCase")]
669pub struct TimerElement {
670 #[serde(rename = "targetDate")]
672 pub target_date: String,
673 #[serde(default, skip_serializing_if = "Option::is_none")]
675 pub counting: Option<TimerCounting>,
676 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
678 pub font_size: Option<f64>,
679 #[serde(
681 rename = "fontWeight",
682 default,
683 skip_serializing_if = "Option::is_none"
684 )]
685 pub font_weight: Option<FontWeight>,
686 #[serde(default, skip_serializing_if = "Option::is_none")]
688 pub color: Option<ColorValue>,
689 #[serde(flatten)]
690 pub style: ElementStyle,
692}
693
694#[derive(Debug, Clone, Serialize, Deserialize, Default)]
696#[cfg_attr(feature = "schema", derive(JsonSchema))]
697#[serde(rename_all = "camelCase")]
698pub struct CanvasElement {
699 pub width: f64,
701 pub height: f64,
703 #[serde(default, skip_serializing_if = "Vec::is_empty")]
705 pub elements: Vec<CanvasDrawCommand>,
706 #[serde(flatten)]
707 pub style: ElementStyle,
709}
710
711#[derive(Debug, Clone, Serialize, Deserialize, Default)]
713#[cfg_attr(feature = "schema", derive(JsonSchema))]
714#[serde(rename_all = "camelCase")]
715pub struct LabelElement {
716 pub text: String,
718 #[serde(rename = "systemName")]
720 pub system_name: String,
721 #[serde(rename = "iconColor", default, skip_serializing_if = "Option::is_none")]
723 pub icon_color: Option<ColorValue>,
724 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
726 pub font_size: Option<f64>,
727 #[serde(
729 rename = "fontWeight",
730 default,
731 skip_serializing_if = "Option::is_none"
732 )]
733 pub font_weight: Option<FontWeight>,
734 #[serde(default, skip_serializing_if = "Option::is_none")]
736 pub color: Option<ColorValue>,
737 #[serde(default, skip_serializing_if = "Option::is_none")]
739 pub spacing: Option<f64>,
740 #[serde(flatten)]
741 pub style: ElementStyle,
743}
744
745impl From<VStackElement> for WidgetElement {
746 fn from(value: VStackElement) -> Self {
747 WidgetElement::VStack(value)
748 }
749}
750
751impl From<HStackElement> for WidgetElement {
752 fn from(value: HStackElement) -> Self {
753 WidgetElement::HStack(value)
754 }
755}
756
757impl From<ZStackElement> for WidgetElement {
758 fn from(value: ZStackElement) -> Self {
759 WidgetElement::ZStack(value)
760 }
761}
762
763impl From<GridElement> for WidgetElement {
764 fn from(value: GridElement) -> Self {
765 WidgetElement::Grid(value)
766 }
767}
768
769impl From<ContainerElement> for WidgetElement {
770 fn from(value: ContainerElement) -> Self {
771 WidgetElement::Container(value)
772 }
773}
774
775impl From<TextElement> for WidgetElement {
776 fn from(value: TextElement) -> Self {
777 WidgetElement::Text(value)
778 }
779}
780
781impl From<ImageElement> for WidgetElement {
782 fn from(value: ImageElement) -> Self {
783 WidgetElement::Image(value)
784 }
785}
786
787impl From<ProgressElement> for WidgetElement {
788 fn from(value: ProgressElement) -> Self {
789 WidgetElement::Progress(value)
790 }
791}
792
793impl From<GaugeElement> for WidgetElement {
794 fn from(value: GaugeElement) -> Self {
795 WidgetElement::Gauge(value)
796 }
797}
798
799impl From<ButtonElement> for WidgetElement {
800 fn from(value: ButtonElement) -> Self {
801 WidgetElement::Button(value)
802 }
803}
804
805impl From<ToggleElement> for WidgetElement {
806 fn from(value: ToggleElement) -> Self {
807 WidgetElement::Toggle(value)
808 }
809}
810
811impl From<DividerElement> for WidgetElement {
812 fn from(value: DividerElement) -> Self {
813 WidgetElement::Divider(value)
814 }
815}
816
817impl From<SpacerElement> for WidgetElement {
818 fn from(value: SpacerElement) -> Self {
819 WidgetElement::Spacer(value)
820 }
821}
822
823impl From<DateElement> for WidgetElement {
824 fn from(value: DateElement) -> Self {
825 WidgetElement::Date(value)
826 }
827}
828
829impl From<ChartElement> for WidgetElement {
830 fn from(value: ChartElement) -> Self {
831 WidgetElement::Chart(value)
832 }
833}
834
835impl From<ListElement> for WidgetElement {
836 fn from(value: ListElement) -> Self {
837 WidgetElement::List(value)
838 }
839}
840
841impl From<LinkElement> for WidgetElement {
842 fn from(value: LinkElement) -> Self {
843 WidgetElement::Link(value)
844 }
845}
846
847impl From<ShapeElement> for WidgetElement {
848 fn from(value: ShapeElement) -> Self {
849 WidgetElement::Shape(value)
850 }
851}
852
853impl From<TimerElement> for WidgetElement {
854 fn from(value: TimerElement) -> Self {
855 WidgetElement::Timer(value)
856 }
857}
858
859impl From<CanvasElement> for WidgetElement {
860 fn from(value: CanvasElement) -> Self {
861 WidgetElement::Canvas(value)
862 }
863}
864
865impl From<LabelElement> for WidgetElement {
866 fn from(value: LabelElement) -> Self {
867 WidgetElement::Label(value)
868 }
869}
870
871pub fn text(content: impl Into<String>) -> TextElement {
873 TextElement {
874 content: content.into(),
875 ..Default::default()
876 }
877}
878
879pub fn vstack(children: Vec<WidgetElement>) -> VStackElement {
881 VStackElement {
882 children,
883 ..Default::default()
884 }
885}
886
887pub fn hstack(children: Vec<WidgetElement>) -> HStackElement {
889 HStackElement {
890 children,
891 ..Default::default()
892 }
893}
894
895fn default_columns() -> u32 {
896 2
897}
898fn default_total() -> f64 {
899 1.0
900}
901
902#[derive(Debug, Clone, Serialize, Deserialize, Default)]
906#[cfg_attr(feature = "schema", derive(JsonSchema))]
907#[serde(rename_all = "camelCase")]
908pub struct ElementStyle {
909 #[serde(default, skip_serializing_if = "Option::is_none")]
911 pub padding: Option<PaddingValue>,
912 #[serde(default, skip_serializing_if = "Option::is_none")]
914 pub background: Option<BackgroundValue>,
915 #[serde(
917 rename = "cornerRadius",
918 default,
919 skip_serializing_if = "Option::is_none"
920 )]
921 pub corner_radius: Option<f64>,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
924 pub opacity: Option<f64>,
925 #[serde(default, skip_serializing_if = "Option::is_none")]
927 pub frame: Option<FrameConfig>,
928 #[serde(default, skip_serializing_if = "Option::is_none")]
930 pub border: Option<BorderConfig>,
931 #[serde(default, skip_serializing_if = "Option::is_none")]
933 pub shadow: Option<ShadowConfig>,
934 #[serde(rename = "clipShape", default, skip_serializing_if = "Option::is_none")]
936 pub clip_shape: Option<ClipShape>,
937 #[serde(default, skip_serializing_if = "Option::is_none")]
939 pub flex: Option<f64>,
940}
941
942#[derive(Debug, Clone, Serialize, Deserialize)]
947#[cfg_attr(feature = "schema", derive(JsonSchema))]
948#[serde(untagged)]
949pub enum ColorValue {
950 Solid(String),
951 Adaptive { light: String, dark: String },
952}
953
954impl From<&str> for ColorValue {
955 fn from(value: &str) -> Self {
956 ColorValue::Solid(value.to_string())
957 }
958}
959
960impl From<String> for ColorValue {
961 fn from(value: String) -> Self {
962 ColorValue::Solid(value)
963 }
964}
965
966#[derive(Debug, Clone, Serialize, Deserialize)]
968#[cfg_attr(feature = "schema", derive(JsonSchema))]
969#[serde(rename_all = "camelCase")]
970pub enum ClipShape {
971 Circle,
973 Capsule,
975 Rectangle,
977}
978
979#[derive(Debug, Clone, Serialize, Deserialize)]
981#[cfg_attr(feature = "schema", derive(JsonSchema))]
982#[serde(rename_all = "camelCase")]
983pub enum TextStyle {
984 LargeTitle,
986 Title,
988 Title2,
990 Title3,
992 Headline,
994 Subheadline,
996 Body,
998 Callout,
1000 Footnote,
1002 Caption,
1004 Caption2,
1006}
1007
1008#[derive(Debug, Clone, Serialize, Deserialize)]
1010#[cfg_attr(feature = "schema", derive(JsonSchema))]
1011#[serde(untagged)]
1012pub enum BackgroundValue {
1013 Solid(String),
1014 Gradient(GradientConfig),
1015 Adaptive { light: String, dark: String },
1016}
1017
1018impl From<&str> for BackgroundValue {
1019 fn from(value: &str) -> Self {
1020 BackgroundValue::Solid(value.to_string())
1021 }
1022}
1023
1024impl From<String> for BackgroundValue {
1025 fn from(value: String) -> Self {
1026 BackgroundValue::Solid(value)
1027 }
1028}
1029
1030#[derive(Debug, Clone, Serialize, Deserialize)]
1031#[cfg_attr(feature = "schema", derive(JsonSchema))]
1032#[serde(rename_all = "camelCase")]
1033pub struct GradientConfig {
1034 #[serde(rename = "gradientType")]
1036 pub gradient_type: GradientType,
1037 pub colors: Vec<String>,
1038 #[serde(default, skip_serializing_if = "Option::is_none")]
1040 pub direction: Option<GradientDirection>,
1041}
1042
1043#[derive(Debug, Clone, Serialize, Deserialize)]
1044#[cfg_attr(feature = "schema", derive(JsonSchema))]
1045#[serde(rename_all = "camelCase")]
1046pub enum GradientType {
1047 Linear,
1049 Radial,
1051 Angular,
1053}
1054
1055#[derive(Debug, Clone, Serialize, Deserialize)]
1056#[cfg_attr(feature = "schema", derive(JsonSchema))]
1057#[serde(rename_all = "camelCase")]
1058pub enum GradientDirection {
1059 TopToBottom,
1061 BottomToTop,
1063 LeadingToTrailing,
1065 TrailingToLeading,
1067 TopLeadingToBottomTrailing,
1069 TopTrailingToBottomLeading,
1071}
1072
1073#[derive(Debug, Clone, Serialize, Deserialize)]
1074#[cfg_attr(feature = "schema", derive(JsonSchema))]
1075#[serde(rename_all = "camelCase")]
1076pub struct ShadowConfig {
1077 #[serde(default, skip_serializing_if = "Option::is_none")]
1078 pub color: Option<String>,
1079 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub radius: Option<f64>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub x: Option<f64>,
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1084 pub y: Option<f64>,
1085}
1086
1087#[derive(Debug, Clone, Serialize, Deserialize)]
1088#[cfg_attr(feature = "schema", derive(JsonSchema))]
1089#[serde(untagged)]
1090pub enum PaddingValue {
1091 Uniform(f64),
1092 Edges {
1093 #[serde(default, skip_serializing_if = "Option::is_none")]
1094 top: Option<f64>,
1095 #[serde(default, skip_serializing_if = "Option::is_none")]
1096 bottom: Option<f64>,
1097 #[serde(default, skip_serializing_if = "Option::is_none")]
1098 leading: Option<f64>,
1099 #[serde(default, skip_serializing_if = "Option::is_none")]
1100 trailing: Option<f64>,
1101 },
1102}
1103
1104impl From<f64> for PaddingValue {
1105 fn from(value: f64) -> Self {
1106 PaddingValue::Uniform(value)
1107 }
1108}
1109
1110impl From<f32> for PaddingValue {
1111 fn from(value: f32) -> Self {
1112 PaddingValue::Uniform(f64::from(value))
1113 }
1114}
1115
1116impl From<i32> for PaddingValue {
1117 fn from(value: i32) -> Self {
1118 PaddingValue::Uniform(f64::from(value))
1119 }
1120}
1121
1122#[derive(Debug, Clone, Serialize, Deserialize)]
1123#[cfg_attr(feature = "schema", derive(JsonSchema))]
1124#[serde(rename_all = "camelCase")]
1125pub struct FrameConfig {
1126 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub width: Option<f64>,
1128 #[serde(default, skip_serializing_if = "Option::is_none")]
1129 pub height: Option<f64>,
1130 #[serde(rename = "maxWidth", default, skip_serializing_if = "Option::is_none")]
1131 pub max_width: Option<FrameDimension>,
1132 #[serde(rename = "maxHeight", default, skip_serializing_if = "Option::is_none")]
1133 pub max_height: Option<FrameDimension>,
1134}
1135
1136#[derive(Debug, Clone, Serialize, Deserialize)]
1137#[cfg_attr(feature = "schema", derive(JsonSchema))]
1138#[serde(untagged)]
1139pub enum FrameDimension {
1140 Fixed(f64),
1141 Keyword(String),
1142}
1143
1144#[derive(Debug, Clone, Serialize, Deserialize)]
1145#[cfg_attr(feature = "schema", derive(JsonSchema))]
1146#[serde(rename_all = "camelCase")]
1147pub struct BorderConfig {
1148 pub color: String,
1149 #[serde(default = "default_border_width")]
1150 pub width: f64,
1151}
1152
1153fn default_border_width() -> f64 {
1154 1.0
1155}
1156
1157#[derive(Debug, Clone, Serialize, Deserialize)]
1160#[cfg_attr(feature = "schema", derive(JsonSchema))]
1161#[serde(rename_all = "camelCase")]
1162pub enum FontWeight {
1163 Ultralight,
1165 Thin,
1167 Light,
1169 Regular,
1171 Medium,
1173 Semibold,
1175 Bold,
1177 Heavy,
1179 Black,
1181}
1182
1183#[derive(Debug, Clone, Serialize, Deserialize)]
1184#[cfg_attr(feature = "schema", derive(JsonSchema))]
1185#[serde(rename_all = "camelCase")]
1186pub enum FontDesign {
1187 Default,
1189 Monospaced,
1191 Rounded,
1193 Serif,
1195}
1196
1197#[derive(Debug, Clone, Serialize, Deserialize)]
1198#[cfg_attr(feature = "schema", derive(JsonSchema))]
1199#[serde(rename_all = "camelCase")]
1200pub enum TextAlignment {
1201 Leading,
1203 Center,
1205 Trailing,
1207}
1208
1209#[derive(Debug, Clone, Serialize, Deserialize)]
1210#[cfg_attr(feature = "schema", derive(JsonSchema))]
1211#[serde(rename_all = "camelCase")]
1212pub enum HorizontalAlignment {
1213 Leading,
1215 Center,
1217 Trailing,
1219}
1220
1221#[derive(Debug, Clone, Serialize, Deserialize)]
1222#[cfg_attr(feature = "schema", derive(JsonSchema))]
1223#[serde(rename_all = "camelCase")]
1224pub enum VerticalAlignment {
1225 Top,
1227 Center,
1229 Bottom,
1231}
1232
1233#[derive(Debug, Clone, Serialize, Deserialize)]
1234#[cfg_attr(feature = "schema", derive(JsonSchema))]
1235#[serde(rename_all = "camelCase")]
1236pub enum ContentMode {
1237 Fit,
1239 Fill,
1241}
1242
1243#[derive(Debug, Clone, Serialize, Deserialize)]
1244#[cfg_attr(feature = "schema", derive(JsonSchema))]
1245#[serde(rename_all = "camelCase")]
1246pub enum ProgressStyle {
1247 Linear,
1249 Circular,
1251}
1252
1253#[derive(Debug, Clone, Serialize, Deserialize)]
1254#[cfg_attr(feature = "schema", derive(JsonSchema))]
1255#[serde(rename_all = "camelCase")]
1256pub enum GaugeStyle {
1257 Circular,
1259 Linear,
1261}
1262
1263#[derive(Debug, Clone, Serialize, Deserialize)]
1264#[cfg_attr(feature = "schema", derive(JsonSchema))]
1265#[serde(rename_all = "camelCase")]
1266pub enum DateStyle {
1267 Time,
1269 Date,
1271 Relative,
1273 Offset,
1275 Timer,
1277}
1278
1279#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1280#[cfg_attr(feature = "schema", derive(JsonSchema))]
1281#[serde(rename_all = "camelCase")]
1282pub enum ChartType {
1283 #[default]
1284 Bar,
1285 Line,
1287 Area,
1289 Pie,
1291}
1292
1293#[derive(Debug, Clone, Serialize, Deserialize, Default)]
1294#[cfg_attr(feature = "schema", derive(JsonSchema))]
1295#[serde(rename_all = "camelCase")]
1296pub enum ShapeType {
1297 #[default]
1298 Circle,
1299 Capsule,
1301 Rectangle,
1303}
1304
1305#[derive(Debug, Clone, Serialize, Deserialize)]
1306#[cfg_attr(feature = "schema", derive(JsonSchema))]
1307#[serde(rename_all = "camelCase")]
1308pub enum TimerCounting {
1309 Up,
1311 Down,
1313}
1314
1315#[derive(Debug, Clone, Serialize, Deserialize)]
1316#[cfg_attr(feature = "schema", derive(JsonSchema))]
1317#[serde(rename_all = "camelCase")]
1318pub struct ChartDataPoint {
1319 pub label: String,
1320 pub value: f64,
1321 #[serde(default, skip_serializing_if = "Option::is_none")]
1322 pub color: Option<ColorValue>,
1323}
1324
1325#[derive(Debug, Clone, Serialize, Deserialize)]
1326#[cfg_attr(feature = "schema", derive(JsonSchema))]
1327#[serde(rename_all = "camelCase")]
1328pub struct ListItem {
1329 pub text: String,
1330 #[serde(default, skip_serializing_if = "Option::is_none")]
1331 pub checked: Option<bool>,
1332 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub action: Option<String>,
1334 #[serde(default, skip_serializing_if = "Option::is_none")]
1335 pub payload: Option<String>,
1336}
1337
1338#[derive(Debug, Clone, Serialize, Deserialize)]
1341#[cfg_attr(feature = "schema", derive(JsonSchema))]
1342#[serde(tag = "draw", rename_all = "camelCase")]
1343pub enum CanvasDrawCommand {
1344 #[serde(rename = "circle")]
1345 Circle {
1346 cx: f64,
1347 cy: f64,
1348 r: f64,
1349 #[serde(default, skip_serializing_if = "Option::is_none")]
1350 fill: Option<ColorValue>,
1351 #[serde(default, skip_serializing_if = "Option::is_none")]
1352 stroke: Option<ColorValue>,
1353 #[serde(
1354 rename = "strokeWidth",
1355 default,
1356 skip_serializing_if = "Option::is_none"
1357 )]
1358 stroke_width: Option<f64>,
1359 },
1360 #[serde(rename = "line")]
1361 Line {
1362 x1: f64,
1363 y1: f64,
1364 x2: f64,
1365 y2: f64,
1366 #[serde(default, skip_serializing_if = "Option::is_none")]
1367 stroke: Option<ColorValue>,
1368 #[serde(
1369 rename = "strokeWidth",
1370 default,
1371 skip_serializing_if = "Option::is_none"
1372 )]
1373 stroke_width: Option<f64>,
1374 #[serde(rename = "lineCap", default, skip_serializing_if = "Option::is_none")]
1375 line_cap: Option<String>,
1376 },
1377 #[serde(rename = "rect")]
1378 Rect {
1379 x: f64,
1380 y: f64,
1381 width: f64,
1382 height: f64,
1383 #[serde(default, skip_serializing_if = "Option::is_none")]
1384 fill: Option<ColorValue>,
1385 #[serde(default, skip_serializing_if = "Option::is_none")]
1386 stroke: Option<ColorValue>,
1387 #[serde(
1388 rename = "strokeWidth",
1389 default,
1390 skip_serializing_if = "Option::is_none"
1391 )]
1392 stroke_width: Option<f64>,
1393 #[serde(
1394 rename = "cornerRadius",
1395 default,
1396 skip_serializing_if = "Option::is_none"
1397 )]
1398 corner_radius: Option<f64>,
1399 },
1400 #[serde(rename = "arc")]
1401 Arc {
1402 cx: f64,
1403 cy: f64,
1404 r: f64,
1405 #[serde(rename = "startAngle")]
1406 start_angle: f64,
1407 #[serde(rename = "endAngle")]
1408 end_angle: f64,
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1410 fill: Option<ColorValue>,
1411 #[serde(default, skip_serializing_if = "Option::is_none")]
1412 stroke: Option<ColorValue>,
1413 #[serde(
1414 rename = "strokeWidth",
1415 default,
1416 skip_serializing_if = "Option::is_none"
1417 )]
1418 stroke_width: Option<f64>,
1419 },
1420 #[serde(rename = "text")]
1421 Text {
1422 x: f64,
1423 y: f64,
1424 content: String,
1425 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
1426 font_size: Option<f64>,
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 color: Option<ColorValue>,
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1430 anchor: Option<String>,
1431 },
1432 #[serde(rename = "path")]
1433 Path {
1434 d: String,
1436 #[serde(default, skip_serializing_if = "Option::is_none")]
1437 fill: Option<ColorValue>,
1438 #[serde(default, skip_serializing_if = "Option::is_none")]
1439 stroke: Option<ColorValue>,
1440 #[serde(
1441 rename = "strokeWidth",
1442 default,
1443 skip_serializing_if = "Option::is_none"
1444 )]
1445 stroke_width: Option<f64>,
1446 },
1447}
1448
1449#[cfg(test)]
1450mod tests {
1451 use super::*;
1452 use std::path::PathBuf;
1453
1454 #[test]
1455 fn option_none_omitted_from_json() {
1456 let cfg = WidgetConfig::small(TextElement {
1457 content: "hi".into(),
1458 font_size: Some(12.0),
1459 ..Default::default()
1460 });
1461 let json = serde_json::to_string(&cfg).unwrap();
1462 assert!(!json.contains("null"), "JSON must not contain null: {json}");
1463 assert!(!json.contains("\"medium\""));
1464 assert!(!json.contains("\"fontWeight\""));
1465 let back: WidgetConfig = serde_json::from_str(&json).unwrap();
1466 assert!(back.medium.is_none());
1467 match back.small.unwrap() {
1468 WidgetElement::Text(TextElement {
1469 font_weight,
1470 content,
1471 ..
1472 }) => {
1473 assert!(font_weight.is_none());
1474 assert_eq!(content, "hi");
1475 }
1476 other => panic!("expected text, got {other:?}"),
1477 }
1478 }
1479
1480 #[test]
1481 fn ergonomic_builders_serialize() {
1482 let cfg = WidgetConfig::small(vstack(vec![text("72°")
1483 .font_size(36.0)
1484 .font_weight(FontWeight::Bold)
1485 .color("#fff")
1486 .into()]));
1487 let v = serde_json::to_value(&cfg).unwrap();
1488 assert_eq!(v["small"]["type"], "vstack");
1489 assert_eq!(v["small"]["children"][0]["type"], "text");
1490 assert_eq!(v["small"]["children"][0]["content"], "72°");
1491 assert_eq!(v["small"]["children"][0]["fontSize"], 36.0);
1492 assert_eq!(v["small"]["children"][0]["fontWeight"], "bold");
1493 assert_eq!(v["small"]["children"][0]["color"], "#fff");
1494 }
1495
1496 fn walk_json_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
1497 let Ok(entries) = std::fs::read_dir(dir) else {
1498 return;
1499 };
1500 for entry in entries.flatten() {
1501 let path = entry.path();
1502 if path.is_dir() {
1503 walk_json_files(&path, out);
1504 } else if path.extension().and_then(|e| e.to_str()) == Some("json") {
1505 out.push(path);
1506 }
1507 }
1508 }
1509
1510 #[test]
1513 fn wire_format_unchanged() {
1514 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
1515 let mut files = Vec::new();
1516 walk_json_files(&root, &mut files);
1517 assert!(
1518 !files.is_empty(),
1519 "expected fixtures under {}",
1520 root.display()
1521 );
1522 for path in files {
1523 let raw = std::fs::read_to_string(&path).unwrap();
1524 let cfg: WidgetConfig = serde_json::from_str(&raw).unwrap_or_else(|e| {
1525 panic!("deserialize {}: {e}", path.display());
1526 });
1527 let encoded = serde_json::to_value(&cfg).unwrap();
1528 let again: WidgetConfig = serde_json::from_value(encoded.clone()).unwrap();
1529 assert_eq!(
1530 encoded,
1531 serde_json::to_value(&again).unwrap(),
1532 "wire drift in {}",
1533 path.display()
1534 );
1535 let out = serde_json::to_string(&cfg).unwrap();
1536 assert!(
1537 !out.contains("null"),
1538 "{} serialized with null: {out}",
1539 path.display()
1540 );
1541 }
1542 }
1543}