1mod data;
8mod style;
9pub use data::*;
10pub use style::*;
11
12use serde::{Deserialize, Serialize};
13
14#[cfg(feature = "schema")]
15use schemars::JsonSchema;
16
17#[derive(Debug, Clone, Serialize, Deserialize)]
24#[cfg_attr(feature = "schema", derive(JsonSchema))]
25#[serde(rename_all = "camelCase")]
26pub struct WidgetWindowConfig {
27 pub label: String,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
32 pub url: Option<String>,
33 pub width: f64,
35 pub height: f64,
37 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub x: Option<f64>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
42 pub y: Option<f64>,
43 #[serde(default)]
45 pub always_on_top: bool,
46 #[serde(default = "default_true")]
48 pub skip_taskbar: bool,
49 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub group: Option<String>,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub widget_id: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub size: Option<String>,
60}
61
62fn default_true() -> bool {
63 true
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize)]
70#[cfg_attr(feature = "schema", derive(JsonSchema))]
71#[serde(rename_all = "camelCase")]
72pub struct WidgetConfig {
73 #[serde(default = "default_version")]
75 pub version: u32,
76 #[serde(default, skip_serializing_if = "Option::is_none")]
78 pub small: Option<WidgetElement>,
79 #[serde(default, skip_serializing_if = "Option::is_none")]
81 pub medium: Option<WidgetElement>,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub large: Option<WidgetElement>,
85}
86
87impl Default for WidgetConfig {
88 fn default() -> Self {
89 Self {
90 version: 1,
91 small: None,
92 medium: None,
93 large: None,
94 }
95 }
96}
97
98impl WidgetConfig {
99 pub fn small(el: impl Into<WidgetElement>) -> Self {
101 Self {
102 small: Some(el.into()),
103 ..Default::default()
104 }
105 }
106
107 pub fn with_medium(mut self, el: impl Into<WidgetElement>) -> Self {
109 self.medium = Some(el.into());
110 self
111 }
112
113 pub fn with_large(mut self, el: impl Into<WidgetElement>) -> Self {
115 self.large = Some(el.into());
116 self
117 }
118}
119
120fn default_version() -> u32 {
121 1
122}
123
124#[derive(Debug, Clone, Serialize, Deserialize)]
126#[cfg_attr(feature = "schema", derive(JsonSchema))]
127#[serde(tag = "type", rename_all = "camelCase")]
128pub enum WidgetElement {
129 #[serde(rename = "vstack")]
131 VStack(VStackElement),
132 #[serde(rename = "hstack")]
134 HStack(HStackElement),
135 #[serde(rename = "zstack")]
137 ZStack(ZStackElement),
138 #[serde(rename = "grid")]
140 Grid(GridElement),
141 #[serde(rename = "container")]
143 Container(ContainerElement),
144 #[serde(rename = "text")]
146 Text(TextElement),
147 #[serde(rename = "image")]
149 Image(ImageElement),
150 #[serde(rename = "progress")]
152 Progress(ProgressElement),
153 #[serde(rename = "gauge")]
155 Gauge(GaugeElement),
156 #[serde(rename = "button")]
158 Button(ButtonElement),
159 #[serde(rename = "toggle")]
161 Toggle(ToggleElement),
162 #[serde(rename = "divider")]
164 Divider(DividerElement),
165 #[serde(rename = "spacer")]
167 Spacer(SpacerElement),
168 #[serde(rename = "date")]
170 Date(DateElement),
171 #[serde(rename = "chart")]
173 Chart(ChartElement),
174 #[serde(rename = "list")]
176 List(ListElement),
177 #[serde(rename = "link")]
179 Link(LinkElement),
180 #[serde(rename = "shape")]
182 Shape(ShapeElement),
183 #[serde(rename = "timer")]
185 Timer(TimerElement),
186 #[serde(rename = "canvas")]
188 Canvas(CanvasElement),
189 #[serde(rename = "label")]
191 Label(LabelElement),
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize, Default)]
196#[cfg_attr(feature = "schema", derive(JsonSchema))]
197#[serde(rename_all = "camelCase")]
198pub struct VStackElement {
199 #[serde(default, skip_serializing_if = "Vec::is_empty")]
201 pub children: Vec<WidgetElement>,
202 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub spacing: Option<f64>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
207 pub alignment: Option<HorizontalAlignment>,
208 #[serde(flatten)]
209 pub style: ElementStyle,
211}
212
213#[derive(Debug, Clone, Serialize, Deserialize, Default)]
215#[cfg_attr(feature = "schema", derive(JsonSchema))]
216#[serde(rename_all = "camelCase")]
217pub struct HStackElement {
218 #[serde(default, skip_serializing_if = "Vec::is_empty")]
220 pub children: Vec<WidgetElement>,
221 #[serde(default, skip_serializing_if = "Option::is_none")]
223 pub spacing: Option<f64>,
224 #[serde(default, skip_serializing_if = "Option::is_none")]
226 pub alignment: Option<VerticalAlignment>,
227 #[serde(flatten)]
228 pub style: ElementStyle,
230}
231
232#[derive(Debug, Clone, Serialize, Deserialize, Default)]
234#[cfg_attr(feature = "schema", derive(JsonSchema))]
235#[serde(rename_all = "camelCase")]
236pub struct ZStackElement {
237 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 pub children: Vec<WidgetElement>,
240 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub alignment: Option<String>,
243 #[serde(flatten)]
244 pub style: ElementStyle,
246}
247
248#[derive(Debug, Clone, Serialize, Deserialize, Default)]
250#[cfg_attr(feature = "schema", derive(JsonSchema))]
251#[serde(rename_all = "camelCase")]
252pub struct GridElement {
253 #[serde(default, skip_serializing_if = "Vec::is_empty")]
255 pub children: Vec<WidgetElement>,
256 #[serde(default = "default_columns")]
258 pub columns: u32,
259 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub spacing: Option<f64>,
262 #[serde(
264 rename = "rowSpacing",
265 default,
266 skip_serializing_if = "Option::is_none"
267 )]
268 pub row_spacing: Option<f64>,
269 #[serde(flatten)]
270 pub style: ElementStyle,
272}
273
274#[derive(Debug, Clone, Serialize, Deserialize, Default)]
276#[cfg_attr(feature = "schema", derive(JsonSchema))]
277#[serde(rename_all = "camelCase")]
278pub struct ContainerElement {
279 #[serde(default, skip_serializing_if = "Vec::is_empty")]
281 pub children: Vec<WidgetElement>,
282 #[serde(
284 rename = "contentAlignment",
285 default,
286 skip_serializing_if = "Option::is_none"
287 )]
288 pub content_alignment: Option<String>,
289 #[serde(flatten)]
290 pub style: ElementStyle,
292}
293
294#[derive(Debug, Clone, Serialize, Deserialize, Default)]
296#[cfg_attr(feature = "schema", derive(JsonSchema))]
297#[serde(rename_all = "camelCase")]
298pub struct TextElement {
299 pub content: String,
301 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
303 pub font_size: Option<f64>,
304 #[serde(
306 rename = "fontWeight",
307 default,
308 skip_serializing_if = "Option::is_none"
309 )]
310 pub font_weight: Option<FontWeight>,
311 #[serde(
313 rename = "fontDesign",
314 default,
315 skip_serializing_if = "Option::is_none"
316 )]
317 pub font_design: Option<FontDesign>,
318 #[serde(rename = "textStyle", default, skip_serializing_if = "Option::is_none")]
321 pub text_style: Option<TextStyle>,
322 #[serde(default, skip_serializing_if = "Option::is_none")]
324 pub color: Option<ColorValue>,
325 #[serde(default, skip_serializing_if = "Option::is_none")]
327 pub alignment: Option<TextAlignment>,
328 #[serde(rename = "lineLimit", default, skip_serializing_if = "Option::is_none")]
330 pub line_limit: Option<u32>,
331 #[serde(flatten)]
332 pub style: ElementStyle,
334}
335
336impl TextElement {
337 pub fn font_size(mut self, size: f64) -> Self {
339 self.font_size = Some(size);
340 self
341 }
342
343 pub fn font_weight(mut self, weight: FontWeight) -> Self {
345 self.font_weight = Some(weight);
346 self
347 }
348
349 pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
351 self.color = Some(color.into());
352 self
353 }
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, Default)]
358#[cfg_attr(feature = "schema", derive(JsonSchema))]
359#[serde(rename_all = "camelCase")]
360pub struct ImageElement {
361 #[serde(
363 rename = "systemName",
364 default,
365 skip_serializing_if = "Option::is_none"
366 )]
367 pub system_name: Option<String>,
368 #[serde(default, skip_serializing_if = "Option::is_none")]
370 pub data: Option<String>,
371 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub url: Option<String>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
376 pub size: Option<f64>,
377 #[serde(default, skip_serializing_if = "Option::is_none")]
379 pub color: Option<ColorValue>,
380 #[serde(
382 rename = "contentMode",
383 default,
384 skip_serializing_if = "Option::is_none"
385 )]
386 pub content_mode: Option<ContentMode>,
387 #[serde(flatten)]
388 pub style: ElementStyle,
390}
391
392#[derive(Debug, Clone, Serialize, Deserialize)]
394#[cfg_attr(feature = "schema", derive(JsonSchema))]
395#[serde(rename_all = "camelCase")]
396pub struct ProgressElement {
397 pub value: f64,
399 #[serde(default = "default_total")]
401 pub total: f64,
402 #[serde(default, skip_serializing_if = "Option::is_none")]
404 pub label: Option<String>,
405 #[serde(default, skip_serializing_if = "Option::is_none")]
407 pub tint: Option<ColorValue>,
408 #[serde(default, skip_serializing_if = "Option::is_none")]
410 pub color: Option<ColorValue>,
411 #[serde(rename = "barStyle", default, skip_serializing_if = "Option::is_none")]
413 pub bar_style: Option<ProgressStyle>,
414 #[serde(flatten)]
415 pub style: ElementStyle,
417}
418
419impl Default for ProgressElement {
420 fn default() -> Self {
421 Self {
422 value: 0.0,
423 total: 1.0,
424 label: None,
425 tint: None,
426 color: None,
427 bar_style: None,
428 style: ElementStyle::default(),
429 }
430 }
431}
432
433#[derive(Debug, Clone, Serialize, Deserialize, Default)]
435#[cfg_attr(feature = "schema", derive(JsonSchema))]
436#[serde(rename_all = "camelCase")]
437pub struct GaugeElement {
438 pub value: f64,
440 #[serde(default, skip_serializing_if = "Option::is_none")]
442 pub min: Option<f64>,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
445 pub max: Option<f64>,
446 #[serde(default, skip_serializing_if = "Option::is_none")]
448 pub label: Option<String>,
449 #[serde(
451 rename = "currentValueLabel",
452 default,
453 skip_serializing_if = "Option::is_none"
454 )]
455 pub current_value_label: Option<String>,
456 #[serde(default, skip_serializing_if = "Option::is_none")]
458 pub tint: Option<ColorValue>,
459 #[serde(default, skip_serializing_if = "Option::is_none")]
461 pub color: Option<ColorValue>,
462 #[serde(
464 rename = "gaugeStyle",
465 default,
466 skip_serializing_if = "Option::is_none"
467 )]
468 pub gauge_style: Option<GaugeStyle>,
469 #[serde(flatten)]
470 pub style: ElementStyle,
472}
473
474#[derive(Debug, Clone, Serialize, Deserialize, Default)]
476#[cfg_attr(feature = "schema", derive(JsonSchema))]
477#[serde(rename_all = "camelCase")]
478pub struct ButtonElement {
479 pub label: String,
481 #[serde(default, skip_serializing_if = "Option::is_none")]
483 pub url: Option<String>,
484 #[serde(default, skip_serializing_if = "Option::is_none")]
486 pub action: Option<String>,
487 #[serde(default, skip_serializing_if = "Option::is_none")]
489 pub color: Option<ColorValue>,
490 #[serde(
492 rename = "backgroundColor",
493 default,
494 skip_serializing_if = "Option::is_none"
495 )]
496 pub background_color: Option<ColorValue>,
497 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
499 pub font_size: Option<f64>,
500 #[serde(
502 rename = "textAlignment",
503 default,
504 skip_serializing_if = "Option::is_none"
505 )]
506 pub text_alignment: Option<TextAlignment>,
507 #[serde(flatten)]
508 pub style: ElementStyle,
510}
511
512#[derive(Debug, Clone, Serialize, Deserialize, Default)]
514#[cfg_attr(feature = "schema", derive(JsonSchema))]
515#[serde(rename_all = "camelCase")]
516pub struct ToggleElement {
517 #[serde(rename = "isOn")]
519 pub is_on: bool,
520 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub label: Option<String>,
523 #[serde(default, skip_serializing_if = "Option::is_none")]
525 pub tint: Option<ColorValue>,
526 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub action: Option<String>,
529 #[serde(flatten)]
530 pub style: ElementStyle,
532}
533
534#[derive(Debug, Clone, Serialize, Deserialize, Default)]
536#[cfg_attr(feature = "schema", derive(JsonSchema))]
537#[serde(rename_all = "camelCase")]
538pub struct DividerElement {
539 #[serde(default, skip_serializing_if = "Option::is_none")]
541 pub color: Option<ColorValue>,
542 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub thickness: Option<f64>,
545 #[serde(flatten)]
546 pub style: ElementStyle,
548}
549
550#[derive(Debug, Clone, Serialize, Deserialize, Default)]
552#[cfg_attr(feature = "schema", derive(JsonSchema))]
553#[serde(rename_all = "camelCase")]
554pub struct SpacerElement {
555 #[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
557 pub min_length: Option<f64>,
558}
559
560#[derive(Debug, Clone, Serialize, Deserialize, Default)]
562#[cfg_attr(feature = "schema", derive(JsonSchema))]
563#[serde(rename_all = "camelCase")]
564pub struct DateElement {
565 pub date: String,
567 #[serde(rename = "dateStyle", default, skip_serializing_if = "Option::is_none")]
569 pub date_style: Option<DateStyle>,
570 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
572 pub font_size: Option<f64>,
573 #[serde(default, skip_serializing_if = "Option::is_none")]
575 pub color: Option<ColorValue>,
576 #[serde(flatten)]
577 pub style: ElementStyle,
579}
580
581#[derive(Debug, Clone, Serialize, Deserialize, Default)]
583#[cfg_attr(feature = "schema", derive(JsonSchema))]
584#[serde(rename_all = "camelCase")]
585pub struct ChartElement {
586 #[serde(rename = "chartType")]
588 pub chart_type: ChartType,
589 #[serde(default, rename = "chartData", skip_serializing_if = "Vec::is_empty")]
591 pub chart_data: Vec<ChartDataPoint>,
592 #[serde(default, skip_serializing_if = "Option::is_none")]
594 pub tint: Option<ColorValue>,
595 #[serde(flatten)]
596 pub style: ElementStyle,
598}
599
600#[derive(Debug, Clone, Serialize, Deserialize, Default)]
602#[cfg_attr(feature = "schema", derive(JsonSchema))]
603#[serde(rename_all = "camelCase")]
604pub struct ListElement {
605 #[serde(default, skip_serializing_if = "Vec::is_empty")]
607 pub items: Vec<ListItem>,
608 #[serde(default, skip_serializing_if = "Vec::is_empty")]
610 pub children: Vec<WidgetElement>,
611 #[serde(default, skip_serializing_if = "Option::is_none")]
613 pub spacing: Option<f64>,
614 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
616 pub font_size: Option<f64>,
617 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub color: Option<ColorValue>,
620 #[serde(flatten)]
621 pub style: ElementStyle,
623}
624
625#[derive(Debug, Clone, Serialize, Deserialize, Default)]
627#[cfg_attr(feature = "schema", derive(JsonSchema))]
628#[serde(rename_all = "camelCase")]
629pub struct LinkElement {
630 #[serde(default, skip_serializing_if = "Vec::is_empty")]
632 pub children: Vec<WidgetElement>,
633 #[serde(default, skip_serializing_if = "Option::is_none")]
635 pub url: Option<String>,
636 #[serde(default, skip_serializing_if = "Option::is_none")]
638 pub action: Option<String>,
639 #[serde(flatten)]
640 pub style: ElementStyle,
642}
643
644#[derive(Debug, Clone, Serialize, Deserialize, Default)]
646#[cfg_attr(feature = "schema", derive(JsonSchema))]
647#[serde(rename_all = "camelCase")]
648pub struct ShapeElement {
649 #[serde(rename = "shapeType")]
651 pub shape_type: ShapeType,
652 #[serde(default, skip_serializing_if = "Option::is_none")]
654 pub fill: Option<ColorValue>,
655 #[serde(default, skip_serializing_if = "Option::is_none")]
657 pub stroke: Option<ColorValue>,
658 #[serde(
660 rename = "strokeWidth",
661 default,
662 skip_serializing_if = "Option::is_none"
663 )]
664 pub stroke_width: Option<f64>,
665 #[serde(default, skip_serializing_if = "Option::is_none")]
667 pub size: Option<f64>,
668 #[serde(flatten)]
669 pub style: ElementStyle,
671}
672
673#[derive(Debug, Clone, Serialize, Deserialize, Default)]
675#[cfg_attr(feature = "schema", derive(JsonSchema))]
676#[serde(rename_all = "camelCase")]
677pub struct TimerElement {
678 #[serde(rename = "targetDate")]
680 pub target_date: String,
681 #[serde(default, skip_serializing_if = "Option::is_none")]
683 pub counting: Option<TimerCounting>,
684 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
686 pub font_size: Option<f64>,
687 #[serde(
689 rename = "fontWeight",
690 default,
691 skip_serializing_if = "Option::is_none"
692 )]
693 pub font_weight: Option<FontWeight>,
694 #[serde(default, skip_serializing_if = "Option::is_none")]
696 pub color: Option<ColorValue>,
697 #[serde(flatten)]
698 pub style: ElementStyle,
700}
701
702#[derive(Debug, Clone, Serialize, Deserialize, Default)]
704#[cfg_attr(feature = "schema", derive(JsonSchema))]
705#[serde(rename_all = "camelCase")]
706pub struct CanvasElement {
707 pub width: f64,
709 pub height: f64,
711 #[serde(default, skip_serializing_if = "Vec::is_empty")]
713 pub elements: Vec<CanvasDrawCommand>,
714 #[serde(flatten)]
715 pub style: ElementStyle,
717}
718
719#[derive(Debug, Clone, Serialize, Deserialize, Default)]
721#[cfg_attr(feature = "schema", derive(JsonSchema))]
722#[serde(rename_all = "camelCase")]
723pub struct LabelElement {
724 pub text: String,
726 #[serde(rename = "systemName")]
728 pub system_name: String,
729 #[serde(rename = "iconColor", default, skip_serializing_if = "Option::is_none")]
731 pub icon_color: Option<ColorValue>,
732 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
734 pub font_size: Option<f64>,
735 #[serde(
737 rename = "fontWeight",
738 default,
739 skip_serializing_if = "Option::is_none"
740 )]
741 pub font_weight: Option<FontWeight>,
742 #[serde(default, skip_serializing_if = "Option::is_none")]
744 pub color: Option<ColorValue>,
745 #[serde(default, skip_serializing_if = "Option::is_none")]
747 pub spacing: Option<f64>,
748 #[serde(flatten)]
749 pub style: ElementStyle,
751}
752
753impl From<VStackElement> for WidgetElement {
754 fn from(value: VStackElement) -> Self {
755 WidgetElement::VStack(value)
756 }
757}
758
759impl From<HStackElement> for WidgetElement {
760 fn from(value: HStackElement) -> Self {
761 WidgetElement::HStack(value)
762 }
763}
764
765impl From<ZStackElement> for WidgetElement {
766 fn from(value: ZStackElement) -> Self {
767 WidgetElement::ZStack(value)
768 }
769}
770
771impl From<GridElement> for WidgetElement {
772 fn from(value: GridElement) -> Self {
773 WidgetElement::Grid(value)
774 }
775}
776
777impl From<ContainerElement> for WidgetElement {
778 fn from(value: ContainerElement) -> Self {
779 WidgetElement::Container(value)
780 }
781}
782
783impl From<TextElement> for WidgetElement {
784 fn from(value: TextElement) -> Self {
785 WidgetElement::Text(value)
786 }
787}
788
789impl From<ImageElement> for WidgetElement {
790 fn from(value: ImageElement) -> Self {
791 WidgetElement::Image(value)
792 }
793}
794
795impl From<ProgressElement> for WidgetElement {
796 fn from(value: ProgressElement) -> Self {
797 WidgetElement::Progress(value)
798 }
799}
800
801impl From<GaugeElement> for WidgetElement {
802 fn from(value: GaugeElement) -> Self {
803 WidgetElement::Gauge(value)
804 }
805}
806
807impl From<ButtonElement> for WidgetElement {
808 fn from(value: ButtonElement) -> Self {
809 WidgetElement::Button(value)
810 }
811}
812
813impl From<ToggleElement> for WidgetElement {
814 fn from(value: ToggleElement) -> Self {
815 WidgetElement::Toggle(value)
816 }
817}
818
819impl From<DividerElement> for WidgetElement {
820 fn from(value: DividerElement) -> Self {
821 WidgetElement::Divider(value)
822 }
823}
824
825impl From<SpacerElement> for WidgetElement {
826 fn from(value: SpacerElement) -> Self {
827 WidgetElement::Spacer(value)
828 }
829}
830
831impl From<DateElement> for WidgetElement {
832 fn from(value: DateElement) -> Self {
833 WidgetElement::Date(value)
834 }
835}
836
837impl From<ChartElement> for WidgetElement {
838 fn from(value: ChartElement) -> Self {
839 WidgetElement::Chart(value)
840 }
841}
842
843impl From<ListElement> for WidgetElement {
844 fn from(value: ListElement) -> Self {
845 WidgetElement::List(value)
846 }
847}
848
849impl From<LinkElement> for WidgetElement {
850 fn from(value: LinkElement) -> Self {
851 WidgetElement::Link(value)
852 }
853}
854
855impl From<ShapeElement> for WidgetElement {
856 fn from(value: ShapeElement) -> Self {
857 WidgetElement::Shape(value)
858 }
859}
860
861impl From<TimerElement> for WidgetElement {
862 fn from(value: TimerElement) -> Self {
863 WidgetElement::Timer(value)
864 }
865}
866
867impl From<CanvasElement> for WidgetElement {
868 fn from(value: CanvasElement) -> Self {
869 WidgetElement::Canvas(value)
870 }
871}
872
873impl From<LabelElement> for WidgetElement {
874 fn from(value: LabelElement) -> Self {
875 WidgetElement::Label(value)
876 }
877}
878
879pub fn text(content: impl Into<String>) -> TextElement {
881 TextElement {
882 content: content.into(),
883 ..Default::default()
884 }
885}
886
887pub fn vstack(children: Vec<WidgetElement>) -> VStackElement {
889 VStackElement {
890 children,
891 ..Default::default()
892 }
893}
894
895pub fn hstack(children: Vec<WidgetElement>) -> HStackElement {
897 HStackElement {
898 children,
899 ..Default::default()
900 }
901}
902
903fn default_columns() -> u32 {
904 2
905}
906fn default_total() -> f64 {
907 1.0
908}
909
910#[cfg(test)]
911mod tests {
912 use super::*;
913 use std::path::PathBuf;
914
915 #[test]
916 fn option_none_omitted_from_json() {
917 let cfg = WidgetConfig::small(TextElement {
918 content: "hi".into(),
919 font_size: Some(12.0),
920 ..Default::default()
921 });
922 let json = serde_json::to_string(&cfg).unwrap();
923 assert!(!json.contains("null"), "JSON must not contain null: {json}");
924 assert!(!json.contains("\"medium\""));
925 assert!(!json.contains("\"fontWeight\""));
926 let back: WidgetConfig = serde_json::from_str(&json).unwrap();
927 assert!(back.medium.is_none());
928 match back.small.unwrap() {
929 WidgetElement::Text(TextElement {
930 font_weight,
931 content,
932 ..
933 }) => {
934 assert!(font_weight.is_none());
935 assert_eq!(content, "hi");
936 }
937 other => panic!("expected text, got {other:?}"),
938 }
939 }
940
941 #[test]
942 fn ergonomic_builders_serialize() {
943 let cfg = WidgetConfig::small(vstack(vec![text("72°")
944 .font_size(36.0)
945 .font_weight(FontWeight::Bold)
946 .color("#fff")
947 .into()]));
948 let v = serde_json::to_value(&cfg).unwrap();
949 assert_eq!(v["small"]["type"], "vstack");
950 assert_eq!(v["small"]["children"][0]["type"], "text");
951 assert_eq!(v["small"]["children"][0]["content"], "72°");
952 assert_eq!(v["small"]["children"][0]["fontSize"], 36.0);
953 assert_eq!(v["small"]["children"][0]["fontWeight"], "bold");
954 assert_eq!(v["small"]["children"][0]["color"], "#fff");
955 }
956
957 fn walk_json_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
958 let Ok(entries) = std::fs::read_dir(dir) else {
959 return;
960 };
961 for entry in entries.flatten() {
962 let path = entry.path();
963 if path.is_dir() {
964 walk_json_files(&path, out);
965 } else if path.extension().and_then(|e| e.to_str()) == Some("json") {
966 out.push(path);
967 }
968 }
969 }
970
971 #[test]
974 fn wire_format_unchanged() {
975 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
976 let mut files = Vec::new();
977 walk_json_files(&root, &mut files);
978 assert!(
979 !files.is_empty(),
980 "expected fixtures under {}",
981 root.display()
982 );
983 for path in files {
984 let raw = std::fs::read_to_string(&path).unwrap();
985 let cfg: WidgetConfig = serde_json::from_str(&raw).unwrap_or_else(|e| {
986 panic!("deserialize {}: {e}", path.display());
987 });
988 let encoded = serde_json::to_value(&cfg).unwrap();
989 let again: WidgetConfig = serde_json::from_value(encoded.clone()).unwrap();
990 assert_eq!(
991 encoded,
992 serde_json::to_value(&again).unwrap(),
993 "wire drift in {}",
994 path.display()
995 );
996 let out = serde_json::to_string(&cfg).unwrap();
997 assert!(
998 !out.contains("null"),
999 "{} serialized with null: {out}",
1000 path.display()
1001 );
1002 }
1003 }
1004}