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(
413 rename = "barStyle",
414 default,
415 skip_serializing_if = "Option::is_none"
416 )]
417 pub bar_style: Option<ProgressStyle>,
418 #[serde(flatten)]
419 pub style: ElementStyle,
421}
422
423impl Default for ProgressElement {
424 fn default() -> Self {
425 Self {
426 value: 0.0,
427 total: 1.0,
428 label: None,
429 tint: None,
430 color: None,
431 bar_style: None,
432 style: ElementStyle::default(),
433 }
434 }
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, Default)]
439#[cfg_attr(feature = "schema", derive(JsonSchema))]
440#[serde(rename_all = "camelCase")]
441pub struct GaugeElement {
442 pub value: f64,
444 #[serde(default, skip_serializing_if = "Option::is_none")]
446 pub min: Option<f64>,
447 #[serde(default, skip_serializing_if = "Option::is_none")]
449 pub max: Option<f64>,
450 #[serde(default, skip_serializing_if = "Option::is_none")]
452 pub label: Option<String>,
453 #[serde(
455 rename = "currentValueLabel",
456 default,
457 skip_serializing_if = "Option::is_none"
458 )]
459 pub current_value_label: Option<String>,
460 #[serde(default, skip_serializing_if = "Option::is_none")]
462 pub tint: Option<ColorValue>,
463 #[serde(default, skip_serializing_if = "Option::is_none")]
465 pub color: Option<ColorValue>,
466 #[serde(
468 rename = "gaugeStyle",
469 default,
470 skip_serializing_if = "Option::is_none"
471 )]
472 pub gauge_style: Option<GaugeStyle>,
473 #[serde(flatten)]
474 pub style: ElementStyle,
476}
477
478#[derive(Debug, Clone, Serialize, Deserialize, Default)]
480#[cfg_attr(feature = "schema", derive(JsonSchema))]
481#[serde(rename_all = "camelCase")]
482pub struct ButtonElement {
483 pub label: String,
485 #[serde(default, skip_serializing_if = "Option::is_none")]
487 pub url: Option<String>,
488 #[serde(default, skip_serializing_if = "Option::is_none")]
490 pub action: Option<String>,
491 #[serde(default, skip_serializing_if = "Option::is_none")]
493 pub color: Option<ColorValue>,
494 #[serde(
496 rename = "backgroundColor",
497 default,
498 skip_serializing_if = "Option::is_none"
499 )]
500 pub background_color: Option<ColorValue>,
501 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
503 pub font_size: Option<f64>,
504 #[serde(
506 rename = "textAlignment",
507 default,
508 skip_serializing_if = "Option::is_none"
509 )]
510 pub text_alignment: Option<TextAlignment>,
511 #[serde(flatten)]
512 pub style: ElementStyle,
514}
515
516#[derive(Debug, Clone, Serialize, Deserialize, Default)]
518#[cfg_attr(feature = "schema", derive(JsonSchema))]
519#[serde(rename_all = "camelCase")]
520pub struct ToggleElement {
521 #[serde(rename = "isOn")]
523 pub is_on: bool,
524 #[serde(default, skip_serializing_if = "Option::is_none")]
526 pub label: Option<String>,
527 #[serde(default, skip_serializing_if = "Option::is_none")]
529 pub tint: Option<ColorValue>,
530 #[serde(default, skip_serializing_if = "Option::is_none")]
532 pub action: Option<String>,
533 #[serde(flatten)]
534 pub style: ElementStyle,
536}
537
538#[derive(Debug, Clone, Serialize, Deserialize, Default)]
540#[cfg_attr(feature = "schema", derive(JsonSchema))]
541#[serde(rename_all = "camelCase")]
542pub struct DividerElement {
543 #[serde(default, skip_serializing_if = "Option::is_none")]
545 pub color: Option<ColorValue>,
546 #[serde(default, skip_serializing_if = "Option::is_none")]
548 pub thickness: Option<f64>,
549 #[serde(flatten)]
550 pub style: ElementStyle,
552}
553
554#[derive(Debug, Clone, Serialize, Deserialize, Default)]
556#[cfg_attr(feature = "schema", derive(JsonSchema))]
557#[serde(rename_all = "camelCase")]
558pub struct SpacerElement {
559 #[serde(rename = "minLength", default, skip_serializing_if = "Option::is_none")]
561 pub min_length: Option<f64>,
562}
563
564#[derive(Debug, Clone, Serialize, Deserialize, Default)]
566#[cfg_attr(feature = "schema", derive(JsonSchema))]
567#[serde(rename_all = "camelCase")]
568pub struct DateElement {
569 pub date: String,
571 #[serde(rename = "dateStyle", default, skip_serializing_if = "Option::is_none")]
573 pub date_style: Option<DateStyle>,
574 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
576 pub font_size: Option<f64>,
577 #[serde(default, skip_serializing_if = "Option::is_none")]
579 pub color: Option<ColorValue>,
580 #[serde(flatten)]
581 pub style: ElementStyle,
583}
584
585#[derive(Debug, Clone, Serialize, Deserialize, Default)]
587#[cfg_attr(feature = "schema", derive(JsonSchema))]
588#[serde(rename_all = "camelCase")]
589pub struct ChartElement {
590 #[serde(rename = "chartType")]
592 pub chart_type: ChartType,
593 #[serde(default, rename = "chartData", skip_serializing_if = "Vec::is_empty")]
595 pub chart_data: Vec<ChartDataPoint>,
596 #[serde(default, skip_serializing_if = "Option::is_none")]
598 pub tint: Option<ColorValue>,
599 #[serde(flatten)]
600 pub style: ElementStyle,
602}
603
604#[derive(Debug, Clone, Serialize, Deserialize, Default)]
606#[cfg_attr(feature = "schema", derive(JsonSchema))]
607#[serde(rename_all = "camelCase")]
608pub struct ListElement {
609 #[serde(default, skip_serializing_if = "Vec::is_empty")]
611 pub items: Vec<ListItem>,
612 #[serde(default, skip_serializing_if = "Option::is_none")]
614 pub spacing: Option<f64>,
615 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
617 pub font_size: Option<f64>,
618 #[serde(default, skip_serializing_if = "Option::is_none")]
620 pub color: Option<ColorValue>,
621 #[serde(flatten)]
622 pub style: ElementStyle,
624}
625
626#[derive(Debug, Clone, Serialize, Deserialize, Default)]
628#[cfg_attr(feature = "schema", derive(JsonSchema))]
629#[serde(rename_all = "camelCase")]
630pub struct LinkElement {
631 #[serde(default, skip_serializing_if = "Vec::is_empty")]
633 pub children: Vec<WidgetElement>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub url: Option<String>,
637 #[serde(default, skip_serializing_if = "Option::is_none")]
639 pub action: Option<String>,
640 #[serde(flatten)]
641 pub style: ElementStyle,
643}
644
645#[derive(Debug, Clone, Serialize, Deserialize, Default)]
647#[cfg_attr(feature = "schema", derive(JsonSchema))]
648#[serde(rename_all = "camelCase")]
649pub struct ShapeElement {
650 #[serde(rename = "shapeType")]
652 pub shape_type: ShapeType,
653 #[serde(default, skip_serializing_if = "Option::is_none")]
655 pub fill: Option<ColorValue>,
656 #[serde(default, skip_serializing_if = "Option::is_none")]
658 pub stroke: Option<ColorValue>,
659 #[serde(
661 rename = "strokeWidth",
662 default,
663 skip_serializing_if = "Option::is_none"
664 )]
665 pub stroke_width: Option<f64>,
666 #[serde(default, skip_serializing_if = "Option::is_none")]
668 pub size: Option<f64>,
669 #[serde(flatten)]
670 pub style: ElementStyle,
672}
673
674#[derive(Debug, Clone, Serialize, Deserialize, Default)]
676#[cfg_attr(feature = "schema", derive(JsonSchema))]
677#[serde(rename_all = "camelCase")]
678pub struct TimerElement {
679 #[serde(rename = "targetDate")]
681 pub target_date: String,
682 #[serde(default, skip_serializing_if = "Option::is_none")]
684 pub counting: Option<TimerCounting>,
685 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
687 pub font_size: Option<f64>,
688 #[serde(
690 rename = "fontWeight",
691 default,
692 skip_serializing_if = "Option::is_none"
693 )]
694 pub font_weight: Option<FontWeight>,
695 #[serde(default, skip_serializing_if = "Option::is_none")]
697 pub color: Option<ColorValue>,
698 #[serde(flatten)]
699 pub style: ElementStyle,
701}
702
703#[derive(Debug, Clone, Serialize, Deserialize, Default)]
705#[cfg_attr(feature = "schema", derive(JsonSchema))]
706#[serde(rename_all = "camelCase")]
707pub struct CanvasElement {
708 pub width: f64,
710 pub height: f64,
712 #[serde(default, skip_serializing_if = "Vec::is_empty")]
714 pub elements: Vec<CanvasDrawCommand>,
715 #[serde(flatten)]
716 pub style: ElementStyle,
718}
719
720#[derive(Debug, Clone, Serialize, Deserialize, Default)]
722#[cfg_attr(feature = "schema", derive(JsonSchema))]
723#[serde(rename_all = "camelCase")]
724pub struct LabelElement {
725 pub text: String,
727 #[serde(rename = "systemName")]
729 pub system_name: String,
730 #[serde(rename = "iconColor", default, skip_serializing_if = "Option::is_none")]
732 pub icon_color: Option<ColorValue>,
733 #[serde(rename = "fontSize", default, skip_serializing_if = "Option::is_none")]
735 pub font_size: Option<f64>,
736 #[serde(
738 rename = "fontWeight",
739 default,
740 skip_serializing_if = "Option::is_none"
741 )]
742 pub font_weight: Option<FontWeight>,
743 #[serde(default, skip_serializing_if = "Option::is_none")]
745 pub color: Option<ColorValue>,
746 #[serde(default, skip_serializing_if = "Option::is_none")]
748 pub spacing: Option<f64>,
749 #[serde(flatten)]
750 pub style: ElementStyle,
752}
753
754impl From<VStackElement> for WidgetElement {
755 fn from(value: VStackElement) -> Self {
756 WidgetElement::VStack(value)
757 }
758}
759
760impl From<HStackElement> for WidgetElement {
761 fn from(value: HStackElement) -> Self {
762 WidgetElement::HStack(value)
763 }
764}
765
766impl From<ZStackElement> for WidgetElement {
767 fn from(value: ZStackElement) -> Self {
768 WidgetElement::ZStack(value)
769 }
770}
771
772impl From<GridElement> for WidgetElement {
773 fn from(value: GridElement) -> Self {
774 WidgetElement::Grid(value)
775 }
776}
777
778impl From<ContainerElement> for WidgetElement {
779 fn from(value: ContainerElement) -> Self {
780 WidgetElement::Container(value)
781 }
782}
783
784impl From<TextElement> for WidgetElement {
785 fn from(value: TextElement) -> Self {
786 WidgetElement::Text(value)
787 }
788}
789
790impl From<ImageElement> for WidgetElement {
791 fn from(value: ImageElement) -> Self {
792 WidgetElement::Image(value)
793 }
794}
795
796impl From<ProgressElement> for WidgetElement {
797 fn from(value: ProgressElement) -> Self {
798 WidgetElement::Progress(value)
799 }
800}
801
802impl From<GaugeElement> for WidgetElement {
803 fn from(value: GaugeElement) -> Self {
804 WidgetElement::Gauge(value)
805 }
806}
807
808impl From<ButtonElement> for WidgetElement {
809 fn from(value: ButtonElement) -> Self {
810 WidgetElement::Button(value)
811 }
812}
813
814impl From<ToggleElement> for WidgetElement {
815 fn from(value: ToggleElement) -> Self {
816 WidgetElement::Toggle(value)
817 }
818}
819
820impl From<DividerElement> for WidgetElement {
821 fn from(value: DividerElement) -> Self {
822 WidgetElement::Divider(value)
823 }
824}
825
826impl From<SpacerElement> for WidgetElement {
827 fn from(value: SpacerElement) -> Self {
828 WidgetElement::Spacer(value)
829 }
830}
831
832impl From<DateElement> for WidgetElement {
833 fn from(value: DateElement) -> Self {
834 WidgetElement::Date(value)
835 }
836}
837
838impl From<ChartElement> for WidgetElement {
839 fn from(value: ChartElement) -> Self {
840 WidgetElement::Chart(value)
841 }
842}
843
844impl From<ListElement> for WidgetElement {
845 fn from(value: ListElement) -> Self {
846 WidgetElement::List(value)
847 }
848}
849
850impl From<LinkElement> for WidgetElement {
851 fn from(value: LinkElement) -> Self {
852 WidgetElement::Link(value)
853 }
854}
855
856impl From<ShapeElement> for WidgetElement {
857 fn from(value: ShapeElement) -> Self {
858 WidgetElement::Shape(value)
859 }
860}
861
862impl From<TimerElement> for WidgetElement {
863 fn from(value: TimerElement) -> Self {
864 WidgetElement::Timer(value)
865 }
866}
867
868impl From<CanvasElement> for WidgetElement {
869 fn from(value: CanvasElement) -> Self {
870 WidgetElement::Canvas(value)
871 }
872}
873
874impl From<LabelElement> for WidgetElement {
875 fn from(value: LabelElement) -> Self {
876 WidgetElement::Label(value)
877 }
878}
879
880pub fn text(content: impl Into<String>) -> TextElement {
882 TextElement {
883 content: content.into(),
884 ..Default::default()
885 }
886}
887
888pub fn vstack(children: Vec<WidgetElement>) -> VStackElement {
890 VStackElement {
891 children,
892 ..Default::default()
893 }
894}
895
896pub fn hstack(children: Vec<WidgetElement>) -> HStackElement {
898 HStackElement {
899 children,
900 ..Default::default()
901 }
902}
903
904fn default_columns() -> u32 {
905 2
906}
907fn default_total() -> f64 {
908 1.0
909}
910
911#[cfg(test)]
912mod tests {
913 use super::*;
914 use std::path::PathBuf;
915
916 #[test]
917 fn option_none_omitted_from_json() {
918 let cfg = WidgetConfig::small(TextElement {
919 content: "hi".into(),
920 font_size: Some(12.0),
921 ..Default::default()
922 });
923 let json = serde_json::to_string(&cfg).unwrap();
924 assert!(!json.contains("null"), "JSON must not contain null: {json}");
925 assert!(!json.contains("\"medium\""));
926 assert!(!json.contains("\"fontWeight\""));
927 let back: WidgetConfig = serde_json::from_str(&json).unwrap();
928 assert!(back.medium.is_none());
929 match back.small.unwrap() {
930 WidgetElement::Text(TextElement {
931 font_weight,
932 content,
933 ..
934 }) => {
935 assert!(font_weight.is_none());
936 assert_eq!(content, "hi");
937 }
938 other => panic!("expected text, got {other:?}"),
939 }
940 }
941
942 #[test]
943 fn ergonomic_builders_serialize() {
944 let cfg = WidgetConfig::small(vstack(vec![text("72°")
945 .font_size(36.0)
946 .font_weight(FontWeight::Bold)
947 .color("#fff")
948 .into()]));
949 let v = serde_json::to_value(&cfg).unwrap();
950 assert_eq!(v["small"]["type"], "vstack");
951 assert_eq!(v["small"]["children"][0]["type"], "text");
952 assert_eq!(v["small"]["children"][0]["content"], "72°");
953 assert_eq!(v["small"]["children"][0]["fontSize"], 36.0);
954 assert_eq!(v["small"]["children"][0]["fontWeight"], "bold");
955 assert_eq!(v["small"]["children"][0]["color"], "#fff");
956 }
957
958 fn walk_json_files(dir: &std::path::Path, out: &mut Vec<PathBuf>) {
959 let Ok(entries) = std::fs::read_dir(dir) else {
960 return;
961 };
962 for entry in entries.flatten() {
963 let path = entry.path();
964 if path.is_dir() {
965 walk_json_files(&path, out);
966 } else if path.extension().and_then(|e| e.to_str()) == Some("json") {
967 out.push(path);
968 }
969 }
970 }
971
972 #[test]
975 fn wire_format_unchanged() {
976 let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures");
977 let mut files = Vec::new();
978 walk_json_files(&root, &mut files);
979 assert!(
980 !files.is_empty(),
981 "expected fixtures under {}",
982 root.display()
983 );
984 for path in files {
985 let raw = std::fs::read_to_string(&path).unwrap();
986 let cfg: WidgetConfig = serde_json::from_str(&raw).unwrap_or_else(|e| {
987 panic!("deserialize {}: {e}", path.display());
988 });
989 let encoded = serde_json::to_value(&cfg).unwrap();
990 let again: WidgetConfig = serde_json::from_value(encoded.clone()).unwrap();
991 assert_eq!(
992 encoded,
993 serde_json::to_value(&again).unwrap(),
994 "wire drift in {}",
995 path.display()
996 );
997 let out = serde_json::to_string(&cfg).unwrap();
998 assert!(
999 !out.contains("null"),
1000 "{} serialized with null: {out}",
1001 path.display()
1002 );
1003 }
1004 }
1005}